Skip to content

Commit 5236268

Browse files
cgwaltersckyrouac
authored andcommitted
ostree-ext: Add sysroot listing + creation helpers
Signed-off-by: Colin Walters <[email protected]>
1 parent b010983 commit 5236268

File tree

2 files changed

+237
-4
lines changed

2 files changed

+237
-4
lines changed

crates/ostree-ext/src/container/deploy.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
//! Perform initial setup for a container image based system root
22
33
use std::collections::HashSet;
4-
#[cfg(feature = "bootc")]
5-
use std::os::fd::BorrowedFd;
64

75
use anyhow::Result;
86
use fn_error_context::context;
@@ -161,7 +159,7 @@ pub async fn deploy(
161159
use cap_std_ext::cmdext::CapStdExtCommandExt;
162160
use ocidir::cap_std::fs::Dir;
163161

164-
let sysroot_dir = &Dir::reopen_dir(&sysroot_fd(sysroot))?;
162+
let sysroot_dir = &Dir::reopen_dir(&crate::sysroot::sysroot_fd(sysroot))?;
165163

166164
// Note that the sysroot is provided as `.` but we use cwd_dir to
167165
// make the process current working directory the sysroot.

crates/ostree-ext/src/sysroot.rs

Lines changed: 236 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
//! Helpers for interacting with sysroots.
22
3-
use std::ops::Deref;
3+
use std::{ops::Deref, os::fd::BorrowedFd, time::SystemTime};
44

55
use anyhow::Result;
6+
use chrono::Datelike as _;
7+
use ocidir::cap_std::fs_utf8::Dir;
8+
use ostree::gio;
9+
10+
/// We may automatically allocate stateroots, this string is the prefix.
11+
const AUTO_STATEROOT_PREFIX: &str = "state-";
612

713
use crate::utils::async_task_with_spinner;
814

@@ -32,6 +38,117 @@ impl Deref for SysrootLock {
3238
}
3339
}
3440

41+
42+
/// Access the file descriptor for a sysroot
43+
#[allow(unsafe_code)]
44+
pub fn sysroot_fd(sysroot: &ostree::Sysroot) -> BorrowedFd {
45+
unsafe { BorrowedFd::borrow_raw(sysroot.fd()) }
46+
}
47+
48+
/// A stateroot can match our auto "state-" prefix, or be manual.
49+
#[derive(Debug, PartialEq, Eq)]
50+
pub enum StaterootKind {
51+
/// This stateroot has an automatic name
52+
Auto((u64, u64)),
53+
/// This stateroot is manually named
54+
Manual,
55+
}
56+
57+
/// Metadata about a stateroot.
58+
#[derive(Debug, PartialEq, Eq)]
59+
pub struct Stateroot {
60+
/// The name
61+
pub name: String,
62+
/// Kind
63+
pub kind: StaterootKind,
64+
/// Creation timestamp (from the filesystem)
65+
pub creation: SystemTime,
66+
}
67+
68+
impl StaterootKind {
69+
fn new(name: &str) -> Self {
70+
if let Some(v) = parse_auto_stateroot_name(name) {
71+
return Self::Auto(v);
72+
}
73+
Self::Manual
74+
}
75+
}
76+
77+
/// Load metadata for a stateroot
78+
fn read_stateroot(sysroot_dir: &Dir, name: &str) -> Result<Stateroot> {
79+
let path = format!("ostree/deploy/{name}");
80+
let kind = StaterootKind::new(&name);
81+
let creation = sysroot_dir.symlink_metadata(&path)?.created()?.into_std();
82+
let r = Stateroot {
83+
name: name.to_owned(),
84+
kind,
85+
creation,
86+
};
87+
Ok(r)
88+
}
89+
90+
/// Enumerate stateroots, which are basically the default place for `/var`.
91+
pub fn list_stateroots(sysroot: &ostree::Sysroot) -> Result<Vec<Stateroot>> {
92+
let sysroot_dir = &Dir::reopen_dir(&sysroot_fd(sysroot))?;
93+
let r = sysroot_dir
94+
.read_dir("ostree/deploy")?
95+
.try_fold(Vec::new(), |mut acc, v| {
96+
let v = v?;
97+
let name = v.file_name()?;
98+
if sysroot_dir.try_exists(format!("ostree/deploy/{name}/deploy"))? {
99+
acc.push(read_stateroot(sysroot_dir, &name)?);
100+
}
101+
anyhow::Ok(acc)
102+
})?;
103+
Ok(r)
104+
}
105+
106+
/// Given a string, if it matches the form of an automatic state root, parse it into its <year>.<serial> pair.
107+
fn parse_auto_stateroot_name(name: &str) -> Option<(u64, u64)> {
108+
let Some(statename) = name.strip_prefix(AUTO_STATEROOT_PREFIX) else {
109+
return None;
110+
};
111+
let Some((year, serial)) = statename.split_once("-") else {
112+
return None;
113+
};
114+
let Ok(year) = year.parse::<u64>() else {
115+
return None;
116+
};
117+
let Ok(serial) = serial.parse::<u64>() else {
118+
return None;
119+
};
120+
Some((year, serial))
121+
}
122+
123+
/// Given a set of stateroots, allocate a new one
124+
pub fn allocate_new_stateroot(
125+
sysroot: &ostree::Sysroot,
126+
stateroots: &[Stateroot],
127+
now: chrono::DateTime<chrono::Utc>,
128+
) -> Result<Stateroot> {
129+
let sysroot_dir = &Dir::reopen_dir(&sysroot_fd(sysroot))?;
130+
131+
let current_year = now.year().try_into().unwrap_or_default();
132+
let (year, serial) = stateroots
133+
.iter()
134+
.filter_map(|v| {
135+
if let StaterootKind::Auto(v) = v.kind {
136+
Some(v)
137+
} else {
138+
None
139+
}
140+
})
141+
.max()
142+
.map(|(year, serial)| (year, serial + 1))
143+
.unwrap_or((current_year, 0));
144+
145+
let name = format!("state-{year}-{serial}");
146+
147+
sysroot.init_osname(&name, gio::Cancellable::NONE)?;
148+
149+
read_stateroot(sysroot_dir, &name)
150+
}
151+
35152
impl SysrootLock {
36153
/// Asynchronously acquire a sysroot lock. If the lock cannot be acquired
37154
/// immediately, a status message will be printed to standard output.
@@ -55,3 +172,121 @@ impl SysrootLock {
55172
}
56173
}
57174
}
175+
176+
#[cfg(test)]
177+
mod tests {
178+
use super::*;
179+
180+
#[test]
181+
fn test_parse_auto_stateroot_name_valid() {
182+
let test_cases = [
183+
// Basic valid cases
184+
("state-2024-0", Some((2024, 0))),
185+
("state-2024-1", Some((2024, 1))),
186+
("state-2023-123", Some((2023, 123))),
187+
// Large numbers
188+
(
189+
"state-18446744073709551615-18446744073709551615",
190+
Some((18446744073709551615, 18446744073709551615)),
191+
),
192+
// Zero values
193+
("state-0-0", Some((0, 0))),
194+
("state-0-123", Some((0, 123))),
195+
// Leading zeros (should work - u64::parse handles them)
196+
("state-0002024-001", Some((2024, 1))),
197+
("state-000-000", Some((0, 0))),
198+
];
199+
200+
for (input, expected) in test_cases {
201+
assert_eq!(
202+
parse_auto_stateroot_name(input),
203+
expected,
204+
"Failed for input: {}",
205+
input
206+
);
207+
}
208+
}
209+
210+
#[test]
211+
fn test_parse_auto_stateroot_name_invalid() {
212+
let test_cases = [
213+
// Missing prefix
214+
"2024-1",
215+
// Wrong prefix
216+
"stat-2024-1",
217+
"states-2024-1",
218+
"prefix-2024-1",
219+
// Empty string
220+
"",
221+
// Only prefix
222+
"state-",
223+
// Missing separator
224+
"state-20241",
225+
// Wrong separator
226+
"state-2024.1",
227+
"state-2024_1",
228+
"state-2024:1",
229+
// Multiple separators
230+
"state-2024-1-2",
231+
// Missing year or serial
232+
"state--1",
233+
"state-2024-",
234+
// Non-numeric year
235+
"state-abc-1",
236+
"state-2024a-1",
237+
// Non-numeric serial
238+
"state-2024-abc",
239+
"state-2024-1a",
240+
// Both non-numeric
241+
"state-abc-def",
242+
// Negative numbers (handled by parse::<u64>() failure)
243+
"state--2024-1",
244+
"state-2024--1",
245+
// Floating point numbers
246+
"state-2024.5-1",
247+
"state-2024-1.5",
248+
// Numbers with whitespace
249+
"state- 2024-1",
250+
"state-2024- 1",
251+
"state-2024 -1",
252+
"state-2024- 1 ",
253+
// Case sensitivity (should fail - prefix is lowercase)
254+
"State-2024-1",
255+
"STATE-2024-1",
256+
// Unicode characters
257+
"state-2024-1🦀",
258+
"state-2024🦀-1",
259+
// Hex-like strings (should fail - not decimal)
260+
"state-0x2024-1",
261+
"state-2024-0x1",
262+
];
263+
264+
for input in test_cases {
265+
assert_eq!(
266+
parse_auto_stateroot_name(input),
267+
None,
268+
"Expected None for input: {}",
269+
input
270+
);
271+
}
272+
}
273+
274+
#[test]
275+
fn test_stateroot_kind_new() {
276+
let test_cases = [
277+
("state-2024-1", StaterootKind::Auto((2024, 1))),
278+
("manual-name", StaterootKind::Manual),
279+
("state-invalid", StaterootKind::Manual),
280+
("", StaterootKind::Manual),
281+
];
282+
283+
for (input, expected) in test_cases {
284+
assert_eq!(
285+
StaterootKind::new(input),
286+
expected,
287+
"Failed for input: {}",
288+
input
289+
);
290+
}
291+
}
292+
}

0 commit comments

Comments
 (0)