Add support for ostree-based Linux distributions (#586)

* Add support for ostree-based Linux distributions

Fixes #389
I've tested this planner on Fedora Silverblue and Endless OS

* Stop duplicating check functions

* Remove `init` cli flag
This commit is contained in:
Hofer-Julian 2023-07-24 20:10:20 +02:00 committed by GitHub
parent a049e52fd8
commit e84fd2bed9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 378 additions and 9 deletions

View file

@ -107,6 +107,8 @@ pub mod linux;
#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(target_os = "linux")]
pub mod ostree;
#[cfg(target_os = "linux")]
pub mod steam_deck;
use std::{collections::HashMap, path::PathBuf, string::FromUtf8Error};
@ -171,6 +173,9 @@ pub enum BuiltinPlanner {
/// A planner suitable for the Valve Steam Deck running SteamOS
#[cfg(target_os = "linux")]
SteamDeck(steam_deck::SteamDeck),
/// A planner suitable for immutable distributions using ostree
#[cfg(target_os = "linux")]
Ostree(ostree::Ostree),
}
impl BuiltinPlanner {
@ -179,15 +184,7 @@ impl BuiltinPlanner {
use target_lexicon::{Architecture, OperatingSystem};
match (Architecture::host(), OperatingSystem::host()) {
#[cfg(target_os = "linux")]
(Architecture::X86_64, OperatingSystem::Linux) => {
let os_release = os_release::OsRelease::new().ok();
match os_release {
Some(os_release) if os_release.id == "steamos" => {
Ok(Self::SteamDeck(steam_deck::SteamDeck::default().await?))
},
_ => Ok(Self::Linux(linux::Linux::default().await?)),
}
},
(Architecture::X86_64, OperatingSystem::Linux) => Self::detect_linux_distro().await,
#[cfg(target_os = "linux")]
(Architecture::X86_32(_), OperatingSystem::Linux) => {
Ok(Self::Linux(linux::Linux::default().await?))
@ -210,6 +207,25 @@ impl BuiltinPlanner {
}
}
async fn detect_linux_distro() -> Result<Self, PlannerError> {
let is_steam_deck =
os_release::OsRelease::new().is_ok_and(|os_release| os_release.id == "steamos");
if is_steam_deck {
return Ok(Self::SteamDeck(steam_deck::SteamDeck::default().await?));
}
let is_ostree = std::process::Command::new("ostree")
.arg("remote")
.arg("list")
.output()
.is_ok_and(|output| output.status.success());
if is_ostree {
return Ok(Self::Ostree(ostree::Ostree::default().await?));
}
Ok(Self::Linux(linux::Linux::default().await?))
}
pub async fn from_common_settings(settings: CommonSettings) -> Result<Self, PlannerError> {
let mut built = Self::default().await?;
match &mut built {
@ -217,6 +233,8 @@ impl BuiltinPlanner {
BuiltinPlanner::Linux(inner) => inner.settings = settings,
#[cfg(target_os = "linux")]
BuiltinPlanner::SteamDeck(inner) => inner.settings = settings,
#[cfg(target_os = "linux")]
BuiltinPlanner::Ostree(inner) => inner.settings = settings,
#[cfg(target_os = "macos")]
BuiltinPlanner::Macos(inner) => inner.settings = settings,
}
@ -231,6 +249,8 @@ impl BuiltinPlanner {
BuiltinPlanner::Linux(inner) => inner.configured_settings().await,
#[cfg(target_os = "linux")]
BuiltinPlanner::SteamDeck(inner) => inner.configured_settings().await,
#[cfg(target_os = "linux")]
BuiltinPlanner::Ostree(inner) => inner.configured_settings().await,
#[cfg(target_os = "macos")]
BuiltinPlanner::Macos(inner) => inner.configured_settings().await,
}
@ -242,6 +262,8 @@ impl BuiltinPlanner {
BuiltinPlanner::Linux(planner) => InstallPlan::plan(planner).await,
#[cfg(target_os = "linux")]
BuiltinPlanner::SteamDeck(planner) => InstallPlan::plan(planner).await,
#[cfg(target_os = "linux")]
BuiltinPlanner::Ostree(planner) => InstallPlan::plan(planner).await,
#[cfg(target_os = "macos")]
BuiltinPlanner::Macos(planner) => InstallPlan::plan(planner).await,
}
@ -252,6 +274,8 @@ impl BuiltinPlanner {
BuiltinPlanner::Linux(i) => i.boxed(),
#[cfg(target_os = "linux")]
BuiltinPlanner::SteamDeck(i) => i.boxed(),
#[cfg(target_os = "linux")]
BuiltinPlanner::Ostree(i) => i.boxed(),
#[cfg(target_os = "macos")]
BuiltinPlanner::Macos(i) => i.boxed(),
}
@ -263,6 +287,8 @@ impl BuiltinPlanner {
BuiltinPlanner::Linux(i) => i.typetag_name(),
#[cfg(target_os = "linux")]
BuiltinPlanner::SteamDeck(i) => i.typetag_name(),
#[cfg(target_os = "linux")]
BuiltinPlanner::Ostree(i) => i.typetag_name(),
#[cfg(target_os = "macos")]
BuiltinPlanner::Macos(i) => i.typetag_name(),
}
@ -274,6 +300,8 @@ impl BuiltinPlanner {
BuiltinPlanner::Linux(i) => i.settings(),
#[cfg(target_os = "linux")]
BuiltinPlanner::SteamDeck(i) => i.settings(),
#[cfg(target_os = "linux")]
BuiltinPlanner::Ostree(i) => i.settings(),
#[cfg(target_os = "macos")]
BuiltinPlanner::Macos(i) => i.settings(),
}
@ -288,6 +316,8 @@ impl BuiltinPlanner {
BuiltinPlanner::Linux(i) => i.diagnostic_data().await,
#[cfg(target_os = "linux")]
BuiltinPlanner::SteamDeck(i) => i.diagnostic_data().await,
#[cfg(target_os = "linux")]
BuiltinPlanner::Ostree(i) => i.diagnostic_data().await,
#[cfg(target_os = "macos")]
BuiltinPlanner::Macos(i) => i.diagnostic_data().await,
}

339
src/planner/ostree.rs Normal file
View file

@ -0,0 +1,339 @@
use crate::{
action::{
base::{CreateDirectory, CreateFile, RemoveDirectory},
common::{ConfigureInitService, ConfigureNix, CreateUsersAndGroups, ProvisionNix},
linux::{ProvisionSelinux, StartSystemdUnit, SystemctlDaemonReload},
StatefulAction,
},
error::HasExpectedErrors,
planner::{Planner, PlannerError},
settings::CommonSettings,
settings::{InitSystem, InstallSettingsError},
Action, BuiltinPlanner,
};
use std::{collections::HashMap, path::PathBuf};
use super::{
linux::{
check_nix_not_already_installed, check_not_nixos, check_not_wsl1, check_systemd_active,
detect_selinux,
},
ShellProfileLocations,
};
/// A planner for Linux installs
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "cli", derive(clap::Parser))]
pub struct Ostree {
/// Where `/nix` will be bind mounted to.
#[cfg_attr(feature = "cli", clap(long, default_value = "/var/home/nix"))]
persistence: PathBuf,
#[cfg_attr(feature = "cli", clap(flatten))]
pub settings: CommonSettings,
}
#[async_trait::async_trait]
#[typetag::serde(name = "ostree")]
impl Planner for Ostree {
async fn default() -> Result<Self, PlannerError> {
Ok(Self {
persistence: PathBuf::from("/var/home/nix"),
settings: CommonSettings::default().await?,
})
}
async fn plan(&self) -> Result<Vec<StatefulAction<Box<dyn Action>>>, PlannerError> {
let has_selinux = detect_selinux().await?;
let mut plan = vec![
// Primarily for uninstall
SystemctlDaemonReload::plan()
.await
.map_err(PlannerError::Action)?
.boxed(),
];
plan.push(
CreateDirectory::plan(&self.persistence, None, None, 0o0755, true)
.await
.map_err(PlannerError::Action)?
.boxed(),
);
let nix_directory_buf = "\
[Unit]\n\
Description=Enable mount points in / for ostree\n\
ConditionPathExists=!/nix\n\
DefaultDependencies=no\n\
Requires=local-fs-pre.target\n\
After=local-fs-pre.target\n\
[Service]\n\
Type=oneshot\n\
ExecStartPre=chattr -i /\n\
ExecStart=mkdir -p /nix\n\
ExecStopPost=chattr +i /\n\
"
.to_string();
let nix_directory_unit = CreateFile::plan(
"/etc/systemd/system/nix-directory.service",
None,
None,
0o0644,
nix_directory_buf,
false,
)
.await
.map_err(PlannerError::Action)?;
plan.push(nix_directory_unit.boxed());
let create_bind_mount_buf = format!(
"\
[Unit]\n\
Description=Mount `{persistence}` on `/nix`\n\
PropagatesStopTo=nix-daemon.service\n\
PropagatesStopTo=nix-directory.service\n\
After=nix-directory.service\n\
Requires=nix-directory.service\n\
ConditionPathIsDirectory=/nix\n\
DefaultDependencies=no\n\
\n\
[Mount]\n\
What={persistence}\n\
Where=/nix\n\
Type=none\n\
DirectoryMode=0755\n\
Options=bind\n\
\n\
[Install]\n\
RequiredBy=nix-daemon.service\n\
RequiredBy=nix-daemon.socket\n
",
persistence = self.persistence.display(),
);
let create_bind_mount_unit = CreateFile::plan(
"/etc/systemd/system/nix.mount",
None,
None,
0o0644,
create_bind_mount_buf,
false,
)
.await
.map_err(PlannerError::Action)?;
plan.push(create_bind_mount_unit.boxed());
let ensure_symlinked_units_resolve_buf = "\
[Unit]\n\
Description=Ensure Nix related units which are symlinked resolve\n\
After=nix.mount\n\
Requires=nix.mount\n\
DefaultDependencies=no\n\
\n\
[Service]\n\
Type=oneshot\n\
RemainAfterExit=yes\n\
ExecStart=/usr/bin/systemctl daemon-reload\n\
ExecStart=/usr/bin/systemctl restart --no-block nix-daemon.socket\n\
\n\
[Install]\n\
WantedBy=sysinit.target\n\
"
.to_string();
let ensure_symlinked_units_resolve_unit = CreateFile::plan(
"/etc/systemd/system/ensure-symlinked-units-resolve.service",
None,
None,
0o0644,
ensure_symlinked_units_resolve_buf,
false,
)
.await
.map_err(PlannerError::Action)?;
plan.push(ensure_symlinked_units_resolve_unit.boxed());
// We need to remove this path since it's part of the read-only install.
let mut shell_profile_locations = ShellProfileLocations::default();
if let Some(index) = shell_profile_locations
.fish
.vendor_confd_prefixes
.iter()
.position(|v| *v == PathBuf::from("/usr/share/fish/"))
{
shell_profile_locations
.fish
.vendor_confd_prefixes
.remove(index);
}
plan.push(
StartSystemdUnit::plan("nix.mount".to_string(), false)
.await
.map_err(PlannerError::Action)?
.boxed(),
);
plan.push(
ProvisionNix::plan(&self.settings.clone())
.await
.map_err(PlannerError::Action)?
.boxed(),
);
plan.push(
CreateUsersAndGroups::plan(self.settings.clone())
.await
.map_err(PlannerError::Action)?
.boxed(),
);
plan.push(
ConfigureNix::plan(shell_profile_locations, &self.settings)
.await
.map_err(PlannerError::Action)?
.boxed(),
);
if has_selinux {
plan.push(
ProvisionSelinux::plan("/etc/nix-installer/selinux/packages/nix.pp".into())
.await
.map_err(PlannerError::Action)?
.boxed(),
);
}
plan.push(
ConfigureInitService::plan(InitSystem::Systemd, true)
.await
.map_err(PlannerError::Action)?
.boxed(),
);
plan.push(
StartSystemdUnit::plan("ensure-symlinked-units-resolve.service".to_string(), true)
.await
.map_err(PlannerError::Action)?
.boxed(),
);
plan.push(
RemoveDirectory::plan(crate::settings::SCRATCH_DIR)
.await
.map_err(PlannerError::Action)?
.boxed(),
);
plan.push(
SystemctlDaemonReload::plan()
.await
.map_err(PlannerError::Action)?
.boxed(),
);
Ok(plan)
}
fn settings(&self) -> Result<HashMap<String, serde_json::Value>, InstallSettingsError> {
let Self {
persistence,
settings,
} = self;
let mut map = HashMap::default();
map.extend(settings.settings()?.into_iter());
map.insert(
"persistence".to_string(),
serde_json::to_value(persistence)?,
);
Ok(map)
}
async fn configured_settings(
&self,
) -> Result<HashMap<String, serde_json::Value>, PlannerError> {
let default = Self::default().await?.settings()?;
let configured = self.settings()?;
let mut settings: HashMap<String, serde_json::Value> = HashMap::new();
for (key, value) in configured.iter() {
if default.get(key) != Some(value) {
settings.insert(key.clone(), value.clone());
}
}
Ok(settings)
}
#[cfg(feature = "diagnostics")]
async fn diagnostic_data(&self) -> Result<crate::diagnostics::DiagnosticData, PlannerError> {
Ok(crate::diagnostics::DiagnosticData::new(
self.settings.diagnostic_endpoint.clone(),
self.typetag_name().into(),
self.configured_settings()
.await?
.into_keys()
.collect::<Vec<_>>(),
self.settings.ssl_cert_file.clone(),
)?)
}
async fn pre_uninstall_check(&self) -> Result<(), PlannerError> {
check_not_wsl1()?;
check_systemd_active()?;
Ok(())
}
async fn pre_install_check(&self) -> Result<(), PlannerError> {
check_not_nixos()?;
check_nix_not_already_installed().await?;
check_not_wsl1()?;
check_systemd_active()?;
Ok(())
}
}
impl From<Ostree> for BuiltinPlanner {
fn from(val: Ostree) -> Self {
BuiltinPlanner::Ostree(val)
}
}
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum OstreeError {
#[error(
"\
systemd was not active.\n\
\n\
If it will be started later consider, passing `--no-start-daemon`.\n\
\n\
To use a `root`-only Nix install, consider passing `--init none`."
)]
SystemdNotActive,
#[error(
"\
systemd was not active.\n\
\n\
On WSL2, systemd is not enabled by default. Consider enabling it by adding it to your `/etc/wsl.conf` with `echo -e '[boot]\\nsystemd=true'` then restarting WSL2 with `wsl.exe --shutdown` and re-entering the WSL shell. For more information, see https://devblogs.microsoft.com/commandline/systemd-support-is-now-available-in-wsl/.\n\
\n\
If it will be started later consider, passing `--no-start-daemon`.\n\
\n\
To use a `root`-only Nix install, consider passing `--init none`."
)]
Wsl2SystemdNotActive,
}
impl HasExpectedErrors for OstreeError {
fn expected<'a>(&'a self) -> Option<Box<dyn std::error::Error + 'a>> {
match self {
OstreeError::SystemdNotActive => Some(Box::new(self)),
OstreeError::Wsl2SystemdNotActive => Some(Box::new(self)),
}
}
}
impl From<OstreeError> for PlannerError {
fn from(v: OstreeError) -> PlannerError {
PlannerError::Custom(Box::new(v))
}
}