Skip to content

Commit 4be42b4

Browse files
authored
Wicket: Add update support code required for MUP (#1866)
For MUP, users will write a TOML file similar to `example-rack-update-spec.toml` that maps to a `RackUpdateSpec`. Users will then create a release update via running `mupdate-pkg`. This package will be sent via ssh to wicket which will unpack it via `Manifest::load` and display the contents to the user. Wicket will use uploaded update packages to update the rack with the help of MGS and other downstream services. Note that the format of an update manifest and package is homeade here. We may possibly re-use it with a TUF wrapper in the future, or discard it altogether post MVP.
1 parent bafa179 commit 4be42b4

7 files changed

+311
-5
lines changed

Cargo.lock

+13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

wicket/Cargo.toml

+23-5
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,36 @@
11
[package]
22
name = "wicket"
3+
description = "Technician port TUI"
34
version = "0.1.0"
45
edition = "2021"
6+
license = "MPL-2.0"
57

68
[dependencies]
9+
anyhow = "1.0.65"
10+
clap = {version = "4.0", features = ["derive"]}
711
crossterm = { version = "0.25.0", features = ["event-stream"] }
8-
tui = "0.19.0"
9-
tokio = { version = "1.21.1", features = ["full"] }
10-
anyhow = "1.0.66"
12+
futures = "0.3.25"
13+
hex = { version = "0.4", features = ["serde"] }
14+
semver = { version = "1.0.14", features = ["std", "serde"] }
15+
serde = { version = "1.0", features = [ "derive" ] }
16+
serde_json = "1.0"
17+
sha3 = "0.10.5"
1118
slog = { version = "2.7.0", features = [ "max_level_trace", "release_max_level_debug" ] }
12-
slog-term = "2.9.0"
1319
slog-async = "2.7.0"
14-
futures = "0.3.25"
20+
slog-term = "2.9.0"
21+
snafu = "0.7.2"
22+
tar = "0.4"
23+
tokio = { version = "1.21.1", features = ["full"] }
24+
toml = "0.5.9"
25+
tui = "0.19.0"
26+
27+
[dev-dependencies]
28+
tempfile = "3.3.0"
1529

1630
[[bin]]
1731
name = "wicket"
1832
doc = false
33+
34+
[[bin]]
35+
name = "mupdate-pkg"
36+
doc = false

wicket/example-rack-update-spec.toml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# This is a specification that is used to generate a rack update for
2+
# installation via wicket. The update is generated by the `mupdate-pkg` tool,
3+
# which generates a `manifest.json` and packages it up along with the files in
4+
# this spec into a tarball.
5+
6+
version = "1.0.0"
7+
8+
[[artifacts]]
9+
filename = "test-data/fake-update-img-1.0.0.tar.gz"
10+
artifact_type = "SledSp"
11+
version = "1.0.0"

wicket/src/bin/mupdate-pkg.rs

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
//! Utility for creating a rack release update as a tarfile.
6+
7+
use anyhow::Result;
8+
use clap::Parser;
9+
use std::path::PathBuf;
10+
use wicket::update::RackUpdateSpec;
11+
12+
#[derive(Debug, Parser)]
13+
struct Args {
14+
/// Path of input toml file that gets parsed into a [`RackUpdateSpec`]
15+
#[arg(short, long)]
16+
spec: PathBuf,
17+
18+
/// Path of the release archive that gets created
19+
#[arg(short, long)]
20+
output_dir: PathBuf,
21+
}
22+
23+
fn main() -> Result<()> {
24+
let args = Args::parse();
25+
let s = std::fs::read_to_string(&args.spec)?;
26+
let spec: RackUpdateSpec = toml::from_str(&s)?;
27+
let path = spec.create_archive(args.output_dir)?;
28+
println!("Created Release Update: {}", path.display());
29+
30+
Ok(())
31+
}

wicket/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ pub(crate) mod defaults;
3131
pub(crate) mod inventory;
3232
mod mgs;
3333
mod screens;
34+
pub mod update;
3435
mod widgets;
3536

3637
use inventory::{Component, ComponentId, Inventory, PowerState};

wicket/src/update.rs

+231
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
//! Types related to rack updates
6+
7+
use semver::Version;
8+
use serde::{Deserialize, Serialize};
9+
use sha3::Digest;
10+
use sha3::Sha3_256;
11+
use snafu::prelude::*;
12+
use std::fs::File;
13+
use std::path::{Path, PathBuf};
14+
15+
#[derive(Debug, Snafu)]
16+
pub enum UpdateError {
17+
#[snafu(display("File access error: {}", path.display()))]
18+
Io { source: std::io::Error, path: PathBuf },
19+
#[snafu(display("serde_json error: {}", path.display()))]
20+
Json { source: serde_json::Error, path: PathBuf },
21+
#[snafu(display("Path must be relative: {}", path.display()))]
22+
RelativePath { path: PathBuf },
23+
}
24+
25+
#[derive(Debug, Clone, Serialize, Deserialize)]
26+
pub struct Sha3_256Digest(#[serde(with = "hex::serde")] [u8; 32]);
27+
28+
#[derive(Debug, Clone, Serialize, Deserialize)]
29+
pub enum ArtifactType {
30+
// Sled Artifacts
31+
SledSp,
32+
SledRoT,
33+
HostPhase1,
34+
HostPhase2,
35+
36+
// PSC Artifacts
37+
PscSp,
38+
PscRot,
39+
40+
// Switch Artifacts
41+
SwitchSp,
42+
SwitchRot,
43+
}
44+
45+
/// A description of a software artifact that can be installed on the rack
46+
#[derive(Debug, Clone, Serialize, Deserialize)]
47+
pub struct Artifact {
48+
filename: PathBuf,
49+
artifact_type: ArtifactType,
50+
version: Version,
51+
digest: Sha3_256Digest,
52+
length: u64,
53+
}
54+
55+
/// Attempt to convert an ArtifactSpec to an Artifact
56+
/// by reading from the filesysetm and hashing.
57+
impl TryFrom<ArtifactSpec> for Artifact {
58+
type Error = UpdateError;
59+
fn try_from(spec: ArtifactSpec) -> Result<Self, Self::Error> {
60+
let mut hasher = Sha3_256::new();
61+
let mut file = File::open(&spec.filename)
62+
.context(IoSnafu { path: &spec.filename })?;
63+
let length = std::io::copy(&mut file, &mut hasher)
64+
.context(IoSnafu { path: &spec.filename })?;
65+
let digest = Sha3_256Digest(*hasher.finalize().as_ref());
66+
Ok(Artifact {
67+
filename: spec.filename,
68+
artifact_type: spec.artifact_type,
69+
version: spec.version,
70+
digest,
71+
length,
72+
})
73+
}
74+
}
75+
76+
/// User Input that describes artifacts as part of the [`RackUpdateSpec`]
77+
#[derive(Debug, Clone, Serialize, Deserialize)]
78+
pub struct ArtifactSpec {
79+
filename: PathBuf,
80+
artifact_type: ArtifactType,
81+
version: Version,
82+
}
83+
84+
/// The set of all artifacts in an update
85+
#[derive(Debug, Clone, Serialize, Deserialize)]
86+
pub struct Manifest {
87+
version: Version,
88+
artifacts: Vec<Artifact>,
89+
}
90+
91+
impl Manifest {
92+
pub fn dump<P: AsRef<Path>>(
93+
&self,
94+
path: P,
95+
) -> Result<PathBuf, UpdateError> {
96+
let path = path.as_ref().join("manifest.json");
97+
let mut file = File::create(&path).context(IoSnafu { path: &path })?;
98+
serde_json::to_writer(&mut file, self)
99+
.context(JsonSnafu { path: &path })?;
100+
Ok(path)
101+
}
102+
103+
/// Unpack a Tar archive into `unpack_dir`, read the manifest, and return
104+
/// the manifest.
105+
pub fn load(
106+
tarfile_path: impl AsRef<Path>,
107+
unpack_dir: impl AsRef<Path>,
108+
) -> Result<Manifest, UpdateError> {
109+
let tarfile = File::open(tarfile_path.as_ref())
110+
.context(IoSnafu { path: tarfile_path.as_ref() })?;
111+
let mut archive = tar::Archive::new(tarfile);
112+
archive
113+
.unpack(&unpack_dir)
114+
.context(IoSnafu { path: tarfile_path.as_ref() })?;
115+
let path = unpack_dir.as_ref().join("manifest.json");
116+
let file = File::open(&path).context(IoSnafu { path: &path })?;
117+
let manifest =
118+
serde_json::from_reader(file).context(JsonSnafu { path: &path })?;
119+
Ok(manifest)
120+
}
121+
}
122+
123+
/// The user input description of a [`RackUpdate`]
124+
///
125+
/// Files are read and processed into a [`RackUpdate`] according to the
126+
/// [`RackUpdateSpec`].
127+
#[derive(Debug, Clone, Serialize, Deserialize)]
128+
pub struct RackUpdateSpec {
129+
version: Version,
130+
artifacts: Vec<ArtifactSpec>,
131+
}
132+
133+
impl RackUpdateSpec {
134+
/// Create a new RackUpdateSpec.
135+
///
136+
/// Typically this will be created by reading from a `rack-update-
137+
/// spec.toml` file.
138+
pub fn new(
139+
version: Version,
140+
artifacts: Vec<ArtifactSpec>,
141+
) -> RackUpdateSpec {
142+
RackUpdateSpec { version, artifacts }
143+
}
144+
145+
/// Return the name of the given release
146+
pub fn release_name(&self) -> String {
147+
format!("oxide-release-{}", self.version)
148+
}
149+
150+
/// Create a Tar archive file including a generated manifest and all release
151+
/// files described by `self.artifacts`.
152+
///
153+
/// Return the path of the created archive.
154+
///
155+
/// This archive file can be loaded into a [`RackUpdate`] via
156+
/// [`RackUpdate::load`].
157+
pub fn create_archive(
158+
self,
159+
output_dir: PathBuf,
160+
) -> Result<PathBuf, UpdateError> {
161+
let mut artifacts = vec![];
162+
let mut filename = output_dir.clone();
163+
filename.push(self.release_name());
164+
filename.set_extension("tar");
165+
let tarfile = File::create(&filename)
166+
.context(IoSnafu { path: filename.clone() })?;
167+
let mut builder = tar::Builder::new(tarfile);
168+
for artifact_spec in self.artifacts {
169+
builder
170+
.append_path_with_name(
171+
&artifact_spec.filename,
172+
&artifact_spec.filename.file_name().ok_or(
173+
UpdateError::RelativePath {
174+
path: artifact_spec.filename.clone(),
175+
},
176+
)?,
177+
)
178+
.context(IoSnafu { path: &artifact_spec.filename })?;
179+
artifacts.push(artifact_spec.try_into()?);
180+
}
181+
let manifest = Manifest { version: self.version, artifacts };
182+
let manifest_path = manifest.dump(output_dir)?;
183+
builder
184+
.append_path_with_name(
185+
&manifest_path,
186+
&manifest_path.file_name().unwrap(),
187+
)
188+
.context(IoSnafu { path: &manifest_path })?;
189+
builder.finish().context(IoSnafu { path: &manifest_path })?;
190+
Ok(filename)
191+
}
192+
}
193+
194+
#[cfg(test)]
195+
mod tests {
196+
use super::*;
197+
use std::fs::File;
198+
use std::io::Write;
199+
use tempfile::TempDir;
200+
201+
fn test_spec() -> (TempDir, RackUpdateSpec) {
202+
let tmp_dir = TempDir::new().unwrap();
203+
let sled_sp = ArtifactSpec {
204+
filename: tmp_dir.path().join("sled-sp-img-1.0.0.tar.gz"),
205+
artifact_type: ArtifactType::SledSp,
206+
version: Version::new(1, 0, 0),
207+
};
208+
209+
// Create a file of junk data for testing
210+
let mut file = File::create(&sled_sp.filename).unwrap();
211+
writeln!(
212+
file,
213+
"This is not a real SP Image. Hell it's not even a tarball!"
214+
)
215+
.unwrap();
216+
let spec = RackUpdateSpec::new(Version::new(1, 0, 0), vec![sled_sp]);
217+
218+
(tmp_dir, spec)
219+
}
220+
221+
#[test]
222+
fn generate_update_archive_then_load_manifest() {
223+
let (input_dir, spec) = test_spec();
224+
let output_dir = TempDir::new().unwrap();
225+
let update_path =
226+
spec.create_archive(input_dir.path().to_owned()).unwrap();
227+
let manifest = Manifest::load(&update_path, output_dir.path()).unwrap();
228+
assert_eq!(manifest.artifacts.len(), 1);
229+
assert_eq!(manifest.version, Version::new(1, 0, 0));
230+
}
231+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This is not a real SP Image. Hell it's not even a tarball!

0 commit comments

Comments
 (0)