/
githubmirror
/
rpm-ostree
Обзор
Документация
Войти
/
githubmirror
/
rpm-ostree
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
rust/src/daemon.rs
610 строк
22 KB
Colin Walters
tree-wide: `cargo clippy --fix` + a few manual warning fixes
19 дек 2024, 00:21
19 дек 2024, 00:21
14dd3de
Код
Авторство
О чём код?
//! Rust portion of rpmostreed-deployment-utils.cxx //! The code here is mainly involved in converting on-disk state (i.e. ostree commits/deployments) //! into GVariant which will be serialized via DBus. // SPDX-License-Identifier: Apache-2.0 OR MIT use crate::cxxrsutil::*; use crate::ffi::{ OverrideReplacementSource, OverrideReplacementType, ParsedRevision, ParsedRevisionKind, }; use anyhow::{anyhow, format_err, Result}; use cap_std::fs::{Dir, MetadataExt, PermissionsExt}; use cap_std_ext::cap_std; use cap_std_ext::dirext::CapStdExtDirExt; use fn_error_context::context; use glib::prelude::*; use ostree_ext::{gio, glib, ostree}; use rustix::fd::BorrowedFd; use std::collections::{BTreeMap, BTreeSet}; use std::io::Read; use std::path::Path; const RPM_OSTREED_COMMIT_VERIFICATION_CACHE: &str = "rpm-ostree/gpgcheck-cache"; /// Validate basic assumptions on daemon startup. pub(crate) fn daemon_sanitycheck_environment(sysroot: &crate::FFIOstreeSysroot) -> CxxResult<()> { let sysroot = &sysroot.glib_reborrow(); let sysroot_dir = Dir::reopen_dir(unsafe { &BorrowedFd::borrow_raw(sysroot.fd()) })?; let loc = crate::composepost::TRADITIONAL_RPMDB_LOCATION; if let Some(metadata) = sysroot_dir.symlink_metadata_optional(loc)? { if !metadata.is_symlink() { return Err(anyhow::anyhow!("/{} must be a symbolic link", loc).into()); } } Ok(()) } /// Insert values from `v` into the target `dict` with key `k`. fn vdict_insert_strv<'a>(dict: &glib::VariantDict, k: &str, v: impl IntoIterator<Item = &'a str>) { // TODO: drive this into variant_utils in ostree-rs-ext so we don't need // to allocate the intermediate vector. let v: Vec<_> = v.into_iter().collect(); dict.insert_value(k, &v.to_variant()); } /// Insert values from `v` into the target `dict` with key `k`. fn vdict_insert_optvec(dict: &glib::VariantDict, k: &str, v: Option<&Vec<String>>) { let v = v.iter().flat_map(|s| s.iter()).map(|s| s.as_str()); vdict_insert_strv(dict, k, v); } /// Insert keys from the provided map into the target `dict` with key `k`. fn vdict_insert_optmap(dict: &glib::VariantDict, k: &str, v: Option<&BTreeMap<String, String>>) { let v = v.iter().flat_map(|s| s.keys()).map(|s| s.as_str()); vdict_insert_strv(dict, k, v); } /// Insert keys from the provided map into the target `dict` with key `k`. fn vdict_insert_optset(dict: &glib::VariantDict, k: &str, v: Option<&BTreeSet<String>>) { let v = v.iter().flat_map(|s| s.iter()).map(|s| s.as_str()); vdict_insert_strv(dict, k, v); } /// Insert into `dict` metadata keys derived from `origin`. fn deployment_populate_variant_origin( origin: &glib::KeyFile, dict: &glib::VariantDict, ) -> Result<()> { // Convert the origin to a treefile, and operate on that. // See https://github.com/coreos/rpm-ostree/issues/2326 let tf = crate::origin::origin_to_treefile_inner(origin)?; let tf = &tf.parsed; // Package mappings. Note these are inserted unconditionally, even if empty. vdict_insert_optset(dict, "requested-packages", tf.packages.as_ref()); vdict_insert_optmap( dict, "requested-local-packages", tf.derive.packages_local.as_ref(), ); vdict_insert_optmap( dict, "requested-local-fileoverride-packages", tf.derive.packages_local_fileoverride.as_ref(), ); vdict_insert_optset( dict, "requested-base-removals", tf.derive.override_remove.as_ref(), ); vdict_insert_optmap( dict, "requested-base-local-replacements", tf.derive.override_replace_local.as_ref(), ); if let Some(remote_overrides) = tf.derive.override_replace.as_ref() { let v: Vec<(String, Vec<&String>)> = remote_overrides .iter() .map(|ovr| (ovr.from.to_string(), ovr.packages.iter().collect())) .collect(); dict.insert_value("requested-base-remote-replacements", &v.to_variant()); } // Initramfs data. if let Some(initramfs) = tf.derive.initramfs.as_ref() { dict.insert("regenerate-initramfs", initramfs.regenerate); vdict_insert_optvec(dict, "initramfs-args", initramfs.args.as_ref()); vdict_insert_optset(dict, "initramfs-etc", initramfs.etc.as_ref()); } else { // This key is also always injected. dict.insert("regenerate-initramfs", false); } // Other bits. if tf.cliwrap.unwrap_or_default() { dict.insert("cliwrap", true); } Ok(()) } /// Serialize information about the given deployment into the `dict`; /// this will be exposed via DBus and is hence public API. pub(crate) fn deployment_populate_variant( sysroot: &crate::FFIOstreeSysroot, deployment: &crate::FFIOstreeDeployment, dict: &crate::FFIGVariantDict, ) -> CxxResult<()> { let sysroot = &sysroot.glib_reborrow(); let deployment = &deployment.glib_reborrow(); let dict = dict.glib_reborrow(); let id = crate::deployment_generate_id_impl(deployment); // First, basic values from ostree dict.insert("id", &id); dict.insert("osname", deployment.osname().as_str()); dict.insert("checksum", deployment.csum().as_str()); dict.insert_value("serial", &deployment.deployserial().to_variant()); let booted: bool = sysroot .booted_deployment() .map(|b| b.equal(deployment)) .unwrap_or_default(); dict.insert("booted", booted); let live_state = crate::live::get_live_apply_state(sysroot.reborrow_cxx(), deployment.reborrow_cxx())?; if !live_state.inprogress.is_empty() { dict.insert("live-inprogress", live_state.inprogress.as_str()); } if !live_state.commit.is_empty() { dict.insert("live-replaced", live_state.commit.as_str()); } /* Staging status */ dict.insert("staged", deployment.is_staged()); if deployment.is_staged() // XXX: this should check deployment.is_finalization_locked() too now // but the libostree Rust bindings need to be updated && std::path::Path::new("/run/ostree/staged-deployment-locked").exists() { dict.insert("finalization-locked", true); } dict.insert("pinned", deployment.is_pinned()); let unlocked = deployment.unlocked(); // Unwrap safety: This always returns a value dict.insert( "unlocked", ostree::Deployment::unlocked_state_to_string(unlocked).as_str(), ); // Some of the origin-based state. But not all yet; see the rest of the // code in rpmostreed-deployment-utils.cxx if let Some(origin) = deployment.origin() { deployment_populate_variant_origin(&origin, &dict)?; } Ok(()) } /// Load basic layering metadata about a deployment commit. pub fn deployment_layeredmeta_from_commit( deployment: &crate::FFIOstreeDeployment, commit: &crate::FFIGVariant, ) -> CxxResult<crate::ffi::DeploymentLayeredMeta> { let deployment = deployment.glib_reborrow(); let commit = &commit.glib_reborrow(); let layered_meta = deployment_layeredmeta_from_commit_impl(&deployment, commit)?; Ok(layered_meta) } /// Load basic layering metadata about a deployment commit. fn deployment_layeredmeta_from_commit_impl( deployment: &ostree::Deployment, commit: &glib::Variant, ) -> Result<crate::ffi::DeploymentLayeredMeta> { let metadata = &commit.child_value(0); let dict = &glib::VariantDict::new(Some(metadata)); // More recent versions have an explicit clientlayer attribute (which // realistically will always be TRUE). For older versions, we just // rely on the treespec being present. */ let is_layered = dict .lookup("rpmostree.clientlayer") .map_err(anyhow::Error::msg)? .unwrap_or_else(|| dict.contains("rpmostree.spec")); if !is_layered { // SAFETY: return value is "not nullable". let checksum = deployment.csum(); Ok(crate::ffi::DeploymentLayeredMeta { is_layered, base_commit: checksum.into(), clientlayer_version: 0, }) } else { let base_commit = ostree::commit_get_parent(commit) .expect("commit parent") .into(); let clientlayer_version = dict .lookup_value("rpmostree.clientlayer_version", None) .and_then(|u| u.get::<u32>()) .unwrap_or_default(); Ok(crate::ffi::DeploymentLayeredMeta { is_layered, base_commit, clientlayer_version, }) } } pub fn deployment_layeredmeta_load( repo: &crate::FFIOstreeRepo, deployment: &crate::FFIOstreeDeployment, ) -> CxxResult<crate::ffi::DeploymentLayeredMeta> { let repo = repo.glib_reborrow(); let deployment = deployment.glib_reborrow(); let layered_meta = deployment_layeredmeta_load_commit(&repo, &deployment)?; Ok(layered_meta) } /// Load basic layering metadata about a deployment. pub(crate) fn deployment_layeredmeta_load_commit( repo: &ostree::Repo, deployment: &ostree::Deployment, ) -> Result<crate::ffi::DeploymentLayeredMeta> { // SAFETY: return value is "not nullable". let checksum = deployment.csum(); let commit = &repo.load_variant(ostree::ObjectType::Commit, &checksum)?; deployment_layeredmeta_from_commit_impl(deployment, commit) } #[context("Loading origin status")] pub(crate) fn variant_add_remote_status( repo: &crate::FFIOstreeRepo, refspec: &str, base_checksum: &str, dict: &crate::FFIGVariantDict, ) -> CxxResult<()> { let repo = &repo.glib_reborrow(); let dict = &dict.glib_reborrow(); let (maybe_remote, _) = ostree::parse_refspec(refspec)?; let gpg_verify = if let Some(ref remote) = maybe_remote { match repo.remote_get_gpg_verify(remote.as_str()) { Ok(b) => b, Err(err) => { if let Some(ioerr) = err.kind::<gio::IOErrorEnum>() { // If the remote doesn't exist, let's note that so that status can render it // specially. if ioerr == gio::IOErrorEnum::NotFound { let msg = format!("{}", err); dict.insert("remote-error", &msg); return Ok(()); } } return Err(err.into()); } } } else { false }; dict.insert("gpg-enabled", gpg_verify); if !gpg_verify { return Ok(()); // note early return; no need to verify signatures! } // there must a remote if GPG checking is enabled let remote = maybe_remote.unwrap(); match get_cached_signatures_variant(repo, remote.as_str(), base_checksum) { Ok(sigs) => dict.insert_value("signatures", &sigs), Err(err) => { // Somehow, we have a deployment which has gpg-verify=true, but we couldn't verify its // signature. Let's not just bomb out here. We need to return this in the variant so // that `status` can show the appropriate msg. tracing::debug!("failed to get cached signature variant: {}", err); } }; Ok(()) } fn get_cached_signatures_variant( repo: &ostree::Repo, remote: &str, checksum: &str, ) -> Result<glib::Variant> { let run = cap_std::fs::Dir::open_ambient_dir("/run", cap_std::ambient_authority())?; run.create_dir_all(RPM_OSTREED_COMMIT_VERIFICATION_CACHE)?; let cachedir = run.open_dir(RPM_OSTREED_COMMIT_VERIFICATION_CACHE)?; let remote_dir = Path::new(remote); let cached_relpath = remote_dir.join(checksum); if let Some(mut f) = cachedir.open_optional(&cached_relpath)? { let m = f.metadata()?; if m.mode() == (libc::S_IFREG | 0o600) && m.uid() == 0 && m.gid() == 0 { tracing::debug!("signature variant cache hit for ({}, {})", remote, checksum); let bytes = { let mut buf: Vec<u8> = Vec::new(); f.read_to_end(&mut buf)?; glib::Bytes::from_owned(buf) }; let v = glib::Variant::from_bytes::<Vec<glib::Variant>>(&bytes); return Ok(v); } } tracing::debug!( "signature variant cache miss for ({}, {})", remote, checksum ); cachedir.create_dir_all(remote_dir)?; cachedir.remove_file_optional(&cached_relpath)?; let verify_result = repo.verify_commit_for_remote(checksum, remote, gio::Cancellable::NONE)?; let n = verify_result.count_all(); let mut sigs: Vec<glib::Variant> = Vec::with_capacity(n as usize); for i in 0..n { sigs.push(glib::Variant::from_variant(&verify_result.all(i))); // we know index is in range } let v = glib::Variant::array_from_iter_with_type(&glib::Variant::static_variant_type(), sigs); let perms = cap_std::fs::Permissions::from_mode(0o600); cachedir.atomic_write_with_perms(&cached_relpath, v.data_as_bytes(), perms)?; Ok(v) } /// Parse kind and name for a source of package overrides. #[context("Parsing override source '{}'", source)] pub fn parse_override_source(source: &str) -> CxxResult<OverrideReplacementSource> { let (kind_label, name) = source .split_once('=') .ok_or_else(|| format_err!("Not in KIND=NAME format"))?; let kind = match kind_label { "repo" => OverrideReplacementType::Repo, x => return Err(anyhow!("Invalid kind '{}'", x).into()), }; if name.is_empty() { return Err(anyhow!("Empty name").into()); } Ok(OverrideReplacementSource { kind, name: name.to_string(), }) } /// Parse a revision (either a SHA256 checksum or a version metadata value). /// /// This determines `revision` to either be a SHA256 checksum or a version /// metadata value, and then returns the parsed value. /// /// The input string may have a "revision=" prefix to denote a SHA256 /// checksum, or a "version=" prefix to denote a version metadata value. If /// the `revision` string lacks either prefix, the function attempts to infer /// the type of revision. The prefixes are case-insensitive. #[context("Parsing revision '{}'", revision)] pub fn parse_revision(revision: &str) -> CxxResult<ParsedRevision> { // Check for an explicit prefix first. if let Some((key, value)) = revision.split_once('=') { let prefix = key.to_ascii_lowercase(); if prefix == "revision" { // Since this claims to be a checksum, fail if it isn't. ostree::validate_checksum_string(value)?; return Ok(ParsedRevision { kind: ParsedRevisionKind::Checksum, value: value.to_string(), }); } else if prefix == "version" { return Ok(ParsedRevision { kind: ParsedRevisionKind::Version, value: value.to_string(), }); } }; // If it looks like a checksum, assume it is. if ostree::validate_checksum_string(revision).is_ok() { return Ok(ParsedRevision { kind: ParsedRevisionKind::Checksum, value: revision.to_string(), }); }; // Treat anything else as a version metadata value. Ok(ParsedRevision { kind: ParsedRevisionKind::Version, value: revision.to_string(), }) } /// Generate refs for baselayer. /// /// For each deployment, if they are layered deployments, then create a ref /// pointing to their bases. This is mostly to work around ostree's auto-ref /// cleanup. Otherwise we might get into a situation where after the origin ref /// is updated, we lose our parent, which means that users can no longer /// add/delete packages on that deployment. (They can always just re-pull it, but /// let's try to be nice). #[context("Generating baselayer refs")] pub fn generate_baselayer_refs( ffi_sysroot: &crate::ffi::OstreeSysroot, ffi_repo: &crate::ffi::OstreeRepo, ffi_cancellable: &crate::FFIGCancellable, ) -> CxxResult<()> { let sysroot = &ffi_sysroot.glib_reborrow(); let repo = ffi_repo.glib_reborrow(); let cancellable = ffi_cancellable.glib_reborrow().clone(); // Delete all the refs. for ref_entry in repo .list_refs_ext( Some("rpmostree/base"), ostree::RepoListRefsExtFlags::NONE, Some(&cancellable), )? .keys() { repo.transaction_set_refspec(ref_entry, None); } // Regenerate the refs from the base commits. let bases = sysroot.deployments().into_iter().filter_map(|deployment| { deployment_layeredmeta_load_commit(&repo, &deployment) .map(|meta| meta.is_layered.then_some(meta.base_commit)) .transpose() }); for (index, base_rev) in bases.enumerate() { let base_rev = base_rev?; let ref_name = format!("rpmostree/base/{index}"); repo.transaction_set_refspec(&ref_name, Some(&base_rev)); } Ok(()) } /// Appends `part` to `base` in a way such that only characters that /// can be used in a D-Bus object path will be used. E.g. a character /// not in `[A-Z][a-z][0-9]_` will be escaped as `_HEX` where HEX is a /// two-digit hexadecimal number. /// /// Note that his mapping is not bijective - e.g. you cannot go back /// to the original string. /// /// # Arguments /// /// * `base` - The base object path (without trailing '/'). /// * `segment` - The UTF-8 string. pub(crate) fn append_to_object_path(base: &mut String, segment: impl AsRef<str>) { for c in segment.as_ref().as_bytes() { match *c { b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_' => base.push(*c as char), b'/' | b'-' => base.push('_'), c => base.push_str(format!("_{c:02X}").as_str()), } } } /// Appends `parts` to `base` in a way such that only characters that /// can be used in a D-Bus object path will be used. E.g. a character /// not in `[A-Z][a-z][0-9]_` will be escaped as `_HEX` where HEX is a /// two-digit hexadecimal number. /// /// Note that his mapping is not bijective - e.g. you cannot go back /// to the original string. /// /// # Arguments /// /// * `base` - The base object path (without trailing '/'). /// * `segments` - The UTF-8 strings. fn generate_object_path_impl( base: impl std::string::ToString, next_segment: impl AsRef<str>, ) -> Result<String> { let mut base = base.to_string(); if !crate::ffiwrappers::is_object_path(&base)? { return Err(anyhow::anyhow!("Value is not an object path: {base}")); } let segment = next_segment.as_ref(); if segment.is_empty() { return Err(anyhow::anyhow!("Cannot append empty segment: {base}")); } base.push('/'); append_to_object_path(&mut base, segment); Ok(base) } pub(crate) fn generate_object_path(base: &str, next_segment: &str) -> CxxResult<String> { generate_object_path_impl(base, next_segment).map_err(Into::into) } #[cfg(test)] mod test { use super::*; fn origin_to_variant(s: &str) -> glib::VariantDict { let kf = &crate::origin::test::kf_from_str(s).unwrap(); let vdict = glib::VariantDict::new(None); deployment_populate_variant_origin(kf, &vdict).unwrap(); vdict } #[test] fn origin_variant() { let vdict = &origin_to_variant(crate::origin::test::COMPLEX); // Operating on container-typed variants in current glib 0.10 + Rust is painful. Just validate // that a value exists as a start. It's *much* better in glib 0.14, but // porting to that is a ways away. assert!(vdict.lookup_value("requested-packages", None).is_some()); } #[test] fn test_parse_override_source() { let ok_cases = [("repo=custom", (OverrideReplacementType::Repo, "custom"))]; for (input, expected) in ok_cases { let out = parse_override_source(input).unwrap(); assert_eq!(out.kind, expected.0); assert_eq!(out.name, expected.1); } let err_cases = ["", "repo", "repo=", "foo=bar"]; for input in err_cases { parse_override_source(input).unwrap_err(); } } #[test] fn test_parse_revision() { let ok_cases = [ ( "revision=078eea7e710630c0c8aec1071d3529fc307d8772f2ca3435fdc947a39c5a12bf", ( ParsedRevisionKind::Checksum, "078eea7e710630c0c8aec1071d3529fc307d8772f2ca3435fdc947a39c5a12bf", ), ), ( "078eea7e710630c0c8aec1071d3529fc307d8772f2ca3435fdc947a39c5a12bf", ( ParsedRevisionKind::Checksum, "078eea7e710630c0c8aec1071d3529fc307d8772f2ca3435fdc947a39c5a12bf", ), ), ( "version=078eea7e710630c0c8aec1071d3529fc307d8772f2ca3435fdc947a39c5a12bf", ( ParsedRevisionKind::Version, "078eea7e710630c0c8aec1071d3529fc307d8772f2ca3435fdc947a39c5a12bf", ), ), ("version=foo", (ParsedRevisionKind::Version, "foo")), ("version=", (ParsedRevisionKind::Version, "")), ("foo", (ParsedRevisionKind::Version, "foo")), ("", (ParsedRevisionKind::Version, "")), ]; for (input, expected) in ok_cases { let out = parse_revision(input).unwrap(); assert_eq!(out.kind, expected.0); assert_eq!(out.value, expected.1); } let err_cases = ["revision=", "revision=bar"]; for input in err_cases { parse_override_source(input).unwrap_err(); } } #[test] fn test_generate_object_path_impl() { assert!(generate_object_path_impl("/invalid", "").is_err()); assert_eq!( generate_object_path_impl("/base", "single").unwrap(), "/base/single".to_string() ); assert_eq!( generate_object_path_impl("/base", "first!").unwrap(), "/base/first_21".to_string() ); } }