From da0219deb019a8586a27d21a29b91574244cf0c9 Mon Sep 17 00:00:00 2001 From: Ana Hobden Date: Tue, 18 Oct 2022 11:03:19 -0700 Subject: [PATCH] Find xpath crate, scaffold more --- Cargo.lock | 41 +++++++ Cargo.toml | 26 ++-- src/actions/base/darwin/bootstrap_volume.rs | 116 ++++++++++++++++++ .../base/darwin/create_synthetic_objects.rs | 116 ++++++++++++++++++ src/actions/base/darwin/create_volume.rs | 116 ++++++++++++++++++ src/actions/base/darwin/enable_ownership.rs | 116 ++++++++++++++++++ src/actions/base/darwin/encrypt_volume.rs | 116 ++++++++++++++++++ src/actions/base/darwin/mod.rs | 13 ++ src/actions/base/darwin/unmount_volume.rs | 116 ++++++++++++++++++ src/actions/base/mod.rs | 1 + src/actions/meta/darwin/create_apfs_volume.rs | 77 ++++++++---- src/actions/mod.rs | 42 +++++++ src/lib.rs | 14 ++- src/planner/darwin/multi_user.rs | 29 ++++- 14 files changed, 893 insertions(+), 46 deletions(-) create mode 100644 src/actions/base/darwin/bootstrap_volume.rs create mode 100644 src/actions/base/darwin/create_synthetic_objects.rs create mode 100644 src/actions/base/darwin/create_volume.rs create mode 100644 src/actions/base/darwin/enable_ownership.rs create mode 100644 src/actions/base/darwin/encrypt_volume.rs create mode 100644 src/actions/base/darwin/mod.rs create mode 100644 src/actions/base/darwin/unmount_volume.rs diff --git a/Cargo.lock b/Cargo.lock index a9b85c0..f70f0d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -758,6 +758,8 @@ dependencies = [ "serde", "serde_json", "serde_with", + "sxd-document", + "sxd-xpath", "tar", "target-lexicon", "tempdir", @@ -1202,6 +1204,12 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +[[package]] +name = "peresil" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f658886ed52e196e850cfbbfddab9eaa7f6d90dd0929e264c31e5cec07e09e57" + [[package]] name = "pin-project" version = "1.0.12" @@ -1287,6 +1295,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.21" @@ -1651,6 +1665,27 @@ dependencies = [ "is_ci", ] +[[package]] +name = "sxd-document" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d82f37be9faf1b10a82c4bd492b74f698e40082f0f40de38ab275f31d42078" +dependencies = [ + "peresil", + "typed-arena", +] + +[[package]] +name = "sxd-xpath" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36e39da5d30887b5690e29de4c5ebb8ddff64ebd9933f98a01daaa4fd11b36ea" +dependencies = [ + "peresil", + "quick-error", + "sxd-document", +] + [[package]] name = "syn" version = "1.0.99" @@ -1921,6 +1956,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "typed-arena" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9b2228007eba4120145f785df0f6c92ea538f5a3635a612ecf4e334c8c1446d" + [[package]] name = "typetag" version = "0.2.3" diff --git a/Cargo.toml b/Cargo.toml index be804fd..7843ba7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,30 +9,32 @@ resolver = "2" async-tar = "0.4.2" async-trait = "0.1.57" atty = "0.2.14" +bytes = "1.2.1" clap = { version = "4", features = ["derive", "env"] } color-eyre = "0.6.2" crossterm = { version = "0.25.0", features = ["event-stream"] } eyre = "0.6.8" futures = "0.3.24" +glob = "0.3.0" +nix = { version = "0.25.0", features = ["user", "fs"], default-features = false } owo-colors = { version = "3.5.0", features = [ "supports-colors" ] } reqwest = { version = "0.11.11", default-features = false, features = ["rustls-tls", "stream"] } +serde = { version = "1.0.144", features = ["derive"] } +serde_json = "1.0.85" +serde_with = "2.0.1" +tar = "0.4.38" target-lexicon = "0.12.4" +tempdir = { version = "0.3.7"} thiserror = "1.0.33" tokio = { version = "1.21.0", features = ["time", "io-std", "process", "fs", "tracing", "rt-multi-thread", "macros", "io-util"] } tokio-util = { version = "0.7", features = ["io"] } tracing = { version = "0.1.36", features = [ "valuable" ] } tracing-error = "0.2.0" tracing-subscriber = { version = "0.3.15", features = [ "env-filter", "valuable" ] } -valuable = { version = "0.1.0", features = ["derive"] } -tempdir = { version = "0.3.7"} -glob = "0.3.0" -xz2 = { version = "0.1.7", features = ["static", "tokio"] } -bytes = "1.2.1" -tar = "0.4.38" -nix = { version = "0.25.0", features = ["user", "fs"], default-features = false } -walkdir = "2.3.2" -serde = { version = "1.0.144", features = ["derive"] } -url = { version = "2.3.1", features = ["serde"] } -serde_json = "1.0.85" typetag = "0.2.3" -serde_with = "2.0.1" +url = { version = "2.3.1", features = ["serde"] } +valuable = { version = "0.1.0", features = ["derive"] } +walkdir = "2.3.2" +sxd-xpath = "0.4.2" +xz2 = { version = "0.1.7", features = ["static", "tokio"] } +sxd-document = "0.3.2" diff --git a/src/actions/base/darwin/bootstrap_volume.rs b/src/actions/base/darwin/bootstrap_volume.rs new file mode 100644 index 0000000..0b2de35 --- /dev/null +++ b/src/actions/base/darwin/bootstrap_volume.rs @@ -0,0 +1,116 @@ +use serde::Serialize; +use tokio::process::Command; + +use crate::execute_command; + +use crate::actions::{Action, ActionDescription, ActionState, Actionable}; + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct BootstrapVolume { + unit: String, + action_state: ActionState, +} + +impl BootstrapVolume { + #[tracing::instrument(skip_all)] + pub async fn plan(unit: String) -> Result { + Ok(Self { + unit, + action_state: ActionState::Uncompleted, + }) + } +} + +#[async_trait::async_trait] +impl Actionable for BootstrapVolume { + type Error = BootstrapVolumeError; + + fn describe_execute(&self) -> Vec { + if self.action_state == ActionState::Completed { + vec![] + } else { + vec![ActionDescription::new( + "Start the systemd Nix service and socket".to_string(), + vec![ + "The `nix` command line tool communicates with a running Nix daemon managed by your init system".to_string() + ] + )] + } + } + + #[tracing::instrument(skip_all, fields( + unit = %self.unit, + ))] + async fn execute(&mut self) -> Result<(), Self::Error> { + let Self { unit, action_state } = self; + if *action_state == ActionState::Completed { + tracing::trace!("Already completed: Starting systemd unit"); + return Ok(()); + } + tracing::debug!("Starting systemd unit"); + + // TODO(@Hoverbear): Handle proxy vars + execute_command( + Command::new("systemctl") + .arg("enable") + .arg("--now") + .arg(format!("{unit}")), + ) + .await + .map_err(BootstrapVolumeError::Command)?; + + tracing::trace!("Started systemd unit"); + *action_state = ActionState::Completed; + Ok(()) + } + + fn describe_revert(&self) -> Vec { + if self.action_state == ActionState::Uncompleted { + vec![] + } else { + vec![ActionDescription::new( + "Stop the systemd Nix service and socket".to_string(), + vec![ + "The `nix` command line tool communicates with a running Nix daemon managed by your init system".to_string() + ] + )] + } + } + + #[tracing::instrument(skip_all, fields( + unit = %self.unit, + ))] + async fn revert(&mut self) -> Result<(), Self::Error> { + let Self { unit, action_state } = self; + if *action_state == ActionState::Uncompleted { + tracing::trace!("Already reverted: Stopping systemd unit"); + return Ok(()); + } + tracing::debug!("Stopping systemd unit"); + + // TODO(@Hoverbear): Handle proxy vars + execute_command(Command::new("systemctl").arg("stop").arg(format!("{unit}"))) + .await + .map_err(BootstrapVolumeError::Command)?; + + tracing::trace!("Stopped systemd unit"); + *action_state = ActionState::Completed; + Ok(()) + } +} + +impl From for Action { + fn from(v: BootstrapVolume) -> Self { + Action::DarwinBootstrapVolume(v) + } +} + +#[derive(Debug, thiserror::Error, Serialize)] +pub enum BootstrapVolumeError { + #[error("Failed to execute command")] + Command( + #[source] + #[serde(serialize_with = "crate::serialize_error_to_display")] + std::io::Error, + ), +} diff --git a/src/actions/base/darwin/create_synthetic_objects.rs b/src/actions/base/darwin/create_synthetic_objects.rs new file mode 100644 index 0000000..d3c04a0 --- /dev/null +++ b/src/actions/base/darwin/create_synthetic_objects.rs @@ -0,0 +1,116 @@ +use serde::Serialize; +use tokio::process::Command; + +use crate::execute_command; + +use crate::actions::{Action, ActionDescription, ActionState, Actionable}; + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct CreateSyntheticObjects { + unit: String, + action_state: ActionState, +} + +impl CreateSyntheticObjects { + #[tracing::instrument(skip_all)] + pub async fn plan(unit: String) -> Result { + Ok(Self { + unit, + action_state: ActionState::Uncompleted, + }) + } +} + +#[async_trait::async_trait] +impl Actionable for CreateSyntheticObjects { + type Error = CreateSyntheticObjectsError; + + fn describe_execute(&self) -> Vec { + if self.action_state == ActionState::Completed { + vec![] + } else { + vec![ActionDescription::new( + "Start the systemd Nix service and socket".to_string(), + vec![ + "The `nix` command line tool communicates with a running Nix daemon managed by your init system".to_string() + ] + )] + } + } + + #[tracing::instrument(skip_all, fields( + unit = %self.unit, + ))] + async fn execute(&mut self) -> Result<(), Self::Error> { + let Self { unit, action_state } = self; + if *action_state == ActionState::Completed { + tracing::trace!("Already completed: Starting systemd unit"); + return Ok(()); + } + tracing::debug!("Starting systemd unit"); + + // TODO(@Hoverbear): Handle proxy vars + execute_command( + Command::new("systemctl") + .arg("enable") + .arg("--now") + .arg(format!("{unit}")), + ) + .await + .map_err(CreateSyntheticObjectsError::Command)?; + + tracing::trace!("Started systemd unit"); + *action_state = ActionState::Completed; + Ok(()) + } + + fn describe_revert(&self) -> Vec { + if self.action_state == ActionState::Uncompleted { + vec![] + } else { + vec![ActionDescription::new( + "Stop the systemd Nix service and socket".to_string(), + vec![ + "The `nix` command line tool communicates with a running Nix daemon managed by your init system".to_string() + ] + )] + } + } + + #[tracing::instrument(skip_all, fields( + unit = %self.unit, + ))] + async fn revert(&mut self) -> Result<(), Self::Error> { + let Self { unit, action_state } = self; + if *action_state == ActionState::Uncompleted { + tracing::trace!("Already reverted: Stopping systemd unit"); + return Ok(()); + } + tracing::debug!("Stopping systemd unit"); + + // TODO(@Hoverbear): Handle proxy vars + execute_command(Command::new("systemctl").arg("stop").arg(format!("{unit}"))) + .await + .map_err(CreateSyntheticObjectsError::Command)?; + + tracing::trace!("Stopped systemd unit"); + *action_state = ActionState::Completed; + Ok(()) + } +} + +impl From for Action { + fn from(v: CreateSyntheticObjects) -> Self { + Action::DarwinCreateSyntheticObjects(v) + } +} + +#[derive(Debug, thiserror::Error, Serialize)] +pub enum CreateSyntheticObjectsError { + #[error("Failed to execute command")] + Command( + #[source] + #[serde(serialize_with = "crate::serialize_error_to_display")] + std::io::Error, + ), +} diff --git a/src/actions/base/darwin/create_volume.rs b/src/actions/base/darwin/create_volume.rs new file mode 100644 index 0000000..eddb6e6 --- /dev/null +++ b/src/actions/base/darwin/create_volume.rs @@ -0,0 +1,116 @@ +use serde::Serialize; +use tokio::process::Command; + +use crate::execute_command; + +use crate::actions::{Action, ActionDescription, ActionState, Actionable}; + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct CreateVolume { + unit: String, + action_state: ActionState, +} + +impl CreateVolume { + #[tracing::instrument(skip_all)] + pub async fn plan(unit: String) -> Result { + Ok(Self { + unit, + action_state: ActionState::Uncompleted, + }) + } +} + +#[async_trait::async_trait] +impl Actionable for CreateVolume { + type Error = CreateVolumeError; + + fn describe_execute(&self) -> Vec { + if self.action_state == ActionState::Completed { + vec![] + } else { + vec![ActionDescription::new( + "Start the systemd Nix service and socket".to_string(), + vec![ + "The `nix` command line tool communicates with a running Nix daemon managed by your init system".to_string() + ] + )] + } + } + + #[tracing::instrument(skip_all, fields( + unit = %self.unit, + ))] + async fn execute(&mut self) -> Result<(), Self::Error> { + let Self { unit, action_state } = self; + if *action_state == ActionState::Completed { + tracing::trace!("Already completed: Starting systemd unit"); + return Ok(()); + } + tracing::debug!("Starting systemd unit"); + + // TODO(@Hoverbear): Handle proxy vars + execute_command( + Command::new("systemctl") + .arg("enable") + .arg("--now") + .arg(format!("{unit}")), + ) + .await + .map_err(CreateVolumeError::Command)?; + + tracing::trace!("Started systemd unit"); + *action_state = ActionState::Completed; + Ok(()) + } + + fn describe_revert(&self) -> Vec { + if self.action_state == ActionState::Uncompleted { + vec![] + } else { + vec![ActionDescription::new( + "Stop the systemd Nix service and socket".to_string(), + vec![ + "The `nix` command line tool communicates with a running Nix daemon managed by your init system".to_string() + ] + )] + } + } + + #[tracing::instrument(skip_all, fields( + unit = %self.unit, + ))] + async fn revert(&mut self) -> Result<(), Self::Error> { + let Self { unit, action_state } = self; + if *action_state == ActionState::Uncompleted { + tracing::trace!("Already reverted: Stopping systemd unit"); + return Ok(()); + } + tracing::debug!("Stopping systemd unit"); + + // TODO(@Hoverbear): Handle proxy vars + execute_command(Command::new("systemctl").arg("stop").arg(format!("{unit}"))) + .await + .map_err(CreateVolumeError::Command)?; + + tracing::trace!("Stopped systemd unit"); + *action_state = ActionState::Completed; + Ok(()) + } +} + +impl From for Action { + fn from(v: CreateVolume) -> Self { + Action::DarwinCreateVolume(v) + } +} + +#[derive(Debug, thiserror::Error, Serialize)] +pub enum CreateVolumeError { + #[error("Failed to execute command")] + Command( + #[source] + #[serde(serialize_with = "crate::serialize_error_to_display")] + std::io::Error, + ), +} diff --git a/src/actions/base/darwin/enable_ownership.rs b/src/actions/base/darwin/enable_ownership.rs new file mode 100644 index 0000000..fbc9ec5 --- /dev/null +++ b/src/actions/base/darwin/enable_ownership.rs @@ -0,0 +1,116 @@ +use serde::Serialize; +use tokio::process::Command; + +use crate::execute_command; + +use crate::actions::{Action, ActionDescription, ActionState, Actionable}; + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct EnableOwnership { + unit: String, + action_state: ActionState, +} + +impl EnableOwnership { + #[tracing::instrument(skip_all)] + pub async fn plan(unit: String) -> Result { + Ok(Self { + unit, + action_state: ActionState::Uncompleted, + }) + } +} + +#[async_trait::async_trait] +impl Actionable for EnableOwnership { + type Error = EnableOwnershipError; + + fn describe_execute(&self) -> Vec { + if self.action_state == ActionState::Completed { + vec![] + } else { + vec![ActionDescription::new( + "Start the systemd Nix service and socket".to_string(), + vec![ + "The `nix` command line tool communicates with a running Nix daemon managed by your init system".to_string() + ] + )] + } + } + + #[tracing::instrument(skip_all, fields( + unit = %self.unit, + ))] + async fn execute(&mut self) -> Result<(), Self::Error> { + let Self { unit, action_state } = self; + if *action_state == ActionState::Completed { + tracing::trace!("Already completed: Starting systemd unit"); + return Ok(()); + } + tracing::debug!("Starting systemd unit"); + + // TODO(@Hoverbear): Handle proxy vars + execute_command( + Command::new("systemctl") + .arg("enable") + .arg("--now") + .arg(format!("{unit}")), + ) + .await + .map_err(EnableOwnershipError::Command)?; + + tracing::trace!("Started systemd unit"); + *action_state = ActionState::Completed; + Ok(()) + } + + fn describe_revert(&self) -> Vec { + if self.action_state == ActionState::Uncompleted { + vec![] + } else { + vec![ActionDescription::new( + "Stop the systemd Nix service and socket".to_string(), + vec![ + "The `nix` command line tool communicates with a running Nix daemon managed by your init system".to_string() + ] + )] + } + } + + #[tracing::instrument(skip_all, fields( + unit = %self.unit, + ))] + async fn revert(&mut self) -> Result<(), Self::Error> { + let Self { unit, action_state } = self; + if *action_state == ActionState::Uncompleted { + tracing::trace!("Already reverted: Stopping systemd unit"); + return Ok(()); + } + tracing::debug!("Stopping systemd unit"); + + // TODO(@Hoverbear): Handle proxy vars + execute_command(Command::new("systemctl").arg("stop").arg(format!("{unit}"))) + .await + .map_err(EnableOwnershipError::Command)?; + + tracing::trace!("Stopped systemd unit"); + *action_state = ActionState::Completed; + Ok(()) + } +} + +impl From for Action { + fn from(v: EnableOwnership) -> Self { + Action::DarwinEnableOwnership(v) + } +} + +#[derive(Debug, thiserror::Error, Serialize)] +pub enum EnableOwnershipError { + #[error("Failed to execute command")] + Command( + #[source] + #[serde(serialize_with = "crate::serialize_error_to_display")] + std::io::Error, + ), +} diff --git a/src/actions/base/darwin/encrypt_volume.rs b/src/actions/base/darwin/encrypt_volume.rs new file mode 100644 index 0000000..2814d91 --- /dev/null +++ b/src/actions/base/darwin/encrypt_volume.rs @@ -0,0 +1,116 @@ +use serde::Serialize; +use tokio::process::Command; + +use crate::execute_command; + +use crate::actions::{Action, ActionDescription, ActionState, Actionable}; + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct EncryptVolume { + unit: String, + action_state: ActionState, +} + +impl EncryptVolume { + #[tracing::instrument(skip_all)] + pub async fn plan(unit: String) -> Result { + Ok(Self { + unit, + action_state: ActionState::Uncompleted, + }) + } +} + +#[async_trait::async_trait] +impl Actionable for EncryptVolume { + type Error = EncryptVolumeError; + + fn describe_execute(&self) -> Vec { + if self.action_state == ActionState::Completed { + vec![] + } else { + vec![ActionDescription::new( + "Start the systemd Nix service and socket".to_string(), + vec![ + "The `nix` command line tool communicates with a running Nix daemon managed by your init system".to_string() + ] + )] + } + } + + #[tracing::instrument(skip_all, fields( + unit = %self.unit, + ))] + async fn execute(&mut self) -> Result<(), Self::Error> { + let Self { unit, action_state } = self; + if *action_state == ActionState::Completed { + tracing::trace!("Already completed: Starting systemd unit"); + return Ok(()); + } + tracing::debug!("Starting systemd unit"); + + // TODO(@Hoverbear): Handle proxy vars + execute_command( + Command::new("systemctl") + .arg("enable") + .arg("--now") + .arg(format!("{unit}")), + ) + .await + .map_err(EncryptVolumeError::Command)?; + + tracing::trace!("Started systemd unit"); + *action_state = ActionState::Completed; + Ok(()) + } + + fn describe_revert(&self) -> Vec { + if self.action_state == ActionState::Uncompleted { + vec![] + } else { + vec![ActionDescription::new( + "Stop the systemd Nix service and socket".to_string(), + vec![ + "The `nix` command line tool communicates with a running Nix daemon managed by your init system".to_string() + ] + )] + } + } + + #[tracing::instrument(skip_all, fields( + unit = %self.unit, + ))] + async fn revert(&mut self) -> Result<(), Self::Error> { + let Self { unit, action_state } = self; + if *action_state == ActionState::Uncompleted { + tracing::trace!("Already reverted: Stopping systemd unit"); + return Ok(()); + } + tracing::debug!("Stopping systemd unit"); + + // TODO(@Hoverbear): Handle proxy vars + execute_command(Command::new("systemctl").arg("stop").arg(format!("{unit}"))) + .await + .map_err(EncryptVolumeError::Command)?; + + tracing::trace!("Stopped systemd unit"); + *action_state = ActionState::Completed; + Ok(()) + } +} + +impl From for Action { + fn from(v: EncryptVolume) -> Self { + Action::DarwinEncryptVolume(v) + } +} + +#[derive(Debug, thiserror::Error, Serialize)] +pub enum EncryptVolumeError { + #[error("Failed to execute command")] + Command( + #[source] + #[serde(serialize_with = "crate::serialize_error_to_display")] + std::io::Error, + ), +} diff --git a/src/actions/base/darwin/mod.rs b/src/actions/base/darwin/mod.rs new file mode 100644 index 0000000..17a5dfc --- /dev/null +++ b/src/actions/base/darwin/mod.rs @@ -0,0 +1,13 @@ +mod bootstrap_volume; +mod create_synthetic_objects; +mod create_volume; +mod enable_ownership; +mod encrypt_volume; +mod unmount_volume; + +pub use bootstrap_volume::{BootstrapVolume, BootstrapVolumeError}; +pub use create_synthetic_objects::{CreateSyntheticObjects, CreateSyntheticObjectsError}; +pub use create_volume::{CreateVolume, CreateVolumeError}; +pub use enable_ownership::{EnableOwnership, EnableOwnershipError}; +pub use encrypt_volume::{EncryptVolume, EncryptVolumeError}; +pub use unmount_volume::{UnmountVolume, UnmountVolumeError}; diff --git a/src/actions/base/darwin/unmount_volume.rs b/src/actions/base/darwin/unmount_volume.rs new file mode 100644 index 0000000..41cd5fe --- /dev/null +++ b/src/actions/base/darwin/unmount_volume.rs @@ -0,0 +1,116 @@ +use serde::Serialize; +use tokio::process::Command; + +use crate::execute_command; + +use crate::actions::{Action, ActionDescription, ActionState, Actionable}; + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct UnmountVolume { + unit: String, + action_state: ActionState, +} + +impl UnmountVolume { + #[tracing::instrument(skip_all)] + pub async fn plan(unit: String) -> Result { + Ok(Self { + unit, + action_state: ActionState::Uncompleted, + }) + } +} + +#[async_trait::async_trait] +impl Actionable for UnmountVolume { + type Error = UnmountVolumeError; + + fn describe_execute(&self) -> Vec { + if self.action_state == ActionState::Completed { + vec![] + } else { + vec![ActionDescription::new( + "Start the systemd Nix service and socket".to_string(), + vec![ + "The `nix` command line tool communicates with a running Nix daemon managed by your init system".to_string() + ] + )] + } + } + + #[tracing::instrument(skip_all, fields( + unit = %self.unit, + ))] + async fn execute(&mut self) -> Result<(), Self::Error> { + let Self { unit, action_state } = self; + if *action_state == ActionState::Completed { + tracing::trace!("Already completed: Starting systemd unit"); + return Ok(()); + } + tracing::debug!("Starting systemd unit"); + + // TODO(@Hoverbear): Handle proxy vars + execute_command( + Command::new("systemctl") + .arg("enable") + .arg("--now") + .arg(format!("{unit}")), + ) + .await + .map_err(UnmountVolumeError::Command)?; + + tracing::trace!("Started systemd unit"); + *action_state = ActionState::Completed; + Ok(()) + } + + fn describe_revert(&self) -> Vec { + if self.action_state == ActionState::Uncompleted { + vec![] + } else { + vec![ActionDescription::new( + "Stop the systemd Nix service and socket".to_string(), + vec![ + "The `nix` command line tool communicates with a running Nix daemon managed by your init system".to_string() + ] + )] + } + } + + #[tracing::instrument(skip_all, fields( + unit = %self.unit, + ))] + async fn revert(&mut self) -> Result<(), Self::Error> { + let Self { unit, action_state } = self; + if *action_state == ActionState::Uncompleted { + tracing::trace!("Already reverted: Stopping systemd unit"); + return Ok(()); + } + tracing::debug!("Stopping systemd unit"); + + // TODO(@Hoverbear): Handle proxy vars + execute_command(Command::new("systemctl").arg("stop").arg(format!("{unit}"))) + .await + .map_err(UnmountVolumeError::Command)?; + + tracing::trace!("Stopped systemd unit"); + *action_state = ActionState::Completed; + Ok(()) + } +} + +impl From for Action { + fn from(v: UnmountVolume) -> Self { + Action::DarwinUnmountVolume(v) + } +} + +#[derive(Debug, thiserror::Error, Serialize)] +pub enum UnmountVolumeError { + #[error("Failed to execute command")] + Command( + #[source] + #[serde(serialize_with = "crate::serialize_error_to_display")] + std::io::Error, + ), +} diff --git a/src/actions/base/mod.rs b/src/actions/base/mod.rs index b9ef9db..b5e6806 100644 --- a/src/actions/base/mod.rs +++ b/src/actions/base/mod.rs @@ -6,6 +6,7 @@ mod create_file; mod create_group; mod create_or_append_file; mod create_user; +pub mod darwin; mod fetch_nix; mod move_unpacked_nix; mod setup_default_profile; diff --git a/src/actions/meta/darwin/create_apfs_volume.rs b/src/actions/meta/darwin/create_apfs_volume.rs index 12118b1..455d776 100644 --- a/src/actions/meta/darwin/create_apfs_volume.rs +++ b/src/actions/meta/darwin/create_apfs_volume.rs @@ -10,7 +10,7 @@ use crate::actions::base::{ CreateDirectory, CreateDirectoryError, CreateFile, CreateFileError, CreateOrAppendFile, CreateOrAppendFileError, }; -use crate::actions::{Action, ActionDescription, ActionState, Actionable}; +use crate::actions::{base::darwin, Action, ActionDescription, ActionState, Actionable}; const NIX_VOLUME_MOUNTD_DEST: &str = "/Library/LaunchDaemons/org.nixos.darwin-store.plist"; @@ -71,7 +71,7 @@ impl CreateApfsVolume { None }; - let bootstrap_volume = BootstrapVolume::plan(NIX_VOLUME_MOUNTD_DEST).await?; + let bootstrap_volume = BootstrapVolume::plan(NIX_VOLUME_MOUNTD_DEST, disk, name).await?; let enable_ownership = EnableOwnership::plan("/nix").await?; Ok(Self { @@ -116,7 +116,7 @@ impl Actionable for CreateApfsVolume { vec![] } else { vec![ActionDescription::new( - format!("Create an APFS volume", destination.display()), + format!("Create an APFS volume `{name}` on `{}`", disk.display()), vec![format!( "Create a writable, persistent systemd system extension.", )], @@ -142,18 +142,21 @@ impl Actionable for CreateApfsVolume { action_state, } = self; if *action_state == ActionState::Completed { - tracing::trace!("Already completed: Creating sysext"); + tracing::trace!("Already completed: Creating APFS volume"); return Ok(()); } - tracing::debug!("Creating sysext"); + tracing::debug!("Creating APFS volume"); - for create_directory in create_directories { - create_directory.execute().await?; - } - create_extension_release.execute().await?; - create_bind_mount_unit.execute().await?; + create_or_append_synthetic_conf.execute().await?; + create_synthetic_objects.execute().await?; + unmount_volume.execute().await?; + create_volume.execute().await?; + create_or_append_fstab.execute().await?; + encrypt_volume.execute().await?; + bootstrap_volume.execute().await?; + enable_ownership.execute().await?; - tracing::trace!("Created sysext"); + tracing::trace!("Created APFS volume"); *action_state = ActionState::Completed; Ok(()) } @@ -178,19 +181,21 @@ impl Actionable for CreateApfsVolume { vec![] } else { vec![ActionDescription::new( - format!("Remove the sysext located at `{}`", destination.display()), - vec![], + format!("Remove the APFS volume `{name}` on `{}`", disk.display()), + vec![format!( + "Create a writable, persistent systemd system extension.", + )], )] } } - #[tracing::instrument(skip_all, fields(destination,))] + #[tracing::instrument(skip_all, fields(disk, name))] async fn revert(&mut self) -> Result<(), Self::Error> { let Self { - disk, - name, - case_sensitive, - encrypt, + disk: _, + name: _, + case_sensitive: _, + encrypt: _, create_or_append_synthetic_conf, create_synthetic_objects, unmount_volume, @@ -202,20 +207,26 @@ impl Actionable for CreateApfsVolume { action_state, } = self; if *action_state == ActionState::Uncompleted { - tracing::trace!("Already reverted: Removing sysext"); + tracing::trace!("Already reverted: Removing APFS volume"); return Ok(()); } - tracing::debug!("Removing sysext"); + tracing::debug!("Removing APFS volume"); - create_bind_mount_unit.revert().await?; - - create_extension_release.revert().await?; - - for create_directory in create_directories.iter_mut().rev() { - create_directory.revert().await?; + enable_ownership.revert().await?; + bootstrap_volume.revert().await?; + if let Some(encrypt_volume) = encrypt_volume { + encrypt_volume.revert().await?; } + create_or_append_fstab.revert().await?; - tracing::trace!("Removed sysext"); + unmount_volume.revert().await?; + create_volume.revert().await?; + + // Purposefully not reversed + create_or_append_synthetic_conf.revert().await?; + create_synthetic_objects.revert().await?; + + tracing::trace!("Removed APFS volume"); *action_state = ActionState::Uncompleted; Ok(()) } @@ -229,6 +240,18 @@ impl From for Action { #[derive(Debug, thiserror::Error, Serialize)] pub enum CreateApfsVolumeError { + #[error(transparent)] + DarwinBootstrapVolume(#[from] BootstrapVolumeError), + #[error(transparent)] + DarwinCreateSyntheticObjects(#[from] CreateSyntheticObjectsError), + #[error(transparent)] + DarwinCreateVolume(#[from] CreateVolumeError), + #[error(transparent)] + DarwinEnableOwnership(#[from] EnableOwnershipError), + #[error(transparent)] + DarwinEncryptVolume(#[from] EncryptVolumeError), + #[error(transparent)] + DarwinUnmountVolume(#[from] UnmountVolumeError), #[error(transparent)] CreateOrAppendFile(#[from] CreateOrAppendFileError), } diff --git a/src/actions/mod.rs b/src/actions/mod.rs index 935f2ac..aa79adb 100644 --- a/src/actions/mod.rs +++ b/src/actions/mod.rs @@ -57,6 +57,12 @@ impl ActionDescription { #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub enum Action { + DarwinBootstrapVolume(base::darwin::BootstrapVolume), + DarwinCreateSyntheticObjects(base::darwin::CreateSyntheticObjects), + DarwinCreateVolume(base::darwin::CreateVolume), + DarwinEnableOwnership(base::darwin::EnableOwnership), + DarwinEncryptVolume(base::darwin::EncryptVolume), + DarwinUnmountVolume(base::darwin::UnmountVolume), ConfigureNix(ConfigureNix), ConfigureNixDaemonService(ConfigureNixDaemonService), ConfigureShellProfile(ConfigureShellProfile), @@ -82,6 +88,18 @@ pub enum Action { #[derive(Debug, thiserror::Error, serde::Serialize)] pub enum ActionError { + #[error(transparent)] + DarwinBootstrapVolume(#[from] base::darwin::BootstrapVolumeError), + #[error(transparent)] + DarwinCreateSyntheticObjects(#[from] base::darwin::CreateSyntheticObjectsError), + #[error(transparent)] + DarwinCreateVolume(#[from] base::darwin::CreateVolumeError), + #[error(transparent)] + DarwinEnableOwnership(#[from] base::darwin::EnableOwnershipError), + #[error(transparent)] + DarwinEncryptVolume(#[from] base::darwin::EncryptVolumeError), + #[error(transparent)] + DarwinUnmountVolume(#[from] base::darwin::UnmountVolumeError), #[error("Attempted to revert an unexecuted action")] NotExecuted(Action), #[error("Attempted to execute an already executed action")] @@ -137,6 +155,12 @@ impl Actionable for Action { type Error = ActionError; fn describe_execute(&self) -> Vec { match self { + Action::DarwinBootstrapVolume(i) => i.describe_execute(), + Action::DarwinCreateSyntheticObjects(i) => i.describe_execute(), + Action::DarwinCreateVolume(i) => i.describe_execute(), + Action::DarwinEnableOwnership(i) => i.describe_execute(), + Action::DarwinEncryptVolume(i) => i.describe_execute(), + Action::DarwinUnmountVolume(i) => i.describe_execute(), Action::ConfigureNix(i) => i.describe_execute(), Action::ConfigureNixDaemonService(i) => i.describe_execute(), Action::ConfigureShellProfile(i) => i.describe_execute(), @@ -163,6 +187,12 @@ impl Actionable for Action { async fn execute(&mut self) -> Result<(), Self::Error> { match self { + Action::DarwinBootstrapVolume(i) => i.execute().await?, + Action::DarwinCreateSyntheticObjects(i) => i.execute().await?, + Action::DarwinCreateVolume(i) => i.execute().await?, + Action::DarwinEnableOwnership(i) => i.execute().await?, + Action::DarwinEncryptVolume(i) => i.execute().await?, + Action::DarwinUnmountVolume(i) => i.execute().await?, Action::ConfigureNix(i) => i.execute().await?, Action::ConfigureNixDaemonService(i) => i.execute().await?, Action::ConfigureShellProfile(i) => i.execute().await?, @@ -190,6 +220,12 @@ impl Actionable for Action { fn describe_revert(&self) -> Vec { match self { + Action::DarwinBootstrapVolume(i) => i.describe_revert(), + Action::DarwinCreateSyntheticObjects(i) => i.describe_revert(), + Action::DarwinCreateVolume(i) => i.describe_revert(), + Action::DarwinEnableOwnership(i) => i.describe_revert(), + Action::DarwinEncryptVolume(i) => i.describe_revert(), + Action::DarwinUnmountVolume(i) => i.describe_revert(), Action::ConfigureNix(i) => i.describe_revert(), Action::ConfigureNixDaemonService(i) => i.describe_revert(), Action::ConfigureShellProfile(i) => i.describe_revert(), @@ -216,6 +252,12 @@ impl Actionable for Action { async fn revert(&mut self) -> Result<(), Self::Error> { match self { + Action::DarwinBootstrapVolume(i) => i.revert().await?, + Action::DarwinCreateSyntheticObjects(i) => i.revert().await?, + Action::DarwinCreateVolume(i) => i.revert().await?, + Action::DarwinEnableOwnership(i) => i.revert().await?, + Action::DarwinEncryptVolume(i) => i.revert().await?, + Action::DarwinUnmountVolume(i) => i.revert().await?, Action::ConfigureNix(i) => i.revert().await?, Action::ConfigureNixDaemonService(i) => i.revert().await?, Action::ConfigureShellProfile(i) => i.revert().await?, diff --git a/src/lib.rs b/src/lib.rs index f5ff8a1..13a6ce9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,11 @@ mod plan; mod planner; mod settings; -use std::{ffi::OsStr, fmt::Display, process::ExitStatus}; +use std::{ + ffi::OsStr, + fmt::Display, + process::{ExitStatus, Output}, +}; pub use error::HarmonicError; pub use plan::InstallPlan; @@ -15,12 +19,12 @@ pub use settings::InstallSettings; use tokio::process::Command; #[tracing::instrument(skip_all, fields(command = %format!("{:?}", command.as_std())))] -async fn execute_command(command: &mut Command) -> Result { +async fn execute_command(command: &mut Command) -> Result { tracing::trace!("Executing"); let command_str = format!("{:?}", command.as_std()); - let status = command.status().await?; - match status.success() { - true => Ok(status), + let output = command.output().await?; + match output.status.success() { + true => Ok(output), false => Err(std::io::Error::new( std::io::ErrorKind::Other, format!("Command `{command_str}` failed status"), diff --git a/src/planner/darwin/multi_user.rs b/src/planner/darwin/multi_user.rs index 7b64632..22f1902 100644 --- a/src/planner/darwin/multi_user.rs +++ b/src/planner/darwin/multi_user.rs @@ -1,9 +1,12 @@ +use tokio::process::Command; + use crate::{ actions::{ meta::{darwin::CreateApfsVolume, ConfigureNix, ProvisionNix, StartNixDaemon}, Action, ActionError, }, - planner::Plannable, + execute_command, + planner::{Plannable, PlannerError}, InstallPlan, Planner, }; @@ -18,6 +21,28 @@ impl Plannable for DarwinMultiUser { async fn plan( settings: crate::InstallSettings, ) -> Result { + let root_disk = { + let root_disk_buf = + execute_command(Command::new("/usr/sbin/diskutil").args(["info", "-plist", "/"])) + .await + .unwrap() + .stdout; + let package = + sxd_document::parser::parse(&String::from_utf8(root_disk_buf).unwrap()).unwrap(); + + match sxd_xpath::evaluate_xpath( + &package.as_document(), + "/plist/dict/key[text()='ParentWholeDisk']/following-sibling::string[1]/text()", + ) + .unwrap() + { + sxd_xpath::Value::String(s) => s, + _ => panic!("At the disk i/o!!!"), + } + }; + + let volume_label = "Nix Store".into(); + Ok(InstallPlan { planner: Self.into(), settings: settings.clone(), @@ -26,7 +51,7 @@ impl Plannable for DarwinMultiUser { // // setup_Synthetic -> create_synthetic_objects // Unmount -> create_volume -> Setup_fstab -> maybe encrypt_volume -> launchctl bootstrap -> launchctl kickstart -> await_volume -> maybe enableOwnership - CreateApfsVolume::plan(settings.clone()) + CreateApfsVolume::plan(root_disk, volume_label, false, None) .await .map(Action::from) .map_err(ActionError::from)?,