From e6c045cadabea68946e5f42acdba302dcea60f39 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Fri, 12 Feb 2021 18:47:36 +0000 Subject: [PATCH] Add an rpmostree-client sub-crate This is intended to be published to https://crates.io/crates/rpmostree-client Part of https://github.com/coreos/rpm-ostree/issues/2389 This directly imports the code from https://github.com/ostreedev/ostree/blob/5551c54c6e6eba8145b95bd3b28223f1941a9e8d/tests/inst/src/rpmostree.rs Once merged and released I'll try converting the ostree test suite over as well as Zincati. Internally add a testutils helper to validate it works. --- Cargo.lock | 11 + Cargo.toml | 1 + rust/rpmostree-client/Cargo.toml | 16 + rust/rpmostree-client/src/lib.rs | 64 +++ .../tests/fixtures/workstation-status.json | 417 ++++++++++++++++++ rust/rpmostree-client/tests/parse.rs | 10 + rust/src/testutils.rs | 16 + tests/kolainst/nondestructive/misc.sh | 3 +- 8 files changed, 537 insertions(+), 1 deletion(-) create mode 100644 rust/rpmostree-client/Cargo.toml create mode 100644 rust/rpmostree-client/src/lib.rs create mode 100644 rust/rpmostree-client/tests/fixtures/workstation-status.json create mode 100644 rust/rpmostree-client/tests/parse.rs diff --git a/Cargo.lock b/Cargo.lock index 37aa4c7705..2acdbe1f51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1197,6 +1197,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "rpmostree-client" +version = "0.1.0" +dependencies = [ + "anyhow", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "rpmostree-rust" version = "0.1.0" @@ -1228,6 +1238,7 @@ dependencies = [ "phf", "rand 0.8.3", "rayon", + "rpmostree-client", "rust-ini", "serde", "serde_derive", diff --git a/Cargo.toml b/Cargo.toml index e1f992b771..a46b80239f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ libdnf-sys = { path = "rust/libdnf-sys", version = "0.1.0" } memfd = "0.3.0" rust-ini = "0.16.1" os-release = "0.1.0" +rpmostree-client = { path = "rust/rpmostree-client", version = "0.1.0" } [build-dependencies] cbindgen = "0.16.0" diff --git a/rust/rpmostree-client/Cargo.toml b/rust/rpmostree-client/Cargo.toml new file mode 100644 index 0000000000..77a8ec2bee --- /dev/null +++ b/rust/rpmostree-client/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "rpmostree-client" +description = "Client side bindings for rpm-ostree" +version = "0.1.0" +edition = "2018" +license = "Apache-2.0" +keywords = ["ostree", "rpm-ostree"] +documentation = "http://docs.rs/rpmostree-client" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.38" +serde = { version = "1.0.123", features = ["derive"] } +serde_derive = "1.0.118" +serde_json = "1.0.62" diff --git a/rust/rpmostree-client/src/lib.rs b/rust/rpmostree-client/src/lib.rs new file mode 100644 index 0000000000..fdf678b76b --- /dev/null +++ b/rust/rpmostree-client/src/lib.rs @@ -0,0 +1,64 @@ +//! APIs for interacting with rpm-ostree client side. + +use anyhow::Context; +use serde_derive::Deserialize; +use std::process::Command; + +/// Our generic catchall fatal error, expected to be converted +/// to a string to output to a terminal or logs. +type Result = std::result::Result>; + +/// Representation of the rpm-ostree client-side state; this +/// can be parsed directly from the output of `rpm-ostree status --json`. +/// Currently not all fields are here, but that is a bug. +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Status { + pub deployments: Vec, +} + +/// A single deployment, i.e. a bootable ostree commit +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Deployment { + pub unlocked: Option, + pub osname: String, + pub pinned: bool, + pub checksum: String, + pub staged: Option, + pub booted: bool, + pub serial: u32, + pub origin: String, +} + +/// Gather a snapshot of the system status. +pub fn query_status() -> Result { + // Retry on temporary activation failures, see + // https://github.com/coreos/rpm-ostree/issues/2531 + let pause = std::time::Duration::from_secs(1); + let max_retries = 10; + let mut retries = 0; + let cmd_res = loop { + retries += 1; + let res = Command::new("rpm-ostree") + .args(&["status", "--json"]) + .output() + .context("failed to spawn 'rpm-ostree status'")?; + + if res.status.success() || retries >= max_retries { + break res; + } + std::thread::sleep(pause); + }; + + if !cmd_res.status.success() { + return Err(format!( + "running 'rpm-ostree status' failed: {}", + String::from_utf8_lossy(&cmd_res.stderr) + ) + .into()); + } + + Ok(serde_json::from_slice(&cmd_res.stdout) + .context("failed to parse 'rpm-ostree status' output")?) +} diff --git a/rust/rpmostree-client/tests/fixtures/workstation-status.json b/rust/rpmostree-client/tests/fixtures/workstation-status.json new file mode 100644 index 0000000000..ed6e68ad7a --- /dev/null +++ b/rust/rpmostree-client/tests/fixtures/workstation-status.json @@ -0,0 +1,417 @@ +{ + "deployments": [ + { + "unlocked": "none", + "base-commit-meta": { + "coreos-assembler.config-gitrev": "80966f951c766846da070b4c168b9170c61513e2", + "coreos-assembler.config-dirty": "false", + "rpmostree.inputhash": "06539cc4a4265eec2045a349fe80de451a61628c1b117e171d80663d3e3f42eb", + "coreos-assembler.basearch": "x86_64", + "version": "33.21", + "rpmostree.initramfs-args": [ + "--add=ignition", + "--no-hostonly", + "--omit=nfs", + "--omit=lvm", + "--omit=iscsi" + ], + "rpmostree.rpmmd-repos": [ + { + "id": "fedora-coreos-pool", + "timestamp": 1053029086517002240 + }, + { + "id": "fedora", + "timestamp": -2945197617627267072 + }, + { + "id": "fedora-updates", + "timestamp": -389530169125109760 + } + ] + }, + "requested-local-packages": [], + "base-removals": [], + "gpg-enabled": false, + "origin": "fedora/33/x86_64/silverblue", + "osname": "fedora-silverblue", + "pinned": false, + "requested-base-local-replacements": [ + "rpm-ostree-2021.1-2.fc33.x86_64", + "rpm-ostree-libs-2021.1-2.fc33.x86_64" + ], + "checksum": "63335a77f9853618ba1a5f139c5805e82176a2a040ef5e34d7402e12263af5bb", + "regenerate-initramfs": false, + "id": "fedora-silverblue-63335a77f9853618ba1a5f139c5805e82176a2a040ef5e34d7402e12263af5bb.0", + "version": "33.21", + "base-version": "33.21", + "requested-base-removals": [], + "base-checksum": "229387d3c0bb8ad698228ca5702eca72aed8b298a7c800be1dc72bab160a9f7f", + "requested-packages": [ + "xsel", + "gdb", + "ykclient", + "krb5-workstation", + "ykpers", + "git-evtag", + "fish", + "qemu-system-aarch64", + "strace", + "qemu-kvm", + "virt-manager", + "opensc", + "tmux", + "pcsc-lite-ccid", + "tilix", + "libvirt" + ], + "base-timestamp": 1612554510, + "serial": 0, + "layered-commit-meta": { + "rpmostree.clientlayer": true, + "rpmostree.removed-base-packages": [], + "version": "33.21", + "rpmostree.packages": [ + "fish", + "gdb", + "git-evtag", + "krb5-workstation", + "libvirt", + "opensc", + "pcsc-lite-ccid", + "qemu-kvm", + "qemu-system-aarch64", + "strace", + "tilix", + "tmux", + "virt-manager", + "xsel", + "ykclient", + "ykpers" + ], + "rpmostree.clientlayer_version": 4, + "rpmostree.replaced-base-packages": [ + [ + [ + "rpm-ostree-2021.1-2.fc33.x86_64", + "rpm-ostree", + 0, + "2021.1", + "2.fc33", + "x86_64" + ], + [ + "rpm-ostree-2021.1-3.fc33.x86_64", + "rpm-ostree", + 0, + "2021.1", + "3.fc33", + "x86_64" + ] + ], + [ + [ + "rpm-ostree-libs-2021.1-2.fc33.x86_64", + "rpm-ostree-libs", + 0, + "2021.1", + "2.fc33", + "x86_64" + ], + [ + "rpm-ostree-libs-2021.1-3.fc33.x86_64", + "rpm-ostree-libs", + 0, + "2021.1", + "3.fc33", + "x86_64" + ] + ] + ], + "rpmostree.state-sha512": "8b037fba282e3773ef17d4c396ee958765c01e85c7a6a29ec9df1bb2213022cf599da15ec4df982c4f0904012b165c4370a9f14b12c48d0684a66928c4f34b34", + "rpmostree.rpmmd-repos": [ + { + "id": "fedora-cisco-openh264", + "timestamp": 1598382634 + }, + { + "id": "updates", + "timestamp": 1612486906 + }, + { + "id": "fedora", + "timestamp": 1603150039 + } + ] + }, + "base-local-replacements": [ + [ + [ + "rpm-ostree-2021.1-2.fc33.x86_64", + "rpm-ostree", + 0, + "2021.1", + "2.fc33", + "x86_64" + ], + [ + "rpm-ostree-2021.1-3.fc33.x86_64", + "rpm-ostree", + 0, + "2021.1", + "3.fc33", + "x86_64" + ] + ], + [ + [ + "rpm-ostree-libs-2021.1-2.fc33.x86_64", + "rpm-ostree-libs", + 0, + "2021.1", + "2.fc33", + "x86_64" + ], + [ + "rpm-ostree-libs-2021.1-3.fc33.x86_64", + "rpm-ostree-libs", + 0, + "2021.1", + "3.fc33", + "x86_64" + ] + ] + ], + "timestamp": 1612555369, + "packages": [ + "fish", + "gdb", + "git-evtag", + "krb5-workstation", + "libvirt", + "opensc", + "pcsc-lite-ccid", + "qemu-kvm", + "qemu-system-aarch64", + "strace", + "tilix", + "tmux", + "virt-manager", + "xsel", + "ykclient", + "ykpers" + ], + "booted": true, + "initramfs-etc": [] + }, + { + "unlocked": "none", + "pending-base-version": "33.21", + "base-commit-meta": { + "coreos-assembler.config-gitrev": "bbd5282b507c5b29e3a5f12e9da21f3aaa0f0e00", + "coreos-assembler.config-dirty": "false", + "rpmostree.inputhash": "f33469d0f6c5d5ce5e30345fa5b002a8e4ebf5ea397caad000bdc32cd74897a6", + "coreos-assembler.basearch": "x86_64", + "version": "33.17", + "rpmostree.initramfs-args": [ + "--add=ignition", + "--no-hostonly", + "--omit=nfs", + "--omit=lvm", + "--omit=iscsi" + ], + "rpmostree.rpmmd-repos": [ + { + "id": "fedora-coreos-pool", + "timestamp": 7926905303512121344 + }, + { + "id": "fedora", + "timestamp": -2945197617627267072 + }, + { + "id": "fedora-updates", + "timestamp": -6611277243593261056 + } + ] + }, + "requested-local-packages": [], + "base-removals": [], + "gpg-enabled": false, + "osname": "fedora-silverblue", + "origin": "fedora/33/x86_64/silverblue", + "packages": [ + "fish", + "gdb", + "git-evtag", + "krb5-workstation", + "libvirt", + "opensc", + "pcsc-lite-ccid", + "qemu-kvm", + "qemu-system-aarch64", + "strace", + "tilix", + "tmux", + "virt-manager", + "xsel", + "ykclient", + "ykpers" + ], + "pinned": false, + "requested-base-local-replacements": [ + "rpm-ostree-2021.1-2.fc33.x86_64", + "rpm-ostree-libs-2021.1-2.fc33.x86_64" + ], + "checksum": "775d54e89bc74731ec27db04f12510c0269c8cbab3ad5e39e0a4d693231ef072", + "regenerate-initramfs": false, + "id": "fedora-silverblue-775d54e89bc74731ec27db04f12510c0269c8cbab3ad5e39e0a4d693231ef072.0", + "version": "33.17", + "base-version": "33.17", + "base-checksum": "deea0555cb7d3eb042df9a85d4efcbb9f70d778a9a9557715c0e398978233cd7", + "requested-base-removals": [], + "requested-packages": [ + "xsel", + "gdb", + "ykclient", + "krb5-workstation", + "ykpers", + "git-evtag", + "fish", + "qemu-system-aarch64", + "strace", + "qemu-kvm", + "virt-manager", + "opensc", + "tmux", + "pcsc-lite-ccid", + "tilix", + "libvirt" + ], + "base-timestamp": 1611079148, + "serial": 0, + "layered-commit-meta": { + "rpmostree.clientlayer": true, + "rpmostree.removed-base-packages": [], + "version": "33.17", + "rpmostree.packages": [ + "fish", + "gdb", + "git-evtag", + "krb5-workstation", + "libvirt", + "opensc", + "pcsc-lite-ccid", + "qemu-kvm", + "qemu-system-aarch64", + "strace", + "tilix", + "tmux", + "virt-manager", + "xsel", + "ykclient", + "ykpers" + ], + "rpmostree.clientlayer_version": 4, + "rpmostree.replaced-base-packages": [ + [ + [ + "rpm-ostree-2021.1-2.fc33.x86_64", + "rpm-ostree", + 0, + "2021.1", + "2.fc33", + "x86_64" + ], + [ + "rpm-ostree-2020.10-1.fc33.x86_64", + "rpm-ostree", + 0, + "2020.10", + "1.fc33", + "x86_64" + ] + ], + [ + [ + "rpm-ostree-libs-2021.1-2.fc33.x86_64", + "rpm-ostree-libs", + 0, + "2021.1", + "2.fc33", + "x86_64" + ], + [ + "rpm-ostree-libs-2020.10-1.fc33.x86_64", + "rpm-ostree-libs", + 0, + "2020.10", + "1.fc33", + "x86_64" + ] + ] + ], + "rpmostree.state-sha512": "684f72c2b63379ee17a8f3055ccdfb3d54d255ed5bf1965788be21e804a0aff9e08620519dacaa34cc8cbad038474e8b0abbc68ee98988c547ad599f93ddcfa1", + "rpmostree.rpmmd-repos": [ + { + "id": "fedora-cisco-openh264", + "timestamp": 1598382634 + }, + { + "id": "updates", + "timestamp": 1611022500 + }, + { + "id": "fedora", + "timestamp": 1603150039 + } + ] + }, + "base-local-replacements": [ + [ + [ + "rpm-ostree-2021.1-2.fc33.x86_64", + "rpm-ostree", + 0, + "2021.1", + "2.fc33", + "x86_64" + ], + [ + "rpm-ostree-2020.10-1.fc33.x86_64", + "rpm-ostree", + 0, + "2020.10", + "1.fc33", + "x86_64" + ] + ], + [ + [ + "rpm-ostree-libs-2021.1-2.fc33.x86_64", + "rpm-ostree-libs", + 0, + "2021.1", + "2.fc33", + "x86_64" + ], + [ + "rpm-ostree-libs-2020.10-1.fc33.x86_64", + "rpm-ostree-libs", + 0, + "2020.10", + "1.fc33", + "x86_64" + ] + ] + ], + "timestamp": 1611081986, + "pending-base-timestamp": 1612554510, + "booted": false, + "pending-base-checksum": "229387d3c0bb8ad698228ca5702eca72aed8b298a7c800be1dc72bab160a9f7f", + "initramfs-etc": [] + } + ], + "transaction": null, + "cached-update": null +} diff --git a/rust/rpmostree-client/tests/parse.rs b/rust/rpmostree-client/tests/parse.rs new file mode 100644 index 0000000000..d6c70ffa18 --- /dev/null +++ b/rust/rpmostree-client/tests/parse.rs @@ -0,0 +1,10 @@ +use anyhow::Result; +use rpmostree_client; + +#[test] +fn parse_workstation() -> Result<()> { + let data = include_str!("fixtures/workstation-status.json"); + let state: rpmostree_client::Status = serde_json::from_str(data)?; + assert_eq!(state.deployments.len(), 2); + Ok(()) +} diff --git a/rust/src/testutils.rs b/rust/src/testutils.rs index 80f8ccd0f7..2ad0eedc4a 100644 --- a/rust/src/testutils.rs +++ b/rust/src/testutils.rs @@ -45,6 +45,8 @@ struct SyntheticUpgradeOpts { enum Opt { /// Generate an OS update by changing ELF files GenerateSyntheticUpgrade(SyntheticUpgradeOpts), + /// Validate that we can parse the output of `rpm-ostree status --json`. + ValidateParseStatus, } /// Returns `true` if a file is ELF; see https://en.wikipedia.org/wiki/Executable_and_Linkable_Format @@ -211,10 +213,24 @@ fn update_os_tree(opts: &SyntheticUpgradeOpts) -> Result<()> { Ok(()) } +// We always expect to be in a booted deployment. The real goal here +// is to ensure that everything output from status --json in our +// test suite can be parsed by our client side bindings. +// +// In the future we'll switch the client API to have like +// query_status_deny_unknown_fields() which will force us +// to update the client bindings when adding new fields. +fn validate_parse_status() -> Result<()> { + let s = rpmostree_client::query_status().map_err(anyhow::Error::msg)?; + assert_ne!(s.deployments.len(), 0); + Ok(()) +} + pub(crate) fn testutils_entrypoint(args: Vec) -> CxxResult<()> { let opt = Opt::from_iter(args.iter()); match opt { Opt::GenerateSyntheticUpgrade(ref opts) => update_os_tree(opts)?, + Opt::ValidateParseStatus => validate_parse_status()?, }; Ok(()) } diff --git a/tests/kolainst/nondestructive/misc.sh b/tests/kolainst/nondestructive/misc.sh index 4704efd91e..f70f6ad0d5 100755 --- a/tests/kolainst/nondestructive/misc.sh +++ b/tests/kolainst/nondestructive/misc.sh @@ -1,5 +1,5 @@ #!/bin/bash -set -euo pipefail +set -xeuo pipefail . ${KOLA_EXT_DATA}/libtest.sh cd $(mktemp -d) @@ -15,6 +15,7 @@ assert_jq status.json \ '.deployments[0]["requested-base-removals"]' \ '.deployments[0]["layered-commit-meta"]|not' rm status.json +rpm-ostree testutils validate-parse-status echo "ok empty pkg arrays, and commit meta correct in status json" # Ensure we return an error when passing a wrong option.