-
Notifications
You must be signed in to change notification settings - Fork 33
/
cli_status.rs
260 lines (227 loc) · 8.69 KB
/
cli_status.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
//! Interface to `rpm-ostree status --json`.
use super::actor::{RpmOstreeClient, StatusCache};
use super::Release;
use anyhow::{anyhow, ensure, Context, Result};
use filetime::FileTime;
use log::trace;
use prometheus::IntCounter;
use serde::Deserialize;
use std::collections::BTreeSet;
use std::fs;
use std::rc::Rc;
/// Path to local OSTree deployments. We use its mtime to check for modifications (e.g. new deployments)
/// to local deployments that might warrant querying `rpm-ostree status` again to update our knowledge
/// of the current state of deployments.
const OSTREE_DEPLS_PATH: &str = "/ostree/deploy";
lazy_static::lazy_static! {
static ref STATUS_CACHE_ATTEMPTS: IntCounter = register_int_counter!(opts!(
"zincati_rpm_ostree_status_cache_requests_total",
"Total number of attempts to query rpm-ostree actor's cached status."
)).unwrap();
static ref STATUS_CACHE_MISSES: IntCounter = register_int_counter!(opts!(
"zincati_rpm_ostree_status_cache_misses_total",
"Total number of times rpm-ostree actor's cached status is stale during queries."
)).unwrap();
// This is not equivalent to `zincati_rpm_ostree_status_cache_misses_total` as there
// are cases where `rpm-ostree status` is called directly without checking the cache.
static ref RPM_OSTREE_STATUS_ATTEMPTS: IntCounter = register_int_counter!(opts!(
"zincati_rpm_ostree_status_attempts_total",
"Total number of 'rpm-ostree status' attempts."
)).unwrap();
static ref RPM_OSTREE_STATUS_FAILURES: IntCounter = register_int_counter!(opts!(
"zincati_rpm_ostree_status_failures_total",
"Total number of 'rpm-ostree status' failures."
)).unwrap();
}
/// JSON output from `rpm-ostree status --json`
#[derive(Clone, Debug, Deserialize)]
pub struct StatusJson {
deployments: Vec<DeploymentJson>,
}
/// Partial deployment object (only fields relevant to zincati).
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct DeploymentJson {
booted: bool,
base_checksum: Option<String>,
#[serde(rename = "base-commit-meta")]
base_metadata: BaseCommitMetaJson,
checksum: String,
// NOTE(lucab): missing field means "not staged".
#[serde(default)]
staged: bool,
version: String,
}
/// Metadata from base commit (only fields relevant to zincati).
#[derive(Clone, Debug, Deserialize)]
struct BaseCommitMetaJson {
#[serde(rename = "coreos-assembler.basearch")]
basearch: String,
#[serde(rename = "fedora-coreos.stream")]
stream: String,
}
impl DeploymentJson {
/// Convert into `Release`.
pub fn into_release(self) -> Release {
Release {
checksum: self.base_revision(),
version: self.version,
age_index: None,
}
}
/// Return the deployment base revision.
pub fn base_revision(&self) -> String {
self.base_checksum
.clone()
.unwrap_or_else(|| self.checksum.clone())
}
}
/// Parse base architecture for booted deployment from status object.
pub fn parse_basearch(status: &StatusJson) -> Result<String> {
let json = booted_json(status)?;
Ok(json.base_metadata.basearch)
}
/// Parse the booted deployment from status object.
pub fn parse_booted(status: &StatusJson) -> Result<Release> {
let json = booted_json(status)?;
Ok(json.into_release())
}
/// Parse updates stream for booted deployment from status object.
pub fn parse_booted_updates_stream(status: &StatusJson) -> Result<String> {
let json = booted_json(status)?;
ensure!(!json.base_metadata.stream.is_empty(), "empty stream value");
Ok(json.base_metadata.stream)
}
/// Parse pending deployment from status object.
pub fn parse_pending_deployment(status: &StatusJson) -> Result<Option<(Release, String)>> {
// There can be at most one staged/pending rpm-ostree deployment,
// thus we only consider the first matching entry (if any).
let staged = status.deployments.iter().find(|d| d.staged).cloned();
match staged {
None => Ok(None),
Some(json) => {
let stream = json.base_metadata.stream.clone();
ensure!(!stream.is_empty(), "empty stream value");
let release = json.into_release();
Ok(Some((release, stream)))
}
}
}
/// Parse local deployments from a status object.
fn parse_local_deployments(status: &StatusJson, omit_staged: bool) -> BTreeSet<Release> {
let mut deployments = BTreeSet::<Release>::new();
for entry in &status.deployments {
if omit_staged && entry.staged {
continue;
}
let release = entry.clone().into_release();
deployments.insert(release);
}
deployments
}
/// Return local deployments, using client's cache if possible.
pub fn local_deployments(
client: &mut RpmOstreeClient,
omit_staged: bool,
) -> Result<BTreeSet<Release>> {
let status = status_json(client)?;
let local_depls = parse_local_deployments(&status, omit_staged);
Ok(local_depls)
}
/// Return JSON object for booted deployment.
fn booted_json(status: &StatusJson) -> Result<DeploymentJson> {
let booted = status
.clone()
.deployments
.into_iter()
.find(|d| d.booted)
.ok_or_else(|| anyhow!("no booted deployment found"))?;
ensure!(!booted.base_revision().is_empty(), "empty base revision");
ensure!(!booted.version.is_empty(), "empty version");
ensure!(!booted.base_metadata.basearch.is_empty(), "empty basearch");
Ok(booted)
}
/// Ensure our status cache is up to date; if empty or out of date, run `rpm-ostree status` to populate it.
fn status_json(client: &mut RpmOstreeClient) -> Result<Rc<StatusJson>> {
STATUS_CACHE_ATTEMPTS.inc();
let ostree_depls_data = fs::metadata(OSTREE_DEPLS_PATH)
.with_context(|| format!("failed to query directory {}", OSTREE_DEPLS_PATH))?;
let ostree_depls_data_mtime = FileTime::from_last_modification_time(&ostree_depls_data);
if let Some(cache) = &client.status_cache {
if cache.mtime == ostree_depls_data_mtime {
trace!("status cache is up to date");
return Ok(cache.status.clone());
}
}
STATUS_CACHE_MISSES.inc();
trace!("cache stale, invoking rpm-ostree to retrieve local deployments");
let status = Rc::new(invoke_cli_status(false)?);
client.status_cache = Some(StatusCache {
status: Rc::clone(&status),
mtime: ostree_depls_data_mtime,
});
Ok(status)
}
/// CLI executor for `rpm-ostree status --json`.
pub fn invoke_cli_status(booted_only: bool) -> Result<StatusJson> {
RPM_OSTREE_STATUS_ATTEMPTS.inc();
let mut cmd = std::process::Command::new("rpm-ostree");
cmd.arg("status").env("RPMOSTREE_CLIENT_ID", "zincati");
// Try to request the minimum scope we need.
if booted_only {
cmd.arg("--booted");
}
let cmdrun = cmd
.arg("--json")
.output()
.context("failed to run 'rpm-ostree' binary")?;
if !cmdrun.status.success() {
RPM_OSTREE_STATUS_FAILURES.inc();
anyhow::bail!(
"rpm-ostree status failed:\n{}",
String::from_utf8_lossy(&cmdrun.stderr)
);
}
let status: StatusJson = serde_json::from_slice(&cmdrun.stdout)?;
Ok(status)
}
#[cfg(test)]
mod tests {
use super::*;
fn mock_status(path: &str) -> Result<StatusJson> {
let fp = std::fs::File::open(path).unwrap();
let bufrd = std::io::BufReader::new(fp);
let status: StatusJson = serde_json::from_reader(bufrd)?;
Ok(status)
}
#[test]
fn mock_deployments() {
{
let status = mock_status("tests/fixtures/rpm-ostree-status.json").unwrap();
let deployments = parse_local_deployments(&status, false);
assert_eq!(deployments.len(), 1);
}
{
let status = mock_status("tests/fixtures/rpm-ostree-staged.json").unwrap();
let deployments = parse_local_deployments(&status, false);
assert_eq!(deployments.len(), 2);
}
{
let status = mock_status("tests/fixtures/rpm-ostree-staged.json").unwrap();
let deployments = parse_local_deployments(&status, true);
assert_eq!(deployments.len(), 1);
}
}
#[test]
fn mock_booted_basearch() {
let status = mock_status("tests/fixtures/rpm-ostree-status.json").unwrap();
let booted = booted_json(&status).unwrap();
assert_eq!(booted.base_metadata.basearch, "x86_64");
}
#[test]
fn mock_booted_updates_stream() {
let status = mock_status("tests/fixtures/rpm-ostree-status.json").unwrap();
let booted = booted_json(&status).unwrap();
assert_eq!(booted.base_metadata.stream, "testing-devel");
}
}