diff --git a/Cargo.lock b/Cargo.lock index 4f7759507c..0829e2bab7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -222,7 +222,7 @@ dependencies = [ "io-lifetimes", "ipnet", "maybe-owned", - "rustix 0.37.19", + "rustix 0.37.20", "windows-sys 0.48.0", "winx", ] @@ -237,7 +237,7 @@ dependencies = [ "cap-primitives", "io-extras", "io-lifetimes", - "rustix 0.37.19", + "rustix 0.37.20", ] [[package]] @@ -257,7 +257,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6012b1e726e3e3ccf8151e2dc9cb454e593e0e7623b0e35464f5e62a15158c27" dependencies = [ "cap-tempfile", - "rustix 0.37.19", + "rustix 0.37.20", ] [[package]] @@ -268,7 +268,7 @@ checksum = "6fd9864347f55a9c31de436ec9d7d3577476f3e6eeb3cc44ae2204de9164f78d" dependencies = [ "cap-std", "rand", - "rustix 0.37.19", + "rustix 0.37.20", "uuid 1.3.2", ] @@ -323,9 +323,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.2" +version = "4.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "401a4694d2bf92537b6867d94de48c4842089645fdcdf6c71865b175d836e9c2" +checksum = "3eab9e8ceb9afdade1ab3f0fd8dbce5b1b2f468ad653baf10e771781b2b67b73" dependencies = [ "clap_builder", "clap_derive", @@ -334,22 +334,21 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.1" +version = "4.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72394f3339a76daf211e57d4bcb374410f3965dcc606dd0e03738c7888766980" +checksum = "9f2763db829349bf00cfc06251268865ed4363b93a943174f638daf3ecdba2cd" dependencies = [ "anstream", "anstyle", - "bitflags 1.3.2", "clap_lex", "strsim", ] [[package]] name = "clap_derive" -version = "4.3.2" +version = "4.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" +checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" dependencies = [ "heck", "proc-macro2", @@ -861,7 +860,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7833d0f115a013d51c55950a3b09d30e4b057be9961b709acb9b5b17a1108861" dependencies = [ "io-lifetimes", - "rustix 0.37.19", + "rustix 0.37.20", "windows-sys 0.48.0", ] @@ -1294,11 +1293,12 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.17.3" +version = "0.17.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef509aa9bc73864d6756f0d34d35504af3cf0844373afe9b8669a5b8005a729" +checksum = "8ff8cc23a7393a397ed1d7f56e6365cba772aba9f9912ab968b03043c395d057" dependencies = [ "console", + "instant", "number_prefix", "portable-atomic", "unicode-width", @@ -1354,7 +1354,7 @@ checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" dependencies = [ "hermit-abi 0.3.1", "io-lifetimes", - "rustix 0.37.19", + "rustix 0.37.20", "windows-sys 0.48.0", ] @@ -1508,7 +1508,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffc89ccdc6e10d6907450f753537ebc5c5d3460d2e4e62ea74bd571db62c0f9e" dependencies = [ - "rustix 0.37.19", + "rustix 0.37.20", ] [[package]] @@ -1693,9 +1693,9 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "openssl" -version = "0.10.52" +version = "0.10.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56" +checksum = "69b3f656a17a6cbc115b5c7a40c616947d213ba182135b014d6051b73ab6f019" dependencies = [ "bitflags 1.3.2", "cfg-if", @@ -1725,9 +1725,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.87" +version = "0.9.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e" +checksum = "c2ce0f250f34a308dcfdbb351f511359857d4ed2134ba715a4eadd46e1ffd617" dependencies = [ "cc", "libc", @@ -1775,9 +1775,9 @@ dependencies = [ [[package]] name = "ostree-ext" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8903d59c4a56794b6ef8a2b11a5674fe85b370f1da33ae34450a6de1e39a20d6" +checksum = "a690495144c18cb333a67a2ec61dd008831710bbd37804cfa79ab93b51146a6f" dependencies = [ "anyhow", "async-compression 0.3.15", @@ -1803,7 +1803,7 @@ dependencies = [ "ostree", "pin-project", "regex", - "rustix 0.37.19", + "rustix 0.37.20", "serde", "serde_json", "tar", @@ -1928,9 +1928,9 @@ checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" [[package]] name = "portable-atomic" -version = "0.3.19" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b" +checksum = "767eb9f07d4a5ebcb39bbf2d452058a93c011373abf6832e24194a1c3f004794" [[package]] name = "ppv-lite86" @@ -2088,13 +2088,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.1" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.1", + "regex-syntax 0.7.2", ] [[package]] @@ -2114,9 +2114,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "reqwest" @@ -2209,7 +2209,7 @@ dependencies = [ "reqwest", "rpmostree-client", "rust-ini", - "rustix 0.37.19", + "rustix 0.37.20", "serde", "serde_derive", "serde_json", @@ -2252,9 +2252,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.19" +version = "0.37.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" dependencies = [ "bitflags 1.3.2", "errno", @@ -2330,18 +2330,18 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.163" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" dependencies = [ "proc-macro2", "quote", @@ -2547,7 +2547,7 @@ dependencies = [ "cfg-if", "fastrand", "redox_syscall 0.3.5", - "rustix 0.37.19", + "rustix 0.37.20", "windows-sys 0.45.0", ] diff --git a/Cargo.toml b/Cargo.toml index c414e92e7a..e28bb83189 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,14 +63,14 @@ fail = { version = "0.5", features = ["failpoints"] } fn-error-context = "0.2.0" futures = "0.3.28" indoc = "2.0.1" -indicatif = "0.17.3" +indicatif = "0.17.5" is-terminal = "0.4" libc = "0.2.146" libdnf-sys = { path = "rust/libdnf-sys", version = "0.1.0" } maplit = "1.0" memfd = "0.6.0" nix = "0.26.1" -openssl = "0.10.49" +openssl = "0.10.54" once_cell = "1.18.0" os-release = "0.1.0" ostree-ext = "0.11.0" @@ -78,11 +78,11 @@ paste = "1.0" phf = { version = "0.11", features = ["macros"] } rand = "0.8.5" rayon = "1.6.0" -regex = "1.7" +regex = "1.8" reqwest = { version = "0.11", features = ["native-tls", "blocking", "gzip"] } rpmostree-client = { path = "rust/rpmostree-client", version = "0.1.0" } rust-ini = "0.19.0" -serde = { version = "1.0.163", features = ["derive"] } +serde = { version = "1.0.164", features = ["derive"] } serde_derive = "1.0.118" serde_json = "1.0.96" serde_yaml = "0.9.16" diff --git a/ci/test-container.sh b/ci/test-container.sh index 0a5a0ba3e2..c7490e9044 100755 --- a/ci/test-container.sh +++ b/ci/test-container.sh @@ -58,8 +58,6 @@ versionid=$(. /usr/lib/os-release && echo $VERSION_ID) # Let's start by trying to install a bona fide module. # NOTE: If changing this also change the layering-modules test case $versionid in - 36) module=cri-o:1.23/default;; - 37) module=cri-o:1.24/default;; 38) module=cri-o:1.25/default;; *) assert_not_reached "Unsupported Fedora version: $versionid";; esac @@ -77,16 +75,8 @@ fi versionid=$(grep -E '^VERSION_ID=' /etc/os-release) versionid=${versionid:11} # trim off VERSION_ID= case $versionid in - 37) - url_suffix=2.14.0/3.fc37/x86_64/ignition-2.14.0-3.fc37.x86_64.rpm - # 2.14.0-4 - koji_url="https://koji.fedoraproject.org/koji/buildinfo?buildID=2013062" - koji_kernel_url="https://koji.fedoraproject.org/koji/buildinfo?buildID=2084352" - kver=6.0.7 - krev=301 - ;; 38) - url_suffix=2.15.0/2.fc38/x86_64/ignition-2.15.0-2.fc38.x86_64.rpm + url_suffix=2.15.0/4.fc37/x86_64/ignition-2.15.0-4.fc37.x86_64.rpm # 2.15.0-3 koji_url="https://koji.fedoraproject.org/koji/buildinfo?buildID=2158585" koji_kernel_url="https://koji.fedoraproject.org/koji/buildinfo?buildID=2174317" diff --git a/configure.ac b/configure.ac index f9aface629..b51a529bd2 100644 --- a/configure.ac +++ b/configure.ac @@ -3,7 +3,7 @@ dnl dnl SEE RELEASE.md FOR INSTRUCTIONS ON HOW TO DO A RELEASE. dnl m4_define([year_version], [2023]) -m4_define([release_version], [4]) +m4_define([release_version], [5]) m4_define([package_version], [year_version.release_version]) AC_INIT([rpm-ostree], [package_version], [coreos@lists.fedoraproject.org]) AC_CONFIG_HEADER([config.h]) diff --git a/docs/_config.yml b/docs/_config.yml index 006ac4bd2d..26bd9ccfdd 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,9 +1,12 @@ -title: coreos/rpm-ostree +# Template generated by https://github.com/coreos/repo-templates; do not edit downstream + +# To test documentation changes locally or using GitHub Pages, see: +# https://github.com/coreos/fedora-coreos-tracker/blob/main/docs/testing-project-documentation-changes.md + +title: rpm-ostree description: rpm-ostree documentation baseurl: "/rpm-ostree" url: "https://coreos.github.io" -# Comment above and use below for local development -# url: "http://localhost:4000" permalink: /:title/ markdown: kramdown kramdown: diff --git a/libdnf b/libdnf index 2362930d10..3fca06e8b1 160000 --- a/libdnf +++ b/libdnf @@ -1 +1 @@ -Subproject commit 2362930d10375a348bf2659db14b0ca4b911d4b7 +Subproject commit 3fca06e8b1037f117ba57b5e824ea59a343b44ed diff --git a/man/rpm-ostree.xml b/man/rpm-ostree.xml index 1b530856f6..dcf7e1974c 100644 --- a/man/rpm-ostree.xml +++ b/man/rpm-ostree.xml @@ -328,6 +328,20 @@ Boston, MA 02111-1307, USA. + + search + + + + Takes one or more query terms as arguments. The packages are + searched within the enabled repositories in + /etc/yum.repos.d/. Packages can be + overlayed and removed using the install + and uninstall commands. + + + + rebase diff --git a/packaging/rpm-ostree.spec.in b/packaging/rpm-ostree.spec.in index 127e807281..67d2793762 100644 --- a/packaging/rpm-ostree.spec.in +++ b/packaging/rpm-ostree.spec.in @@ -3,9 +3,9 @@ Summary: Hybrid image/package system Name: rpm-ostree -Version: 2023.4 +Version: 2023.5 Release: 1%{?dist} -License: LGPLv2+ +License: LGPL-2.0-or-later URL: https://github.com/coreos/rpm-ostree # This tarball is generated via "cd packaging && make -f Makefile.dist-packaging dist-snapshot" # in the upstream git. It also contains vendored Rust sources. diff --git a/rust/rpmostree-client/Cargo.toml b/rust/rpmostree-client/Cargo.toml index a41e1b92d3..2886312870 100644 --- a/rust/rpmostree-client/Cargo.toml +++ b/rust/rpmostree-client/Cargo.toml @@ -12,6 +12,6 @@ publish = false [dependencies] anyhow = "1.0.69" -serde = { version = "1.0.163", features = ["derive"] } +serde = { version = "1.0.164", features = ["derive"] } serde_derive = "1.0.118" serde_json = "1.0.96" diff --git a/rust/src/builtins/scriptlet_intercept/groupadd.rs b/rust/src/builtins/scriptlet_intercept/groupadd.rs index 0b74a0a463..ef9e6dc393 100644 --- a/rust/src/builtins/scriptlet_intercept/groupadd.rs +++ b/rust/src/builtins/scriptlet_intercept/groupadd.rs @@ -46,7 +46,12 @@ fn cli_cmd() -> Command { Command::new(name) .bin_name(name) .about("create a new group") - .arg(Arg::new("force").short('f').long("force")) + .arg( + Arg::new("force") + .short('f') + .long("force") + .action(ArgAction::SetTrue), + ) .arg( Arg::new("gid") .short('g') diff --git a/rust/src/container.rs b/rust/src/container.rs index 6ca9605dbf..468ced8809 100644 --- a/rust/src/container.rs +++ b/rust/src/container.rs @@ -7,13 +7,14 @@ use std::num::NonZeroU32; use std::process::Command; use std::rc::Rc; -use anyhow::Result; +use anyhow::{Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; use cap_std::fs::Dir; use cap_std_ext::cap_std; use cap_std_ext::prelude::*; use chrono::prelude::*; use clap::Parser; +use fn_error_context::context; use ostree::glib; use ostree_ext::chunking::ObjectMetaSized; use ostree_ext::container::{Config, ExportOpts, ImageReference}; @@ -425,7 +426,10 @@ pub fn container_encapsulate(args: Vec) -> CxxResult<()> { let package_structure = opt .previous_build_manifest .as_ref() - .map(|p| oci_spec::image::ImageManifest::from_file(&p).map_err(anyhow::Error::new)) + .map(|p| { + oci_spec::image::ImageManifest::from_file(&p) + .map_err(|e| anyhow::anyhow!("Failed to read previous manifest {p}: {e}")) + }) .transpose()?; let mut copy_meta_keys = opt.copy_meta_keys; @@ -483,6 +487,7 @@ struct UpdateFromRunningOpts { } // This reimplements https://github.com/ostreedev/ostree/pull/2691 basically +#[context("Finding encapsulated commits")] fn find_encapsulated_commits(repo: &Utf8Path) -> Result> { let objects = Dir::open_ambient_dir(&repo.join("objects"), cap_std::ambient_authority())?; let mut r = Vec::new(); @@ -550,7 +555,10 @@ pub(crate) fn deploy_from_self_entrypoint(args: Vec) -> CxxResult<()> { let encapsulated_commits = find_encapsulated_commits(src_repo_path)?; let commit = match encapsulated_commits.as_slice() { [] => return Err(format!("No encapsulated commit found in container").into()), - [c] => c.as_str(), + [c] => { + ostree::validate_checksum_string(&c)?; + c.as_str() + } o => return Err(format!("Found {} commit objects, expected just one", o.len()).into()), }; @@ -564,12 +572,14 @@ pub(crate) fn deploy_from_self_entrypoint(args: Vec) -> CxxResult<()> { opts.insert("refs", &&refs[..]); opts.insert("flags", &(flags.bits() as i32)); let options = opts.to_variant(); - target_repo.pull_with_options( - &format!("file://{src_repo_path}"), - &options, - None, - cancellable, - )?; + target_repo + .pull_with_options( + &format!("file://{src_repo_path}"), + &options, + None, + cancellable, + ) + .context("Pulling from embedded repo")?; } println!("Imported: {commit}"); diff --git a/rust/src/importer.rs b/rust/src/importer.rs index eb91ff23a1..f552bc64ca 100644 --- a/rust/src/importer.rs +++ b/rust/src/importer.rs @@ -346,16 +346,9 @@ fn tweak_imported_file_info(file_info: &FileInfo, ro_executables: bool) { #[context("Analyzing {}", path)] fn import_filter( path: &str, - file_info: &FileInfo, + _file_info: &FileInfo, skip_extraneous: bool, ) -> Result { - // Sanity check that RPM isn't using CPIO id fields. - let uid = file_info.attribute_uint32("unix::uid"); - let gid = file_info.attribute_uint32("unix::gid"); - if uid != 0 || gid != 0 { - bail!("Unexpected non-root owned path (marked as {}:{})", uid, gid); - } - // Skip some empty lock files, they are known to cause problems: // https://github.com/projectatomic/rpm-ostree/pull/1002 if path.starts_with("/usr/etc/selinux") && path.ends_with(".LOCK") { diff --git a/rust/src/passwd.rs b/rust/src/passwd.rs index 9348f8627d..4bedf107bc 100644 --- a/rust/src/passwd.rs +++ b/rust/src/passwd.rs @@ -315,8 +315,8 @@ fn passwd_compose_prep_impl( rootfs.create_dir_with(dest, &db)?; // TODO(lucab): consider reworking these to avoid boolean results. - let found_passwd_data = data_from_json(rootfs, treefile, dest, "passwd")?; - let found_groups_data = data_from_json(rootfs, treefile, dest, "group")?; + let found_passwd_data = write_data_from_treefile(rootfs, treefile, dest, &PasswdKind::User)?; + let found_groups_data = write_data_from_treefile(rootfs, treefile, dest, &PasswdKind::Group)?; // We should error if we are getting passwd data from JSON and group from // previous commit, or vice versa, as that'll confuse everyone when it goes @@ -340,53 +340,110 @@ fn passwd_compose_prep_impl( Ok(()) } -fn data_from_json( +// PasswdKind includes 2 types: user and group. +#[derive(Debug)] +enum PasswdKind { + User, + Group, +} + +impl PasswdKind { + // Get user/group passwd file + fn passwd_file(&self) -> &'static str { + return match *self { + PasswdKind::User => "passwd", + PasswdKind::Group => "group", + }; + } + // Get user/group shadow file + fn shadow_file(&self) -> &'static str { + return match *self { + PasswdKind::User => "shadow", + PasswdKind::Group => "gshadow", + }; + } +} + +// This function writes the static passwd/group data from the treefile to the +// target root filesystem. +fn write_data_from_treefile( rootfs: &Dir, treefile: &mut Treefile, dest_path: &str, - target: &str, + target: &PasswdKind, ) -> Result { anyhow::ensure!(!dest_path.is_empty(), "missing destination path"); let append_unique_entries = match target { - "passwd" => passwd_append_unique, - "group" => group_append_unique, - x => anyhow::bail!("invalid merge target '{}'", x), + PasswdKind::User => passwd_append_unique, + PasswdKind::Group => group_append_unique, }; - let target_etc_filename = format!("{}{}", dest_path, target); + let passwd_name = target.passwd_file(); + let target_passwd_path = format!("{}{}", dest_path, passwd_name); // Migrate the check data from the specified file to /etc. - let mut src_file = if target == "passwd" { - let check_passwd_file = match treefile.parsed.get_check_passwd() { - CheckPasswd::File(cfg) => cfg, - _ => return Ok(false), - }; - treefile.externals.passwd_file_mut(check_passwd_file)? - } else if target == "group" { - let check_groups_file = match treefile.parsed.get_check_groups() { - CheckGroups::File(cfg) => cfg, - _ => return Ok(false), - }; - treefile.externals.group_file_mut(check_groups_file)? - } else { - unreachable!("impossible merge target '{}'", target); + let mut src_file = match target { + PasswdKind::User => { + let check_passwd_file = match treefile.parsed.get_check_passwd() { + CheckPasswd::File(cfg) => cfg, + _ => return Ok(false), + }; + treefile.externals.passwd_file_mut(check_passwd_file)? + } + PasswdKind::Group => { + let check_groups_file = match treefile.parsed.get_check_groups() { + CheckGroups::File(cfg) => cfg, + _ => return Ok(false), + }; + treefile.externals.group_file_mut(check_groups_file)? + } }; let mut seen_names = HashSet::new(); rootfs - .atomic_replace_with(&target_etc_filename, |dest_bufwr| -> Result<()> { + .atomic_replace_with(&target_passwd_path, |dest_bufwr| -> Result<()> { dest_bufwr .get_mut() .as_file_mut() .set_permissions(DEFAULT_PERMS.clone())?; let mut buf_rd = BufReader::new(&mut src_file); - append_unique_entries(&mut buf_rd, &mut seen_names, dest_bufwr) - .with_context(|| format!("failed to process '{}' content from JSON", &target))?; + append_unique_entries(&mut buf_rd, &mut seen_names, dest_bufwr).with_context(|| { + format!("failed to process '{}' content from JSON", &passwd_name) + })?; Ok(()) }) - .with_context(|| format!("failed to write /{}", &target_etc_filename))?; + .with_context(|| format!("failed to write /{}", &target_passwd_path))?; + // Regenerate etc/{,g}shadow to sync with etc/{passwd,group} + let db = rootfs.open(target_passwd_path).map(BufReader::new)?; + let shadow_name = target.shadow_file(); + let target_shadow_path = format!("{}{}", dest_path, shadow_name); + + match target { + PasswdKind::User => { + let entries = nameservice::passwd::parse_passwd_content(db)?; + rootfs + .atomic_replace_with(&target_shadow_path, |target_shadow| -> Result<()> { + for user in entries { + writeln!(target_shadow, "{}:*::0:99999:7:::", user.name)?; + } + Ok(()) + }) + .with_context(|| format!("Writing {target_shadow_path}"))?; + } + PasswdKind::Group => { + let entries = nameservice::group::parse_group_content(db)?; + rootfs + .atomic_replace_with(&target_shadow_path, |target_shadow| -> Result<()> { + for group in entries { + writeln!(target_shadow, "{}:::", group.name)?; + } + Ok(()) + }) + .with_context(|| format!("Writing {target_shadow_path}"))?; + } + } Ok(true) } diff --git a/src/app/libmain.cxx b/src/app/libmain.cxx index 60059238eb..497bef878b 100644 --- a/src/app/libmain.cxx +++ b/src/app/libmain.cxx @@ -73,6 +73,8 @@ static RpmOstreeCommand commands[] = { "Overlay additional packages", rpmostree_builtin_install }, { "uninstall", static_cast (RPM_OSTREE_BUILTIN_FLAG_CONTAINER_CAPABLE), "Remove overlayed additional packages", rpmostree_builtin_uninstall }, + { "search", static_cast (RPM_OSTREE_BUILTIN_FLAG_CONTAINER_CAPABLE), + "Search for packages", rpmostree_builtin_search }, { "override", static_cast (RPM_OSTREE_BUILTIN_FLAG_LOCAL_CMD), "Manage base package overrides", rpmostree_builtin_override }, { "reset", static_cast (RPM_OSTREE_BUILTIN_FLAG_SUPPORTS_PKG_INSTALLS), @@ -154,6 +156,14 @@ static GOptionEntry pkg_entries[] "Remove overlayed additional package", "PKG" }, { NULL } }; +static int +cmp_by_name (const void *a, const void *b) +{ + struct RpmOstreeCommand *command_a = (RpmOstreeCommand *)a; + struct RpmOstreeCommand *command_b = (RpmOstreeCommand *)b; + return strcmp (command_a->name, command_b->name); +} + static GOptionContext * option_context_new_with_commands (RpmOstreeCommandInvocation *invocation, RpmOstreeCommand *commands) @@ -171,6 +181,13 @@ option_context_new_with_commands (RpmOstreeCommandInvocation *invocation, else /* top level */ g_string_append (summary, "Builtin Commands:"); + int command_count = 0; + for (RpmOstreeCommand *command = commands; command->name != NULL; command++) + { + command_count++; + } + + qsort (commands, command_count, sizeof (RpmOstreeCommand), cmp_by_name); for (RpmOstreeCommand *command = commands; command->name != NULL; command++) { gboolean hidden = (command->flags & RPM_OSTREE_BUILTIN_FLAG_HIDDEN) > 0; diff --git a/src/app/rpmostree-builtins.h b/src/app/rpmostree-builtins.h index 43ef9ca6a8..877bdd34e0 100644 --- a/src/app/rpmostree-builtins.h +++ b/src/app/rpmostree-builtins.h @@ -50,6 +50,7 @@ BUILTINPROTO (internals); BUILTINPROTO (container); BUILTINPROTO (install); BUILTINPROTO (uninstall); +BUILTINPROTO (search); BUILTINPROTO (override); BUILTINPROTO (kargs); BUILTINPROTO (reset); diff --git a/src/app/rpmostree-pkg-builtins.cxx b/src/app/rpmostree-pkg-builtins.cxx index f2c26730d0..1ca3564c83 100644 --- a/src/app/rpmostree-pkg-builtins.cxx +++ b/src/app/rpmostree-pkg-builtins.cxx @@ -31,6 +31,8 @@ #include +#include + static char *opt_osname; static gboolean opt_reboot; static gboolean opt_dry_run; @@ -310,3 +312,102 @@ rpmostree_builtin_uninstall (int argc, char **argv, RpmOstreeCommandInvocation * return pkg_change (invocation, sysroot_proxy, FALSE, (const char *const *)opt_install, (const char *const *)argv, cancellable, error); } + +struct cstrless +{ + bool + operator() (const gchar *a, const gchar *b) const + { + return strcmp (a, b) < 0; + } +}; + +gboolean +rpmostree_builtin_search (int argc, char **argv, RpmOstreeCommandInvocation *invocation, + GCancellable *cancellable, GError **error) +{ + GOptionContext *context; + glnx_unref_object RPMOSTreeSysroot *sysroot_proxy = NULL; + + context = g_option_context_new ("PACKAGE [PACKAGE...]"); + g_option_context_add_main_entries (context, install_option_entry, NULL); + g_option_context_add_main_entries (context, uninstall_option_entry, NULL); + + if (!rpmostree_option_context_parse (context, option_entries, &argc, &argv, invocation, + cancellable, NULL, NULL, &sysroot_proxy, error)) + return FALSE; + + if (argc < 2) + { + rpmostree_usage_error (context, "At least one PACKAGE must be specified", error); + return FALSE; + } + + glnx_unref_object RPMOSTreeOS *os_proxy = NULL; + + if (!rpmostree_load_os_proxy (sysroot_proxy, opt_osname, cancellable, &os_proxy, error)) + return FALSE; + + g_autoptr (GPtrArray) arg_names = g_ptr_array_new (); + for (guint i = 1; i < argc; i++) + { + g_ptr_array_add (arg_names, (char *)argv[i]); + } + g_ptr_array_add (arg_names, NULL); + + g_autoptr (GVariant) out_packages = NULL; + + if (!rpmostree_os_call_search_sync (os_proxy, (const char *const *)arg_names->pdata, + &out_packages, cancellable, error)) + return FALSE; + + g_autoptr (GVariantIter) iter1 = NULL; + g_variant_get (out_packages, "aa{sv}", &iter1); + + g_autoptr (GVariantIter) iter2 = NULL; + std::set query_set; + + while (g_variant_iter_loop (iter1, "a{sv}", &iter2)) + { + const gchar *key; + const gchar *name; + const gchar *summary; + const gchar *query; + const gchar *match_group = ""; + + g_autoptr (GVariant) value = NULL; + + while (g_variant_iter_loop (iter2, "{sv}", &key, &value)) + { + if (strcmp (key, "key") == 0) + g_variant_get (value, "s", &query); + else if (strcmp (key, "name") == 0) + g_variant_get (value, "s", &name); + else if (strcmp (key, "summary") == 0) + g_variant_get (value, "s", &summary); + } + + if (!query_set.count (query)) + { + query_set.insert (query); + + if (strcmp (query, "match_group_a") == 0) + match_group = "Summary & Name"; + else if (strcmp (query, "match_group_b") == 0) + match_group = "Name"; + else if (strcmp (query, "match_group_c") == 0) + match_group = "Summary"; + + g_print ("\n===== %s Matched =====\n", match_group); + } + + g_print ("%s : %s\n", name, summary); + } + + if (query_set.size () == 0) + { + g_print ("No matches found.\n"); + } + + return TRUE; +} diff --git a/src/daemon/org.projectatomic.rpmostree1.xml b/src/daemon/org.projectatomic.rpmostree1.xml index b11ff5dfd2..7765b3fe54 100644 --- a/src/daemon/org.projectatomic.rpmostree1.xml +++ b/src/daemon/org.projectatomic.rpmostree1.xml @@ -477,6 +477,12 @@ + + + + + + diff --git a/src/daemon/rpm-ostreed.service b/src/daemon/rpm-ostreed.service index 8d1ef69cba..406ead652f 100644 --- a/src/daemon/rpm-ostreed.service +++ b/src/daemon/rpm-ostreed.service @@ -20,10 +20,6 @@ MountFlags=slave # and have a system rpm-ostreed-transaction.service that runs privileged # but as a subprocess. ProtectHome=true -# Explicitly list paths here which we should never access. The initial -# entry here ensures that the skopeo process we fork won't interact with -# application containers. -BindReadOnlyPaths=-/var/lib/containers NotifyAccess=main # Significantly bump this timeout from the default because # we do a lot of stuff on daemon startup. diff --git a/src/daemon/rpmostreed-os.cxx b/src/daemon/rpmostreed-os.cxx index 090c2dfd13..52185c663b 100644 --- a/src/daemon/rpmostreed-os.cxx +++ b/src/daemon/rpmostreed-os.cxx @@ -36,6 +36,11 @@ #include "rpmostreed-transaction.h" #include "rpmostreed-utils.h" +#include + +#include +#include + typedef struct _RpmostreedOSClass RpmostreedOSClass; struct _RpmostreedOS @@ -141,7 +146,7 @@ os_authorize_method (GDBusInterfaceSkeleton *interface, GDBusMethodInvocation *i else if (g_strcmp0 (method_name, "GetDeploymentBootConfig") == 0 || g_strcmp0 (method_name, "ListRepos") == 0 || g_strcmp0 (method_name, "WhatProvides") == 0 - || g_strcmp0 (method_name, "GetPackages") == 0) + || g_strcmp0 (method_name, "GetPackages") == 0 || g_strcmp0 (method_name, "Search") == 0) { /* Note: early return here because no need authentication * for these methods @@ -1138,6 +1143,189 @@ os_handle_get_packages (RPMOSTreeOS *interface, GDBusMethodInvocation *invocatio return TRUE; } +/* helper function to sort and search within a set of (const gchar *) */ +struct cstrless +{ + bool + operator() (const gchar *a, const gchar *b) const + { + return strcmp (a, b) < 0; + } +}; + +/* wrapper function to both query for packages and add them to the builder */ +static void +query_results_to_builder (HyQuery query, GVariantBuilder *builder, const gchar *id, + std::set *result_set) +{ + g_autoptr (GPtrArray) pkglist = hy_query_run (query); + for (guint i = 0; i < pkglist->len && (*result_set).size () < 50; i++) + { + auto pkg = static_cast (g_ptr_array_index (pkglist, i)); + if (!(*result_set).count (dnf_package_get_name (pkg))) + { + os_add_package_info_to_builder (pkg, builder, id); + (*result_set).insert (dnf_package_get_name (pkg)); + } + } +} + +/* helper function to apply Name/Summary or HY_EQ/HY_SUBSTR filters on search term */ +static void +apply_search_filter (HyQuery *query, int keyname, const gchar *const name, int cmp_type) +{ + if (!hy_is_glob_pattern (name)) + { + hy_query_filter (*query, keyname, cmp_type | HY_ICASE, name); + } + else + { + hy_query_filter (*query, keyname, HY_GLOB | HY_ICASE, name); + } +} + +/* helper function to filter package query results */ +static void +search_packages_by_filter (HyQuery query, GVariantBuilder *builder, const gchar *const *names, + std::vector keynames, const gchar *id) +{ + std::set result_set; + HyQuery intermediate_query = hy_query_clone (query); + HyQuery final_query = hy_query_clone (query); + + int names_count = 0; + for (guint i = 0; names[i] != NULL; i++) + { + names_count++; + } + + /* Name/Summary matches */ + if (keynames.size () < 2) + { + hy_query_clear (query); + for (guint i = 0; names[i] != NULL; i++) + { + apply_search_filter (&query, keynames[0], names[i], HY_EQ); + } + query_results_to_builder (query, builder, id, &result_set); + + hy_query_clear (query); + for (guint i = 0; names[i] != NULL; i++) + { + apply_search_filter (&query, keynames[0], names[i], HY_SUBSTR); + } + query_results_to_builder (query, builder, id, &result_set); + } + + /* Name AND Summary matches for more than one search term */ + /* ========================================================================================= + For each search term, apply a query with the keyname filter (Name or Summary) and unions the + results. This allows multi-term searches to return matches when search terms are found in + either the Name or Summary of a package. After this, return the intersection of the results + for each search term to filter out results that do not contain all matching terms. + ========================================================================================= */ + else if (keynames.size () >= 2 && names_count >= 2) + { + + for (guint i = 0; names[i] != NULL; i++) + { + hy_query_clear (intermediate_query); + for (guint j = 0; j < keynames.size (); j++) + { + hy_query_clear (query); + apply_search_filter (&query, keynames[j], names[i], HY_EQ); + + if (j != 0) + { + hy_query_union (intermediate_query, query); + } + else + { + intermediate_query = hy_query_clone (query); + } + + hy_query_clear (query); + apply_search_filter (&query, keynames[j], names[i], HY_SUBSTR); + hy_query_union (intermediate_query, query); + } + + if (i != 0) + { + hy_query_intersection (final_query, intermediate_query); + } + else + { + final_query = hy_query_clone (intermediate_query); + } + } + query_results_to_builder (final_query, builder, id, &result_set); + } + + /* Name AND Summary matches for only one search term */ + /* ========================================================================================= + For the case of a single search term, return the intersection of both Name and Summary matches + of the search term (instead of the union for multi-term searches). + ========================================================================================= */ + else if (keynames.size () >= 2 && names_count < 2) + { + for (guint i = 0; i < keynames.size (); i++) + { + hy_query_clear (query); + apply_search_filter (&query, keynames[i], names[0], HY_EQ); + intermediate_query = hy_query_clone (query); + + hy_query_clear (query); + apply_search_filter (&query, keynames[i], names[0], HY_SUBSTR); + hy_query_union (intermediate_query, query); + + if (i != 0) + { + hy_query_intersection (final_query, intermediate_query); + } + else + { + final_query = hy_query_clone (intermediate_query); + } + } + query_results_to_builder (final_query, builder, id, &result_set); + } +} + +static gboolean +os_handle_search (RPMOSTreeOS *interface, GDBusMethodInvocation *invocation, + const gchar *const *names) +{ + GError *local_error = NULL; + g_autoptr (GCancellable) cancellable = NULL; + + sd_journal_print (LOG_INFO, "Handling Search for caller %s", + g_dbus_method_invocation_get_sender (invocation)); + + g_autoptr (DnfContext) dnfctx + = os_create_dnf_context_simple (interface, TRUE, cancellable, &local_error); + if (dnfctx == NULL) + return os_throw_dbus_invocation_error (invocation, &local_error); + + hy_autoquery HyQuery query = hy_query_create (dnf_context_get_sack (dnfctx)); + + GVariantBuilder builder; + g_variant_builder_init (&builder, (const GVariantType *)"aa{sv}"); + + std::vector keynames_a = { HY_PKG_NAME, HY_PKG_SUMMARY }; + search_packages_by_filter (query, &builder, names, keynames_a, "match_group_a"); + + std::vector keynames_b = { HY_PKG_NAME }; + search_packages_by_filter (query, &builder, names, keynames_b, "match_group_b"); + + std::vector keynames_c = { HY_PKG_SUMMARY }; + search_packages_by_filter (query, &builder, names, keynames_c, "match_group_c"); + + GVariant *pkgs_result = g_variant_builder_end (&builder); + g_dbus_method_invocation_return_value (invocation, g_variant_new ("(@aa{sv})", pkgs_result)); + + return TRUE; +} + /* This is an older variant of Cleanup, kept for backcompat */ static gboolean os_handle_clear_rollback_target (RPMOSTreeOS *interface, GDBusMethodInvocation *invocation, @@ -1813,6 +2001,7 @@ rpmostreed_os_iface_init (RPMOSTreeOSIface *iface) iface->handle_finalize_deployment = os_handle_finalize_deployment; iface->handle_what_provides = os_handle_what_provides; iface->handle_get_packages = os_handle_get_packages; + iface->handle_search = os_handle_search; /* legacy cleanup API; superseded by Cleanup() */ iface->handle_clear_rollback_target = os_handle_clear_rollback_target; /* legacy deployment change API; superseded by UpdateDeployment() */ diff --git a/src/libpriv/rpmostree-core.cxx b/src/libpriv/rpmostree-core.cxx index 9ac15b3154..5c3b0144b0 100644 --- a/src/libpriv/rpmostree-core.cxx +++ b/src/libpriv/rpmostree-core.cxx @@ -1466,7 +1466,18 @@ check_goal_solution (RpmOstreeContext *self, GPtrArray *removed_pkgnames, * for it anyway so that we get a bug report in case it somehow happens. */ { g_autoptr (GPtrArray) packages = dnf_goal_get_packages (goal, DNF_PACKAGE_INFO_REINSTALL, -1); - g_assert_cmpint (packages->len, ==, 0); + if (packages->len > 0) + { + g_autoptr (GString) buf = g_string_new (""); + for (guint i = 0; i < packages->len; i++) + { + if (i > 0) + g_string_append_c (buf, ' '); + auto pkg = static_cast (packages->pdata[i]); + g_string_append (buf, dnf_package_get_name (pkg)); + } + return glnx_throw (error, "Request to reinstall exact base package versions: %s", buf->str); + } } /* Look at UPDATE and DOWNGRADE, and see whether they're doing what we expect */ diff --git a/src/libpriv/rpmostree-kernel.cxx b/src/libpriv/rpmostree-kernel.cxx index 2cf6d00fa8..e5dcb8f1f3 100644 --- a/src/libpriv/rpmostree-kernel.cxx +++ b/src/libpriv/rpmostree-kernel.cxx @@ -89,7 +89,8 @@ find_kernel_and_initramfs_in_bootdir (int rootfs_dfd, const char *bootdir, char if (out_ksuffix ? g_str_has_prefix (name, "vmlinuz-") : g_str_equal (name, "vmlinuz")) { if (ret_kernel) - return glnx_throw (error, "Multiple vmlinuz%s in %s", out_ksuffix ? "-" : "", bootdir); + return glnx_throw (error, "Multiple vmlinuz%s in %s, occurrences '%s' and '%s/%s'", + out_ksuffix ? "-" : "", bootdir, ret_kernel, bootdir, name); if (out_ksuffix) ret_ksuffix = g_strdup (name + strlen ("vmlinuz-")); ret_kernel = g_strconcat (bootdir, "/", name, NULL); @@ -97,7 +98,8 @@ find_kernel_and_initramfs_in_bootdir (int rootfs_dfd, const char *bootdir, char else if (g_str_equal (name, "initramfs.img") || g_str_has_prefix (name, "initramfs-")) { if (ret_initramfs) - return glnx_throw (error, "Multiple initramfs- in %s", bootdir); + return glnx_throw (error, "Multiple initramfs- in %s, occurrences '%s' and '%s/%s'", + bootdir, ret_initramfs, bootdir, name); ret_initramfs = g_strconcat (bootdir, "/", name, NULL); } } diff --git a/src/libpriv/rpmostree-postprocess.cxx b/src/libpriv/rpmostree-postprocess.cxx index bfadaa73dd..9b38da931a 100644 --- a/src/libpriv/rpmostree-postprocess.cxx +++ b/src/libpriv/rpmostree-postprocess.cxx @@ -397,8 +397,8 @@ postprocess_final (int rootfs_dfd, rpmostreecxx::Treefile &treefile, gboolean un /* Temporary workaround for https://github.com/openshift/os/issues/1036. */ { - rust::Vec child_argv = { rust::String ("semodule"), rust::String ("-n"), - rust::String ("--rebuild-if-modules-changed") }; + rust::Vec child_argv + = { rust::String ("semodule"), rust::String ("-n"), rust::String ("--refresh") }; ROSCXX_TRY (bubblewrap_run_sync (rootfs_dfd, child_argv, false, (bool)unified_core_mode), error); } diff --git a/tests/common/libvm.sh b/tests/common/libvm.sh index 25f349e8bf..c5bf52008e 100644 --- a/tests/common/libvm.sh +++ b/tests/common/libvm.sh @@ -66,7 +66,7 @@ vm_kola_spawn() { exit 1 fi setpriv --pdeathsig SIGKILL -- \ - env MANTLE_SSH_DIR="$PWD/kola-ssh" kola spawn -p qemu-unpriv \ + env MANTLE_SSH_DIR="$PWD/kola-ssh" kola spawn -p qemu \ --qemu-image "$test_image" -v --idle \ --json-info-fd 4 --output-dir "$outputdir" & # hack; need cleaner API for async kola spawn diff --git a/tests/kolainst/destructive/container-image b/tests/kolainst/destructive/container-image index 4cb34d5f8a..fc812d975f 100755 --- a/tests/kolainst/destructive/container-image +++ b/tests/kolainst/destructive/container-image @@ -162,9 +162,7 @@ EOF if test "${touched_resolv_conf}" -eq 1; then rm -vf /etc/resolv.conf fi - derived=oci:$image_dir:derived - skopeo copy containers-storage:localhost/fcos-derived $derived - rpm-ostree rebase ostree-unverified-image:$derived + rpm-ostree rebase ostree-unverified-image:containers-storage:localhost/fcos-derived ostree container image list --repo=/ostree/repo | tee imglist.txt assert_streq "$(wc -l < imglist.txt)" 1 rm $image_dir -rf @@ -181,7 +179,7 @@ EOF assert_streq $(rpm -q baz) baz-2.0-1.${arch} test -f /usr/bin/baz ! rpm -q nano - rpmostree_assert_status ".deployments[0][\"container-image-reference\"] == \"ostree-unverified-image:oci:$image_dir:derived\"" + rpmostree_assert_status ".deployments[0][\"container-image-reference\"] == \"ostree-unverified-image:containers-storage:localhost/fcos-derived\"" # We'll test the "apply" automatic updates policy here systemctl stop rpm-ostreed @@ -190,9 +188,7 @@ EOF rpm-ostree reload # Now revert back to the base image, but keep our layered package foo - rm "${image_dir}" -rf - skopeo copy containers-storage:localhost/fcos ${image}:latest - rpm-ostree rebase ostree-unverified-image:${image}:latest + rpm-ostree rebase ostree-unverified-image:containers-storage:localhost/fcos /tmp/autopkgtest-reboot 4 ;; 4) @@ -204,7 +200,7 @@ EOF fatal "found $p" fi done - rpmostree_assert_status ".deployments[0][\"container-image-reference\"] == \"ostree-unverified-image:oci:$image_dir:latest\"" + rpmostree_assert_status ".deployments[0][\"container-image-reference\"] == \"ostree-unverified-image:containers-storage:localhost/fcos\"" ;; *) echo "unexpected mark: ${AUTOPKGTEST_REBOOT_MARK}"; exit 1;; esac diff --git a/tests/kolainst/nondestructive/misc.sh b/tests/kolainst/nondestructive/misc.sh index 6965b2f280..223bd45d61 100755 --- a/tests/kolainst/nondestructive/misc.sh +++ b/tests/kolainst/nondestructive/misc.sh @@ -101,6 +101,39 @@ rpmostree_busctl_call_os GetPackages as 1 should-not-exist-p-equals-np > out.txt assert_file_has_content_literal out.txt 'aa{sv} 0' echo "ok dbus GetPackages" +rpmostree_busctl_call_os Search as 1 testdaemon > out.txt +assert_file_has_content_literal out.txt '"epoch" t 0' +assert_file_has_content_literal out.txt '"reponame" s "libtest"' +assert_file_has_content_literal out.txt '"nevra" s "testdaemon' +rpmostree_busctl_call_os Search as 1 should-not-exist-p-equals-np > out.txt +assert_file_has_content_literal out.txt 'aa{sv} 0' +echo "ok dbus Search" + +rpm-ostree search testdaemon > out.txt +assert_file_has_content_literal out.txt '===== Name Matched =====' +assert_file_has_content_literal out.txt 'testdaemon : awesome-daemon-for-testing' +echo "ok Search name match" + +rpm-ostree search awesome-daemon > out.txt +assert_file_has_content_literal out.txt '===== Summary Matched =====' +assert_file_has_content_literal out.txt 'testdaemon : awesome-daemon-for-testing' +echo "ok Search summary match" + +rpm-ostree search testdaemon awesome-daemon > out.txt +assert_file_has_content_literal out.txt '===== Summary & Name Matched =====' +assert_file_has_content_literal out.txt 'testdaemon : awesome-daemon-for-testing' +echo "ok Search name and summary match" + +rpm-ostree search "test*" > out.txt +assert_file_has_content_literal out.txt '===== Summary & Name Matched =====' +assert_file_has_content_literal out.txt '===== Name Matched =====' +assert_file_has_content_literal out.txt '===== Summary Matched =====' +assert_file_has_content_literal out.txt 'testdaemon : awesome-daemon-for-testing' +assert_file_has_content_literal out.txt 'testpkg-etc : testpkg-etc' +assert_file_has_content_literal out.txt 'testpkg-post-infinite-loop : testpkg-post-infinite-loop' +assert_file_has_content_literal out.txt 'testpkg-touch-run : testpkg-touch-run' +echo "ok Search glob pattern match" + # Verify operations as non-root runuser -u core rpm-ostree status echo "ok status doesn't require root" diff --git a/tests/vm.sh b/tests/vm.sh index a54f85d8a4..996f67304f 100755 --- a/tests/vm.sh +++ b/tests/vm.sh @@ -39,7 +39,7 @@ spawn_vm() { exec 4> .kolavm/info.json env MANTLE_SSH_DIR="$PWD/.kolavm/ssh" \ - kola spawn -k -p qemu-unpriv \ + kola spawn -k -p qemu \ --qemu-image "$image" -v --idle \ --json-info-fd 4 --output-dir "$PWD/.kolavm/output" &