Explore planner pattern using steam deck example

This commit is contained in:
Ana Hobden 2022-10-17 12:19:07 -07:00
parent 144af153f6
commit 64e7423a0a
12 changed files with 455 additions and 34 deletions

View file

@ -14,6 +14,7 @@ pub struct CreateDirectory {
group: String,
mode: u32,
action_state: ActionState,
force_prune_on_revert: bool,
}
impl CreateDirectory {
@ -23,23 +24,37 @@ impl CreateDirectory {
user: String,
group: String,
mode: u32,
force: bool,
force_prune_on_revert: bool,
) -> Result<Self, CreateDirectoryError> {
let path = path.as_ref();
if path.exists() && !force {
return Err(CreateDirectoryError::Exists(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
format!("Directory `{}` already exists", path.display()),
)));
}
let action_state = if path.exists() {
let metadata = tokio::fs::metadata(path)
.await
.map_err(|e| CreateDirectoryError::GettingMetadata(path.to_path_buf(), e))?;
if metadata.is_dir() {
// TODO: Validate owner/group...
ActionState::Completed
} else {
return Err(CreateDirectoryError::Exists(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
format!(
"Path `{}` already exists and is not directory",
path.display()
),
)));
}
} else {
ActionState::Uncompleted
};
Ok(Self {
path: path.to_path_buf(),
user,
group,
mode,
action_state: ActionState::Uncompleted,
force_prune_on_revert,
action_state,
})
}
}
@ -54,6 +69,7 @@ impl Actionable for CreateDirectory {
user,
group,
mode,
force_prune_on_revert: _,
action_state: _,
} = &self;
if self.action_state == ActionState::Completed {
@ -81,6 +97,7 @@ impl Actionable for CreateDirectory {
user,
group,
mode,
force_prune_on_revert: _,
action_state,
} = self;
if *action_state == ActionState::Completed {
@ -118,13 +135,22 @@ impl Actionable for CreateDirectory {
user: _,
group: _,
mode: _,
force_prune_on_revert,
action_state: _,
} = &self;
if self.action_state == ActionState::Uncompleted {
vec![]
} else {
vec![ActionDescription::new(
format!("Remove the directory `{}`", path.display()),
format!(
"Remove the directory `{}`{}",
path.display(),
if *force_prune_on_revert {
""
} else {
" if no other contents exists"
}
),
vec![],
)]
}
@ -142,6 +168,7 @@ impl Actionable for CreateDirectory {
user: _,
group: _,
mode: _,
force_prune_on_revert,
action_state,
} = self;
if *action_state == ActionState::Uncompleted {
@ -151,9 +178,18 @@ impl Actionable for CreateDirectory {
tracing::debug!("Removing directory");
tracing::trace!(path = %path.display(), "Removing directory");
remove_dir_all(path.clone())
.await
.map_err(|e| Self::Error::Removing(path.clone(), e))?;
let is_empty = path
.read_dir()
.map_err(|e| CreateDirectoryError::ReadDir(path.clone(), e))?
.next()
.is_some();
match (is_empty, force_prune_on_revert) {
(true, _) | (false, true) => remove_dir_all(path.clone())
.await
.map_err(|e| Self::Error::Removing(path.clone(), e))?,
(false, false) => {},
};
tracing::trace!("Removed directory");
*action_state = ActionState::Uncompleted;
@ -185,6 +221,20 @@ pub enum CreateDirectoryError {
#[serde(serialize_with = "crate::serialize_error_to_display")]
std::io::Error,
),
#[error("Getting metadata for {0}`")]
GettingMetadata(
std::path::PathBuf,
#[source]
#[serde(serialize_with = "crate::serialize_error_to_display")]
std::io::Error,
),
#[error("Reading directory `{0}``")]
ReadDir(
std::path::PathBuf,
#[source]
#[serde(serialize_with = "crate::serialize_error_to_display")]
std::io::Error,
),
#[error("Set mode `{0}` on `{1}`")]
SetPermissions(
u32,

View file

@ -10,6 +10,7 @@ mod fetch_nix;
mod move_unpacked_nix;
mod setup_default_profile;
mod start_systemd_unit;
mod systemd_sysext_merge;
pub use configure_nix_daemon_service::{ConfigureNixDaemonService, ConfigureNixDaemonServiceError};
pub use create_directory::{CreateDirectory, CreateDirectoryError};
@ -21,3 +22,4 @@ pub use fetch_nix::{FetchNix, FetchNixError};
pub use move_unpacked_nix::{MoveUnpackedNix, MoveUnpackedNixError};
pub use setup_default_profile::{SetupDefaultProfile, SetupDefaultProfileError};
pub use start_systemd_unit::{StartSystemdUnit, StartSystemdUnitError};
pub use systemd_sysext_merge::{SystemdSysextMerge, SystemdSysextMergeError};

View file

@ -0,0 +1,120 @@
use std::path::PathBuf;
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 SystemdSysextMerge {
device: PathBuf,
action_state: ActionState,
}
impl SystemdSysextMerge {
#[tracing::instrument(skip_all)]
pub async fn plan(device: PathBuf) -> Result<Self, SystemdSysextMergeError> {
Ok(Self {
device,
action_state: ActionState::Uncompleted,
})
}
}
#[async_trait::async_trait]
impl Actionable for SystemdSysextMerge {
type Error = SystemdSysextMergeError;
fn describe_execute(&self) -> Vec<ActionDescription> {
let Self {
action_state,
device,
} = self;
if *action_state == ActionState::Completed {
vec![]
} else {
vec![ActionDescription::new(
format!("Run `systemd-sysext merge `{}`", device.display()),
vec![],
)]
}
}
#[tracing::instrument(skip_all, fields(
device = %self.device.display(),
))]
async fn execute(&mut self) -> Result<(), Self::Error> {
let Self {
device,
action_state,
} = self;
if *action_state == ActionState::Completed {
tracing::trace!("Already completed: Merging systemd-sysext");
return Ok(());
}
tracing::debug!("Merging systemd-sysext");
execute_command(Command::new("systemd-sysext").arg("merge").arg(device))
.await
.map_err(SystemdSysextMergeError::Command)?;
tracing::trace!("Merged systemd-sysext");
*action_state = ActionState::Completed;
Ok(())
}
fn describe_revert(&self) -> Vec<ActionDescription> {
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(
device = %self.device.display(),
))]
async fn revert(&mut self) -> Result<(), Self::Error> {
let Self {
device,
action_state,
} = self;
if *action_state == ActionState::Uncompleted {
tracing::trace!("Already reverted: Stopping systemd unit");
return Ok(());
}
tracing::debug!("Unmrging systemd-sysext");
// TODO(@Hoverbear): Handle proxy vars
execute_command(Command::new("systemd-sysext").arg("unmerge").arg(device))
.await
.map_err(SystemdSysextMergeError::Command)?;
tracing::trace!("Unmerged systemd-sysext");
*action_state = ActionState::Completed;
Ok(())
}
}
impl From<SystemdSysextMerge> for Action {
fn from(v: SystemdSysextMerge) -> Self {
Action::SystemdSysextMerge(v)
}
}
#[derive(Debug, thiserror::Error, Serialize)]
pub enum SystemdSysextMergeError {
#[error("Failed to execute command")]
Command(
#[source]
#[serde(serialize_with = "crate::serialize_error_to_display")]
std::io::Error,
),
}

View file

@ -27,12 +27,12 @@ pub struct CreateNixTree {
impl CreateNixTree {
#[tracing::instrument(skip_all)]
pub async fn plan(force: bool) -> Result<Self, CreateNixTreeError> {
pub async fn plan() -> Result<Self, CreateNixTreeError> {
let mut create_directories = Vec::default();
for path in PATHS {
// We use `create_dir` over `create_dir_all` to ensure we always set permissions right
create_directories.push(
CreateDirectory::plan(path, "root".into(), "root".into(), 0o0755, force).await?,
CreateDirectory::plan(path, "root".into(), "root".into(), 0o0755, false).await?,
)
}

View file

@ -0,0 +1,216 @@
use serde::Serialize;
use std::path::{Path, PathBuf};
use crate::actions::base::{CreateDirectory, CreateDirectoryError, CreateFile, CreateFileError};
use crate::actions::{Action, ActionDescription, ActionState, Actionable};
const PATHS: &[&str] = &[
"usr",
"usr/lib",
"usr/lib/extension-release.d",
"usr/lib/systemd",
"usr/lib/systemd/system",
];
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct CreateSystemdSysext {
destination: PathBuf,
create_directories: Vec<CreateDirectory>,
create_extension_release: CreateFile,
create_bind_mount_unit: CreateFile,
action_state: ActionState,
}
impl CreateSystemdSysext {
#[tracing::instrument(skip_all)]
pub async fn plan(destination: impl AsRef<Path>) -> Result<Self, CreateSystemdSysextError> {
let destination = destination.as_ref();
let mut create_directories = vec![
CreateDirectory::plan(destination, "root".into(), "root".into(), 0o0755, true).await?,
];
for path in PATHS {
create_directories.push(
CreateDirectory::plan(
destination.join(path),
"root".into(),
"root".into(),
0o0755,
false,
)
.await?,
)
}
let version_id = tokio::fs::read_to_string("/etc/os-release")
.await
.map(|buf| {
buf.lines()
.find_map(|line| match line.starts_with("VERSION_ID") {
true => line.rsplit("=").next().map(|inner| inner.to_owned()),
false => None,
})
})
.map_err(CreateSystemdSysextError::ReadingOsRelease)?
.ok_or(CreateSystemdSysextError::NoVersionId)?;
let extension_release_buf =
format!("SYSEXT_LEVEL=1.0\nID=steamos\nVERSION_ID={version_id}");
let create_extension_release = CreateFile::plan(
destination.join("usr/lib/extension-release.d/extension-release.nix"),
"root".into(),
"root".into(),
0o0755,
extension_release_buf,
false,
)
.await?;
let create_bind_mount_buf = format!(
"
[Mount]\n\
What={}\n\
Where=/nix\n\
Type=none\n\
Options=bind\n\
",
destination.display(),
);
let create_bind_mount_unit = CreateFile::plan(
destination.join("usr/lib/systemd/system/nix.mount"),
"root".into(),
"root".into(),
0o0755,
create_bind_mount_buf,
false,
)
.await?;
Ok(Self {
destination: destination.to_path_buf(),
create_directories,
create_extension_release,
create_bind_mount_unit,
action_state: ActionState::Uncompleted,
})
}
}
#[async_trait::async_trait]
impl Actionable for CreateSystemdSysext {
type Error = CreateSystemdSysextError;
fn describe_execute(&self) -> Vec<ActionDescription> {
let Self {
action_state: _,
destination,
create_bind_mount_unit: _,
create_directories: _,
create_extension_release: _,
} = &self;
if self.action_state == ActionState::Completed {
vec![]
} else {
vec![ActionDescription::new(
format!("Create a systemd sysext at `{}`", destination.display()),
vec![format!(
"Create a writable, persistent systemd system extension.",
)],
)]
}
}
#[tracing::instrument(skip_all, fields(destination,))]
async fn execute(&mut self) -> Result<(), Self::Error> {
let Self {
destination: _,
action_state,
create_directories,
create_extension_release,
create_bind_mount_unit,
} = self;
if *action_state == ActionState::Completed {
tracing::trace!("Already completed: Creating sysext");
return Ok(());
}
tracing::debug!("Creating sysext");
for create_directory in create_directories {
create_directory.execute().await?;
}
create_extension_release.execute().await?;
create_bind_mount_unit.execute().await?;
tracing::trace!("Created sysext");
*action_state = ActionState::Completed;
Ok(())
}
fn describe_revert(&self) -> Vec<ActionDescription> {
let Self {
destination,
action_state: _,
create_directories: _,
create_extension_release: _,
create_bind_mount_unit: _,
} = &self;
if self.action_state == ActionState::Uncompleted {
vec![]
} else {
vec![ActionDescription::new(
format!("Remove the sysext located at `{}`", destination.display()),
vec![],
)]
}
}
#[tracing::instrument(skip_all, fields(destination,))]
async fn revert(&mut self) -> Result<(), Self::Error> {
let Self {
destination: _,
action_state,
create_directories,
create_extension_release,
create_bind_mount_unit,
} = self;
if *action_state == ActionState::Uncompleted {
tracing::trace!("Already reverted: Removing sysext");
return Ok(());
}
tracing::debug!("Removing sysext");
create_bind_mount_unit.revert().await?;
create_extension_release.revert().await?;
for create_directory in create_directories.iter_mut().rev() {
create_directory.revert().await?;
}
tracing::trace!("Removed sysext");
*action_state = ActionState::Uncompleted;
Ok(())
}
}
impl From<CreateSystemdSysext> for Action {
fn from(v: CreateSystemdSysext) -> Self {
Action::CreateSystemdSysext(v)
}
}
#[derive(Debug, thiserror::Error, Serialize)]
pub enum CreateSystemdSysextError {
#[error(transparent)]
CreateDirectory(#[from] CreateDirectoryError),
#[error(transparent)]
CreateFile(#[from] CreateFileError),
#[error("Reading /etc/os-release")]
ReadingOsRelease(
#[source]
#[from]
#[serde(serialize_with = "crate::serialize_error_to_display")]
std::io::Error,
),
#[error("No `VERSION_ID` line in /etc/os-release")]
NoVersionId,
}

View file

@ -3,6 +3,7 @@
mod configure_nix;
mod configure_shell_profile;
mod create_nix_tree;
mod create_systemd_sysext;
mod create_users_and_group;
mod place_channel_configuration;
mod place_nix_configuration;
@ -12,6 +13,7 @@ mod start_nix_daemon;
pub use configure_nix::{ConfigureNix, ConfigureNixError};
pub use configure_shell_profile::{ConfigureShellProfile, ConfigureShellProfileError};
pub use create_nix_tree::{CreateNixTree, CreateNixTreeError};
pub use create_systemd_sysext::{CreateSystemdSysext, CreateSystemdSysextError};
pub use create_users_and_group::{CreateUsersAndGroup, CreateUsersAndGroupError};
pub use place_channel_configuration::{PlaceChannelConfiguration, PlaceChannelConfigurationError};
pub use place_nix_configuration::{PlaceNixConfiguration, PlaceNixConfigurationError};

View file

@ -27,8 +27,7 @@ impl ProvisionNix {
#[tracing::instrument(skip_all)]
pub async fn plan(settings: InstallSettings) -> Result<Self, ProvisionNixError> {
let create_nix_dir =
CreateDirectory::plan("/nix", "root".into(), "root".into(), 0o0755, settings.force)
.await?;
CreateDirectory::plan("/nix", "root".into(), "root".into(), 0o0755, true).await?;
let fetch_nix = FetchNix::plan(
settings.nix_package_url.clone(),
@ -36,7 +35,7 @@ impl ProvisionNix {
)
.await?;
let create_users_and_group = CreateUsersAndGroup::plan(settings.clone()).await?;
let create_nix_tree = CreateNixTree::plan(settings.force).await?;
let create_nix_tree = CreateNixTree::plan().await?;
let move_unpacked_nix =
MoveUnpackedNix::plan(PathBuf::from("/nix/temp-install-dir")).await?;
Ok(Self {

View file

@ -6,19 +6,18 @@ use base::{
CreateDirectoryError, CreateFile, CreateFileError, CreateGroup, CreateGroupError,
CreateOrAppendFile, CreateOrAppendFileError, CreateUser, CreateUserError, FetchNix,
FetchNixError, MoveUnpackedNix, MoveUnpackedNixError, SetupDefaultProfile,
SetupDefaultProfileError,
SetupDefaultProfileError, StartSystemdUnit, StartSystemdUnitError, SystemdSysextMerge,
SystemdSysextMergeError,
};
use meta::{
ConfigureNix, ConfigureNixError, ConfigureShellProfile, ConfigureShellProfileError,
CreateNixTree, CreateNixTreeError, CreateUsersAndGroup, CreateUsersAndGroupError,
PlaceChannelConfiguration, PlaceChannelConfigurationError, PlaceNixConfiguration,
PlaceNixConfigurationError, ProvisionNix, ProvisionNixError, StartNixDaemon,
StartNixDaemonError,
CreateNixTree, CreateNixTreeError, CreateSystemdSysext, CreateSystemdSysextError,
CreateUsersAndGroup, CreateUsersAndGroupError, PlaceChannelConfiguration,
PlaceChannelConfigurationError, PlaceNixConfiguration, PlaceNixConfigurationError,
ProvisionNix, ProvisionNixError, StartNixDaemon, StartNixDaemonError,
};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use self::base::{StartSystemdUnit, StartSystemdUnitError};
#[async_trait::async_trait]
pub trait Actionable: DeserializeOwned + Serialize + Into<Action> {
type Error: std::error::Error + std::fmt::Debug + Serialize + Into<ActionError>;
@ -61,6 +60,7 @@ pub enum Action {
ConfigureNix(ConfigureNix),
ConfigureShellProfile(ConfigureShellProfile),
CreateDirectory(CreateDirectory),
CreateSystemdSysext(CreateSystemdSysext),
CreateFile(CreateFile),
CreateGroup(CreateGroup),
CreateOrAppendFile(CreateOrAppendFile),
@ -74,6 +74,7 @@ pub enum Action {
SetupDefaultProfile(SetupDefaultProfile),
StartNixDaemon(StartNixDaemon),
StartSystemdUnit(StartSystemdUnit),
SystemdSysextMerge(SystemdSysextMerge),
ProvisionNix(ProvisionNix),
}
@ -94,6 +95,8 @@ pub enum ActionError {
#[error(transparent)]
CreateDirectory(#[from] CreateDirectoryError),
#[error(transparent)]
CreateSystemdSysext(#[from] CreateSystemdSysextError),
#[error(transparent)]
CreateFile(#[from] CreateFileError),
#[error(transparent)]
CreateGroup(#[from] CreateGroupError),
@ -120,6 +123,8 @@ pub enum ActionError {
#[error(transparent)]
StartSystemdUnit(#[from] StartSystemdUnitError),
#[error(transparent)]
SystemdSysExtMerge(#[from] SystemdSysextMergeError),
#[error(transparent)]
ProvisionNix(#[from] ProvisionNixError),
}
@ -132,6 +137,7 @@ impl Actionable for Action {
Action::ConfigureNix(i) => i.describe_execute(),
Action::ConfigureShellProfile(i) => i.describe_execute(),
Action::CreateDirectory(i) => i.describe_execute(),
Action::CreateSystemdSysext(i) => i.describe_execute(),
Action::CreateFile(i) => i.describe_execute(),
Action::CreateGroup(i) => i.describe_execute(),
Action::CreateOrAppendFile(i) => i.describe_execute(),
@ -145,6 +151,7 @@ impl Actionable for Action {
Action::SetupDefaultProfile(i) => i.describe_execute(),
Action::StartNixDaemon(i) => i.describe_execute(),
Action::StartSystemdUnit(i) => i.describe_execute(),
Action::SystemdSysextMerge(i) => i.describe_execute(),
Action::ProvisionNix(i) => i.describe_execute(),
}
}
@ -155,6 +162,7 @@ impl Actionable for Action {
Action::ConfigureNix(i) => i.execute().await?,
Action::ConfigureShellProfile(i) => i.execute().await?,
Action::CreateDirectory(i) => i.execute().await?,
Action::CreateSystemdSysext(i) => i.execute().await?,
Action::CreateFile(i) => i.execute().await?,
Action::CreateGroup(i) => i.execute().await?,
Action::CreateOrAppendFile(i) => i.execute().await?,
@ -168,6 +176,7 @@ impl Actionable for Action {
Action::SetupDefaultProfile(i) => i.execute().await?,
Action::StartNixDaemon(i) => i.execute().await?,
Action::StartSystemdUnit(i) => i.execute().await?,
Action::SystemdSysextMerge(i) => i.execute().await?,
Action::ProvisionNix(i) => i.execute().await?,
};
Ok(())
@ -179,6 +188,7 @@ impl Actionable for Action {
Action::ConfigureNix(i) => i.describe_revert(),
Action::ConfigureShellProfile(i) => i.describe_revert(),
Action::CreateDirectory(i) => i.describe_revert(),
Action::CreateSystemdSysext(i) => i.describe_revert(),
Action::CreateFile(i) => i.describe_revert(),
Action::CreateGroup(i) => i.describe_revert(),
Action::CreateOrAppendFile(i) => i.describe_revert(),
@ -192,6 +202,7 @@ impl Actionable for Action {
Action::SetupDefaultProfile(i) => i.describe_revert(),
Action::StartNixDaemon(i) => i.describe_revert(),
Action::StartSystemdUnit(i) => i.describe_revert(),
Action::SystemdSysextMerge(i) => i.describe_revert(),
Action::ProvisionNix(i) => i.describe_revert(),
}
}
@ -202,6 +213,7 @@ impl Actionable for Action {
Action::ConfigureNix(i) => i.revert().await?,
Action::ConfigureShellProfile(i) => i.revert().await?,
Action::CreateDirectory(i) => i.revert().await?,
Action::CreateSystemdSysext(i) => i.revert().await?,
Action::CreateFile(i) => i.revert().await?,
Action::CreateGroup(i) => i.revert().await?,
Action::CreateOrAppendFile(i) => i.revert().await?,
@ -215,6 +227,7 @@ impl Actionable for Action {
Action::SetupDefaultProfile(i) => i.revert().await?,
Action::StartNixDaemon(i) => i.revert().await?,
Action::StartSystemdUnit(i) => i.revert().await?,
Action::SystemdSysextMerge(i) => i.revert().await?,
Action::ProvisionNix(i) => i.revert().await?,
}
Ok(())

View file

@ -1,10 +1,7 @@
use std::path::PathBuf;
use crate::{
actions::{
meta::{ConfigureNix, ProvisionNix, StartNixDaemon},
Action, ActionDescription, ActionError, Actionable,
},
actions::{Action, ActionDescription, ActionError, Actionable},
planner::PlannerError,
settings::InstallSettings,
HarmonicError, Planner,

View file

@ -2,9 +2,7 @@ mod darwin;
mod linux;
mod specific;
use std::{ffi::OsStr, str::FromStr};
use crate::{actions::ActionError, HarmonicError, InstallPlan, InstallSettings};
use crate::{actions::ActionError, InstallPlan, InstallSettings};
#[derive(Debug, Clone, clap::ValueEnum, serde::Serialize, serde::Deserialize)]
pub enum Planner {

View file

@ -1,4 +1,11 @@
use crate::{planner::Plannable, Planner};
use crate::{
actions::{
meta::{CreateSystemdSysext, ProvisionNix, StartNixDaemon},
Action, ActionError,
},
planner::Plannable,
InstallPlan, Planner,
};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
pub struct SteamDeck;
@ -11,7 +18,24 @@ impl Plannable for SteamDeck {
async fn plan(
settings: crate::InstallSettings,
) -> Result<crate::InstallPlan, crate::planner::PlannerError> {
todo!()
Ok(InstallPlan {
planner: Self.into(),
settings: settings.clone(),
actions: vec![
CreateSystemdSysext::plan("/var/lib/extensions")
.await
.map(Action::from)
.map_err(ActionError::from)?,
ProvisionNix::plan(settings.clone())
.await
.map(Action::from)
.map_err(ActionError::from)?,
StartNixDaemon::plan()
.await
.map(Action::from)
.map_err(ActionError::from)?,
],
})
}
}

View file

@ -1,4 +1,4 @@
use crate::{planner, Planner};
use crate::planner;
use target_lexicon::Triple;
use url::Url;