Initial commit of update utility to simplify image upgrades
This commit is contained in:
parent
77f19777a8
commit
ba29516212
@ -15,6 +15,7 @@ mod install;
|
|||||||
mod mkimage;
|
mod mkimage;
|
||||||
mod realmfs;
|
mod realmfs;
|
||||||
mod sync;
|
mod sync;
|
||||||
|
mod update;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let exe = match env::current_exe() {
|
let exe = match env::current_exe() {
|
||||||
@ -34,6 +35,8 @@ fn main() {
|
|||||||
image::main(args);
|
image::main(args);
|
||||||
} else if exe == Path::new("/usr/bin/citadel-realmfs") {
|
} else if exe == Path::new("/usr/bin/citadel-realmfs") {
|
||||||
realmfs::main(args);
|
realmfs::main(args);
|
||||||
|
} else if exe == Path::new("/usr/bin/citadel-update") {
|
||||||
|
update::main(args);
|
||||||
} else if exe == Path::new("/usr/libexec/citadel-desktop-sync") {
|
} else if exe == Path::new("/usr/libexec/citadel-desktop-sync") {
|
||||||
sync::main(args);
|
sync::main(args);
|
||||||
} else if exe == Path::new("/usr/libexec/citadel-run") {
|
} else if exe == Path::new("/usr/libexec/citadel-run") {
|
||||||
@ -54,6 +57,7 @@ fn dispatch_command(args: Vec<String>) {
|
|||||||
"install" => install::main(rebuild_args("citadel-install", args)),
|
"install" => install::main(rebuild_args("citadel-install", args)),
|
||||||
"image" => image::main(rebuild_args("citadel-image", args)),
|
"image" => image::main(rebuild_args("citadel-image", args)),
|
||||||
"realmfs" => realmfs::main(rebuild_args("citadel-realmfs", args)),
|
"realmfs" => realmfs::main(rebuild_args("citadel-realmfs", args)),
|
||||||
|
"update" => update::main(rebuild_args("citadel-update", args)),
|
||||||
"mkimage" => mkimage::main(rebuild_args("citadel-mkimage", args)),
|
"mkimage" => mkimage::main(rebuild_args("citadel-mkimage", args)),
|
||||||
"sync" => sync::main(rebuild_args("citadel-desktop-sync", args)),
|
"sync" => sync::main(rebuild_args("citadel-desktop-sync", args)),
|
||||||
"run" => do_citadel_run(rebuild_args("citadel-run", args)),
|
"run" => do_citadel_run(rebuild_args("citadel-run", args)),
|
||||||
|
479
citadel-tool/src/update/kernel.rs
Normal file
479
citadel-tool/src/update/kernel.rs
Normal file
@ -0,0 +1,479 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::fmt::{self,Write};
|
||||||
|
use std::path::{Path,PathBuf};
|
||||||
|
|
||||||
|
use libcitadel::{Result,util};
|
||||||
|
|
||||||
|
const DEFAULT_MAX_ENTRIES: usize = 3;
|
||||||
|
const DEFAULT_BOOT_COUNT: u32 = 3;
|
||||||
|
const DEFAULT_KERNEL_CMDLINE: &str = "root=/dev/mapper/rootfs add_efi_memmap intel_iommu=off cryptomgr.notests rcupdate.rcu_expedited=1 rcu_nocbs=0-64 tsc=reliable no_timer_check noreplace-smp i915.fastboot=1 quiet splash";
|
||||||
|
|
||||||
|
pub struct KernelInstaller {
|
||||||
|
max_entries: usize,
|
||||||
|
new_kernel: KernelBzImage,
|
||||||
|
all_entries: BootEntries,
|
||||||
|
boot_entries: BootEntries,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KernelInstaller {
|
||||||
|
|
||||||
|
pub fn install_kernel(new_kernel: &Path, version: &str) -> Result<()> {
|
||||||
|
let mut installer = Self::new(new_kernel, version)?;
|
||||||
|
if installer.is_already_installed() {
|
||||||
|
bail!("identical kernel is is already installed");
|
||||||
|
}
|
||||||
|
installer.install()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(new_kernel: &Path, version: &str) -> Result<KernelInstaller> {
|
||||||
|
let new_kernel = KernelBzImage::from_path_and_version(new_kernel.to_path_buf(), version)?;
|
||||||
|
let all_entries = BootEntries::load()?;
|
||||||
|
let boot_entries = all_entries.find_by_name("boot");
|
||||||
|
|
||||||
|
Ok(KernelInstaller {
|
||||||
|
max_entries: DEFAULT_MAX_ENTRIES,
|
||||||
|
new_kernel,
|
||||||
|
all_entries,
|
||||||
|
boot_entries,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_already_installed(&self) -> bool {
|
||||||
|
self.all_entries.0.iter()
|
||||||
|
.flat_map(|e| e.bzimage.as_ref())
|
||||||
|
.any(|k| k.shasum == self.new_kernel.shasum)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn install(&mut self) -> Result<PathBuf> {
|
||||||
|
let install_path = self.install_kernel_path()?;
|
||||||
|
info!("Copying kernel bzImage to {}", install_path.display());
|
||||||
|
fs::copy(&self.new_kernel.path, &install_path)?;
|
||||||
|
|
||||||
|
self.boot_entries.rotate()?;
|
||||||
|
|
||||||
|
let options = self.generate_options_line();
|
||||||
|
let entry = BootEntry::create_for_kernel("boot", self.new_kernel.clone(), options, Some(DEFAULT_BOOT_COUNT.to_string()));
|
||||||
|
entry.write(&install_path)?;
|
||||||
|
|
||||||
|
while self.boot_entries.0.len() >= self.max_entries {
|
||||||
|
let mut e = self.boot_entries.0.pop().unwrap();
|
||||||
|
e.remove()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 0) if boot.conf does not exist, just write it. done.
|
||||||
|
// 1) if current boot.conf is not verified, just replace it. done.
|
||||||
|
// 2) rotate boot.conf to boot.1.conf
|
||||||
|
// 3) create new boot.conf entry
|
||||||
|
|
||||||
|
Ok(install_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_kernel_path(&self) -> Result<PathBuf> {
|
||||||
|
let version = match self.new_kernel.version {
|
||||||
|
Some(v) => v,
|
||||||
|
None => bail!("new kernel does not have a version"),
|
||||||
|
};
|
||||||
|
let mut path = Path::new("/boot").join(format!("bzImage-{}", version));
|
||||||
|
|
||||||
|
for i in 1..5 {
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(path);
|
||||||
|
}
|
||||||
|
path = Path::new("/boot").join(format!("bzImage-{}-{}", version, i));
|
||||||
|
}
|
||||||
|
bail!("Unable to find unused name for new kernel")
|
||||||
|
}
|
||||||
|
|
||||||
|
// return kernel commandline from most recent boot entry.
|
||||||
|
// If no boot entries exist, return default kernel commandline
|
||||||
|
fn generate_options_line(&self) -> &str {
|
||||||
|
if let Some(entry) = self.boot_entries.0.first() {
|
||||||
|
entry.options.as_str()
|
||||||
|
} else {
|
||||||
|
DEFAULT_KERNEL_CMDLINE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq,Ord,PartialOrd,Eq,Copy,Clone,Debug)]
|
||||||
|
pub struct KernelVersion {
|
||||||
|
version: u32,
|
||||||
|
major: u32,
|
||||||
|
minor: Option<u32>,
|
||||||
|
revision: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KernelVersion {
|
||||||
|
// return a KernelVersion instance if the string can be parsed as
|
||||||
|
// a valid kernel version string. Otherwise return None
|
||||||
|
fn parse_from_str(s: &str) -> Option<KernelVersion> {
|
||||||
|
let mut split = s.split("-");
|
||||||
|
|
||||||
|
let fields = split.next()
|
||||||
|
.and_then(Self::parse_version_field);
|
||||||
|
|
||||||
|
let revision = split.next()
|
||||||
|
.and_then(|s| s.parse::<u32>().ok());
|
||||||
|
|
||||||
|
fields.map(|v| {
|
||||||
|
KernelVersion {
|
||||||
|
version: v.0,
|
||||||
|
major: v.1,
|
||||||
|
minor: v.2,
|
||||||
|
revision,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_from_path(path: &Path) -> Option<KernelVersion> {
|
||||||
|
Self::path_version_string(path)
|
||||||
|
.and_then(|s| Self::parse_from_str(&s))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return version as a string without including revision
|
||||||
|
pub fn version(&self) -> String {
|
||||||
|
if let Some(minor) = self.minor {
|
||||||
|
format!("{}.{}.{}", self.version, self.major, minor)
|
||||||
|
} else {
|
||||||
|
format!("{}.{}", self.version, self.major)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// turn path such as /path/to/bzImage-1.2.3 into the string "1.2.3"
|
||||||
|
// If path does not have a filename or if there is no '-' character
|
||||||
|
// in filename, return None
|
||||||
|
fn path_version_string(path: &Path) -> Option<String> {
|
||||||
|
path.file_name()
|
||||||
|
.and_then(|fname| fname.to_str())
|
||||||
|
.and_then(|s| s.splitn(2, "-").nth(1))
|
||||||
|
.map(ToString::to_string)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_version_field(s: &str) -> Option<(u32,u32,Option<u32>)> {
|
||||||
|
let elems: Vec<u32> = s.split(".")
|
||||||
|
.flat_map(|s| s.parse::<u32>().ok())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
match elems.len() {
|
||||||
|
2 => Some((elems[0], elems[1], None)),
|
||||||
|
3 => Some((elems[0], elems[1], Some(elems[2]))),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for KernelVersion {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}.{}", self.version, self.major)?;
|
||||||
|
if let Some(minor) = self.minor {
|
||||||
|
write!(f, ".{}", minor)?;
|
||||||
|
}
|
||||||
|
if let Some(revision) = self.revision {
|
||||||
|
write!(f, "-{}", revision)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BootEntries(Vec<BootEntry>);
|
||||||
|
|
||||||
|
impl BootEntries {
|
||||||
|
const BASE_PATH: &'static str = "/boot/loader/entries";
|
||||||
|
|
||||||
|
// The directory where boot entries are found
|
||||||
|
fn base_path() -> &'static Path {
|
||||||
|
Path::new(Self::BASE_PATH)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load() -> Result<BootEntries> {
|
||||||
|
let mut entries = BootEntries(Vec::new());
|
||||||
|
entries.load_entries()?;
|
||||||
|
Ok(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_entries(&mut self) -> Result<()> {
|
||||||
|
let base_path = Self::base_path();
|
||||||
|
if !base_path.exists() {
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
for dirent in fs::read_dir(base_path)? {
|
||||||
|
let dirent = dirent?;
|
||||||
|
if let Some(fname) = dirent.file_name().to_str() {
|
||||||
|
self.load_filename(fname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_filename(&mut self, fname: &str) {
|
||||||
|
if fname.ends_with(".conf") {
|
||||||
|
let mut entry = BootEntry::from_filename(fname);
|
||||||
|
if let Err(e) = entry.load() {
|
||||||
|
warn!("Error loading boot entry {}: {}", fname, e);
|
||||||
|
} else {
|
||||||
|
self.0.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_by_name(&self, name: &str) -> BootEntries {
|
||||||
|
let mut v: Vec<BootEntry> = self.0.iter()
|
||||||
|
.filter(|e| e.name.as_str() == name)
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
v.sort_by(|a,b| a.index.cmp(&b.index));
|
||||||
|
BootEntries(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename entries in a series so that the base name
|
||||||
|
// (the name with no associated index value) is unused.
|
||||||
|
// so if boot.conf and boot.1.conf exist, they will
|
||||||
|
// be renamed to:
|
||||||
|
// boot.1.conf and boot.2.conf
|
||||||
|
fn rotate(&mut self) -> Result<()> {
|
||||||
|
if let Some(entry) = self.0.first() {
|
||||||
|
// Only rotate if the first entry:
|
||||||
|
// 1) exists
|
||||||
|
// 2) does not have an index value
|
||||||
|
// 3) does not have boot count (ie: in 'good' boot state)
|
||||||
|
if entry.index.is_none() && entry.is_good() {
|
||||||
|
self._rotate()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _rotate(&mut self) -> Result<()> {
|
||||||
|
for entry in self.0.iter_mut().rev() {
|
||||||
|
if !entry.rotate()? {
|
||||||
|
bail!("Failed to rotate boot entry {} because next index already exists", entry.path().display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct BootEntry {
|
||||||
|
// The filename with index,bootcount,and suffix removed
|
||||||
|
name: String,
|
||||||
|
// An optional integer value parsed from filename
|
||||||
|
index: Option<u32>,
|
||||||
|
// See systemd-boot(7) for description of boot count name convention
|
||||||
|
boot_count: Option<String>,
|
||||||
|
// Contents of the 'title' line
|
||||||
|
title: String,
|
||||||
|
// The kernel image corresponding to the 'linux' line, if it exists
|
||||||
|
bzimage: Option<KernelBzImage>,
|
||||||
|
// Contents of the 'options' line
|
||||||
|
options: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BootEntry {
|
||||||
|
// parse filename into 3 components:
|
||||||
|
//
|
||||||
|
// Only the name field is mandatory. The index or bootcount may not exist.
|
||||||
|
//
|
||||||
|
// $(name).$(index)+$(bootcount).conf
|
||||||
|
//
|
||||||
|
// boot.2+3.conf ("boot", Some(2), Some("3"))
|
||||||
|
// boot.conf ("boot", None, None)
|
||||||
|
// boot+2-2.conf ("boot", None, Some("2-2"))
|
||||||
|
//
|
||||||
|
fn parse_filename(filename: &str) -> (String, Option<u32>, Option<String>) {
|
||||||
|
let filename = filename.trim_end_matches(".conf");
|
||||||
|
let mut parts = filename.splitn(2, '+');
|
||||||
|
let name = parts.next().unwrap().to_string();
|
||||||
|
let boot_count = parts.next().map(|s| s.to_string());
|
||||||
|
let v: Vec<&str> = name.rsplitn(2, '.').collect();
|
||||||
|
if v.len() == 2 {
|
||||||
|
if let Ok(n) = v[0].parse::<u32>() {
|
||||||
|
let index = Some(n);
|
||||||
|
let name = v[1].to_string();
|
||||||
|
return (name, index, boot_count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(name, None, boot_count)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_filename(filename: &str) -> BootEntry {
|
||||||
|
let (name, index, boot_count) = Self::parse_filename(filename);
|
||||||
|
Self::new(name, index, boot_count)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new<S: AsRef<str>>(name: S, index: Option<u32>, boot_count: Option<String>) -> BootEntry {
|
||||||
|
let name = name.as_ref().to_string();
|
||||||
|
BootEntry {
|
||||||
|
name, index, boot_count,
|
||||||
|
title: String::new(),
|
||||||
|
bzimage: None,
|
||||||
|
options: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_for_kernel(name: &str, kernel: KernelBzImage, options: &str, boot_count: Option<String>) -> BootEntry {
|
||||||
|
let mut entry = BootEntry::new(name, None, boot_count);
|
||||||
|
entry.options = options.to_string();
|
||||||
|
entry.generate_title(&kernel);
|
||||||
|
entry.bzimage = Some(kernel);
|
||||||
|
entry
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&self, kernel_path: &Path) -> Result<()> {
|
||||||
|
let kernel = if let Some(fname) = kernel_path.file_name() {
|
||||||
|
fname.to_str().expect("could not convert filename to string").to_string()
|
||||||
|
} else {
|
||||||
|
bail!("kernel path does not have filename");
|
||||||
|
};
|
||||||
|
let mut buffer = String::new();
|
||||||
|
writeln!(&mut buffer, "title {}", self.title)?;
|
||||||
|
writeln!(&mut buffer, "linux /{}", kernel)?;
|
||||||
|
writeln!(&mut buffer, "options {}", self.options)?;
|
||||||
|
fs::write(self.path(), buffer)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_good(&self) -> bool {
|
||||||
|
self.boot_count.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_title(&mut self, kernel: &KernelBzImage) {
|
||||||
|
if let Some(v) = kernel.version {
|
||||||
|
self.title = format!("Subgraph OS (Citadel {})", v);
|
||||||
|
} else {
|
||||||
|
self.title = format!("Subgraph OS (Citadel)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load(&mut self) -> Result<()> {
|
||||||
|
let path = self.path();
|
||||||
|
for line in fs::read_to_string(&path)?.lines() {
|
||||||
|
if line.starts_with("title ") {
|
||||||
|
self.title = line.trim_start_matches("title ").to_owned();
|
||||||
|
} else if line.starts_with("linux /") {
|
||||||
|
let path = Path::new("/boot").join(line.trim_start_matches("linux /"));
|
||||||
|
if path.exists() {
|
||||||
|
let bzimage = KernelBzImage::from_path(&path)?;
|
||||||
|
self.bzimage = Some(bzimage);
|
||||||
|
} else {
|
||||||
|
bail!("kernel path {} in boot entry does not exist", path.display());
|
||||||
|
}
|
||||||
|
} else if line.starts_with("options ") {
|
||||||
|
self.options = line.trim_start_matches("options ").to_owned();
|
||||||
|
} else {
|
||||||
|
warn!("unexpected line in boot entry file {}: {}", path.display(), line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.title.is_empty() {
|
||||||
|
bail!("no 'title' line in boot entry file {}", path.display());
|
||||||
|
}
|
||||||
|
if self.bzimage.is_none() {
|
||||||
|
bail!("no 'linux' line in boot entry file {}", path.display());
|
||||||
|
}
|
||||||
|
if self.options.is_empty() {
|
||||||
|
bail!("no 'options' line in boot entry file {}", path.display());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(&self) -> PathBuf {
|
||||||
|
let mut filename = self.name.clone();
|
||||||
|
if let Some(index) = self.index {
|
||||||
|
filename.push_str(&format!(".{}", index));
|
||||||
|
}
|
||||||
|
if let Some(ref count) = self.boot_count {
|
||||||
|
filename.push_str(&format!("+{}.conf", count));
|
||||||
|
} else {
|
||||||
|
filename.push_str(".conf");
|
||||||
|
}
|
||||||
|
BootEntries::base_path().join(filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment index value and rename boot entry file. Return false
|
||||||
|
// if new name already exists.
|
||||||
|
fn rotate(&mut self) -> Result<(bool)> {
|
||||||
|
let old_path = self.path();
|
||||||
|
let old_index = self.index;
|
||||||
|
self.index = match self.index {
|
||||||
|
Some(idx) => Some(idx + 1),
|
||||||
|
None => Some(1),
|
||||||
|
};
|
||||||
|
let new_path = self.path();
|
||||||
|
if new_path.exists() {
|
||||||
|
self.index = old_index;
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
verbose!("Rotating boot entry {} to {}", old_path.display(), new_path.display());
|
||||||
|
fs::rename(old_path, new_path)?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove boot entry file and associated kernel bzimage
|
||||||
|
fn remove(&mut self) -> Result<()> {
|
||||||
|
if let Some(ref bzimage) = self.bzimage {
|
||||||
|
bzimage.remove_file()?;
|
||||||
|
self.bzimage = None;
|
||||||
|
}
|
||||||
|
fs::remove_file(self.path())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone,PartialEq)]
|
||||||
|
struct KernelBzImage {
|
||||||
|
path: PathBuf,
|
||||||
|
version: Option<KernelVersion>,
|
||||||
|
shasum: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KernelBzImage {
|
||||||
|
fn from_path_and_version(path: PathBuf, version: &str) -> Result<KernelBzImage> {
|
||||||
|
let shasum = util::sha256(&path)?;
|
||||||
|
let version = KernelVersion::parse_from_str(version);
|
||||||
|
Ok(KernelBzImage {
|
||||||
|
path, version, shasum
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_path(path: &Path) -> Result<KernelBzImage> {
|
||||||
|
let version = KernelVersion::parse_from_path(&path);
|
||||||
|
let shasum = util::sha256(path)?;
|
||||||
|
let path = path.to_path_buf();
|
||||||
|
Ok(KernelBzImage { path, version, shasum })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_file(&self) -> Result<()> {
|
||||||
|
fs::remove_file(&self.path)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_version_parse() {
|
||||||
|
let path = Path::new("/boot/bzImage-2.2-x");
|
||||||
|
let kv = KernelVersion::parse_from_path(path).unwrap();
|
||||||
|
assert_eq!(kv.version, 2);
|
||||||
|
assert_eq!(kv.major, 2);
|
||||||
|
assert_eq!(kv.minor, None);
|
||||||
|
let kv2 = KernelVersion::parse_from_str("5.1.1").unwrap();
|
||||||
|
let kv3 = KernelVersion::parse_from_str("5.8.1").unwrap();
|
||||||
|
let kv4 = KernelVersion::parse_from_str("5.8").unwrap();
|
||||||
|
assert!(kv < kv2);
|
||||||
|
assert!(kv2 < kv3);
|
||||||
|
assert!(kv4 < kv3);
|
||||||
|
println!("{} {} {} {}", kv, kv2, kv3, kv4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bootentry_parse_filename() {
|
||||||
|
let fields = BootEntry::parse_filename("foo.heh.2+abc.conf");
|
||||||
|
assert_eq!(fields, ("foo.heh".to_string(), Some(2), Some("abc".to_string())));
|
||||||
|
let fields = BootEntry::parse_filename("foo+abc.conf");
|
||||||
|
assert_eq!(fields, ("foo".to_string(), None, Some("abc".to_string())));
|
||||||
|
let fields = BootEntry::parse_filename("foo.2.conf");
|
||||||
|
assert_eq!(fields, ("foo".to_string(), Some(2), None));
|
||||||
|
}
|
338
citadel-tool/src/update/mod.rs
Normal file
338
citadel-tool/src/update/mod.rs
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use libcitadel::{Result, Partition, ResourceImage, ImageHeader, LogLevel, Logger};
|
||||||
|
use crate::update::kernel::{KernelInstaller, KernelVersion};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::fs::DirEntry;
|
||||||
|
|
||||||
|
mod kernel;
|
||||||
|
|
||||||
|
const FLAG_SKIP_SHA: u32 = 0x01;
|
||||||
|
const FLAG_NO_PREFER: u32 = 0x02;
|
||||||
|
const FLAG_QUIET: u32 = 0x04;
|
||||||
|
|
||||||
|
pub fn main(args: Vec<String>) {
|
||||||
|
let mut args = args.iter().skip(1);
|
||||||
|
let mut flags = 0;
|
||||||
|
|
||||||
|
Logger::set_log_level(LogLevel::Info);
|
||||||
|
|
||||||
|
while let Some(arg) = args.next() {
|
||||||
|
if arg == "--skip-sha" {
|
||||||
|
flags |= FLAG_SKIP_SHA;
|
||||||
|
} else if arg == "--no-prefer" {
|
||||||
|
flags |= FLAG_NO_PREFER;
|
||||||
|
} else if arg == "--quiet" {
|
||||||
|
flags |= FLAG_QUIET;
|
||||||
|
Logger::set_log_level(LogLevel::Warn);
|
||||||
|
} else if arg == "--verbose" {
|
||||||
|
Logger::set_log_level(LogLevel::Debug);
|
||||||
|
} else if arg == "--choose-rootfs" {
|
||||||
|
let _ = choose_install_partition(true);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
let path = Path::new(arg);
|
||||||
|
if let Err(e) = install_image(path, flags) {
|
||||||
|
warn!("Update failed: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search directory containing installed image files for an
|
||||||
|
// image file that has an identical shasum and abort the installation
|
||||||
|
// if a duplicate is found.
|
||||||
|
fn detect_duplicates(image: &ResourceImage) -> Result<()> {
|
||||||
|
let metainfo = image.metainfo();
|
||||||
|
let channel = metainfo.channel();
|
||||||
|
let shasum = metainfo.shasum();
|
||||||
|
|
||||||
|
validate_channel_name(&channel)?;
|
||||||
|
|
||||||
|
let resource_dir = Path::new("/storage/resources/")
|
||||||
|
.join(channel);
|
||||||
|
|
||||||
|
if !resource_dir.exists() {
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
for dirent in fs::read_dir(resource_dir)? {
|
||||||
|
let dirent = dirent?;
|
||||||
|
match ResourceImage::from_path(dirent.path()) {
|
||||||
|
Ok(img) => if img.metainfo().shasum() == shasum {
|
||||||
|
bail!("A duplicate image file with the same shasum already exists at {}", img.path().display());
|
||||||
|
},
|
||||||
|
Err(err) => warn!("{}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_image(path: &Path, flags: u32) -> Result<()> {
|
||||||
|
if !path.exists() {
|
||||||
|
bail!("file path {} does not exist", path.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut image = ResourceImage::from_path(path)?;
|
||||||
|
detect_duplicates(&image)?;
|
||||||
|
prepare_image(&image, flags)?;
|
||||||
|
|
||||||
|
match image.metainfo().image_type() {
|
||||||
|
"kernel" => install_kernel_image(&mut image),
|
||||||
|
"extra" => install_extra_image(&image),
|
||||||
|
"rootfs" => install_rootfs_image(&image, flags),
|
||||||
|
image_type => bail!("Unknown image type: {}", image_type),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the image file for installation by decompressing and generating
|
||||||
|
// dmverity hash tree.
|
||||||
|
fn prepare_image(image: &ResourceImage, flags: u32) -> Result<()> {
|
||||||
|
if image.is_compressed() {
|
||||||
|
image.decompress()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if flags & FLAG_SKIP_SHA == 0 {
|
||||||
|
info!("Verifying sha256 hash of image");
|
||||||
|
let shasum = image.generate_shasum()?;
|
||||||
|
if shasum != image.metainfo().shasum() {
|
||||||
|
bail!("image file does not have expected sha256 value");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !image.has_verity_hashtree() {
|
||||||
|
image.generate_verity_hashtree()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_extra_image(image: &ResourceImage) -> Result<()> {
|
||||||
|
let filename = format!("citadel-extra-{:03}.img", image.header().metainfo().version());
|
||||||
|
install_image_file(image, filename.as_str())?;
|
||||||
|
remove_old_extra_images(image)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_old_extra_images(image: &ResourceImage) -> Result<()> {
|
||||||
|
let new_meta = image.header().metainfo();
|
||||||
|
let shasum = new_meta.shasum();
|
||||||
|
let target_dir = target_directory(image)?;
|
||||||
|
for dirent in fs::read_dir(target_dir)? {
|
||||||
|
let dirent = dirent?;
|
||||||
|
let path = dirent.path();
|
||||||
|
maybe_remove_old_extra_image(&path, shasum)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maybe_remove_old_extra_image(path: &Path, shasum: &str) -> Result<()> {
|
||||||
|
let header = ImageHeader::from_file(&path)?;
|
||||||
|
if !header.is_magic_valid() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let meta = header.metainfo();
|
||||||
|
if meta.image_type() != "extra" {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if meta.shasum() != shasum {
|
||||||
|
info!("Removing old extra resource image {}", path.display());
|
||||||
|
fs::remove_file(&path)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
fn install_kernel_image(image: &mut ResourceImage) -> Result<()> {
|
||||||
|
if !Path::new("/boot/loader/loader.conf").exists() {
|
||||||
|
bail!("failed to automount /boot partition. Please manually mount correct partition.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let metainfo = image.header().metainfo();
|
||||||
|
let version = metainfo.version();
|
||||||
|
let kernel_version = match metainfo.kernel_version() {
|
||||||
|
Some(kv) => kv,
|
||||||
|
None => bail!("Kernel image does not have kernel version field"),
|
||||||
|
};
|
||||||
|
info!("kernel version is {}", kernel_version);
|
||||||
|
install_kernel_file(image, &kernel_version)?;
|
||||||
|
|
||||||
|
let filename = format!("citadel-kernel-{}-{:03}.img", kernel_version, version);
|
||||||
|
install_image_file(image, &filename)?;
|
||||||
|
|
||||||
|
let all_versions = all_boot_kernel_versions()?;
|
||||||
|
let image_dir = target_directory(image)?;
|
||||||
|
let mut remove_paths = Vec::new();
|
||||||
|
for dirent in fs::read_dir(image_dir)? {
|
||||||
|
let dirent = dirent?;
|
||||||
|
let path = dirent.path();
|
||||||
|
if is_unused_kernel_image(&path, &all_versions)? {
|
||||||
|
remove_paths.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for p in remove_paths {
|
||||||
|
fs::remove_file(p)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_unused_kernel_image(path: &Path, versions: &HashSet<String>) -> Result<bool> {
|
||||||
|
let header = ImageHeader::from_file(path)?;
|
||||||
|
if !header.is_magic_valid() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
let meta = header.metainfo();
|
||||||
|
if meta.image_type() != "kernel" {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
if let Some(version) = meta.kernel_version() {
|
||||||
|
if !versions.contains(version) {
|
||||||
|
info!("Removing kernel image {} because kernel version {} is unused", path.display(), version);
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("kernel image {} does not have kernel-version metainfo field", path.display());
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_kernel_file(image: &mut ResourceImage, kernel_version: &str) -> Result<()> {
|
||||||
|
let mountpoint = Path::new("/run/citadel/images/kernel-install.mountpoint");
|
||||||
|
info!("Temporarily mounting kernel resource image");
|
||||||
|
let mut handle = image.mount_at(mountpoint)?;
|
||||||
|
let kernel_path = mountpoint.join("kernel/bzImage");
|
||||||
|
if !kernel_path.exists() {
|
||||||
|
handle.unmount()?;
|
||||||
|
bail!("kernel not found in kernel resource image at /kernel/bzImage")
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = KernelInstaller::install_kernel(&kernel_path, kernel_version);
|
||||||
|
info!("Unmounting kernel resource image");
|
||||||
|
handle.unmount()?;
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn all_boot_kernel_versions() -> Result<HashSet<String>> {
|
||||||
|
let mut result = HashSet::new();
|
||||||
|
for dirent in fs::read_dir("/boot")? {
|
||||||
|
let dirent = dirent?;
|
||||||
|
if is_kernel_dirent(&dirent) {
|
||||||
|
if let Some(kv) = KernelVersion::parse_from_path(&dirent.path()) {
|
||||||
|
result.insert(kv.version());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_kernel_dirent(dirent: &DirEntry) -> bool {
|
||||||
|
if let Some(fname) = dirent.file_name().to_str() {
|
||||||
|
fname.starts_with("bzImage-")
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_image_file(image: &ResourceImage, filename: &str) -> Result<()> {
|
||||||
|
let image_dir = target_directory(image)?;
|
||||||
|
let image_dest = image_dir.join(filename);
|
||||||
|
if image_dest.exists() {
|
||||||
|
rotate(&image_dest)?;
|
||||||
|
}
|
||||||
|
info!("installing image file by moving from {} to {}", image.path().display(), image_dest.display());
|
||||||
|
fs::rename(image.path(), image_dest)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn target_directory(image: &ResourceImage) -> Result<PathBuf> {
|
||||||
|
let metainfo = image.header().metainfo();
|
||||||
|
let channel = metainfo.channel();
|
||||||
|
validate_channel_name(channel)?;
|
||||||
|
Ok(Path::new("/storage/resources").join(channel))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rotate(path: &Path) -> Result<()> {
|
||||||
|
if !path.exists() || path.file_name().is_none() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let filename = path.file_name().unwrap();
|
||||||
|
let dot_zero = path.with_file_name(format!("{}.0", filename.to_string_lossy()));
|
||||||
|
if dot_zero.exists() {
|
||||||
|
fs::remove_file(&dot_zero)?;
|
||||||
|
}
|
||||||
|
fs::rename(path, &dot_zero)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_channel_name(channel: &str) -> Result<()> {
|
||||||
|
if !channel.chars().all(|c| c.is_ascii_lowercase()) {
|
||||||
|
bail!("Image has invalid channel name '{}'", channel);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_rootfs_image(image: &ResourceImage, flags: u32) -> Result<()> {
|
||||||
|
let quiet = flags & FLAG_QUIET != 0;
|
||||||
|
let partition = choose_install_partition(!quiet)?;
|
||||||
|
|
||||||
|
if flags & FLAG_NO_PREFER == 0 {
|
||||||
|
clear_prefer_boot()?;
|
||||||
|
image.header().set_flag(ImageHeader::FLAG_PREFER_BOOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
image.write_to_partition(&partition)?;
|
||||||
|
info!("Image written to {:?}", partition.path());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_prefer_boot() -> Result<()> {
|
||||||
|
for mut p in Partition::rootfs_partitions()? {
|
||||||
|
if p.is_initialized() && p.header().has_flag(ImageHeader::FLAG_PREFER_BOOT) {
|
||||||
|
p.clear_flag_and_write(ImageHeader::FLAG_PREFER_BOOT)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bool_to_yesno(val: bool) -> &'static str {
|
||||||
|
if val {
|
||||||
|
"YES"
|
||||||
|
} else {
|
||||||
|
" NO"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn choose_install_partition(verbose: bool) -> Result<Partition> {
|
||||||
|
let partitions = Partition::rootfs_partitions()?;
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
for p in &partitions {
|
||||||
|
info!("Partition: {} (Mounted: {}) (Empty: {})",
|
||||||
|
p.path().display(),
|
||||||
|
bool_to_yesno(p.is_mounted()),
|
||||||
|
bool_to_yesno(!p.is_initialized()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for p in &partitions {
|
||||||
|
if !p.is_mounted() && !p.is_initialized() {
|
||||||
|
if verbose {
|
||||||
|
info!("Choosing {} because it is empty and not mounted", p.path().display());
|
||||||
|
}
|
||||||
|
return Ok(p.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for p in &partitions {
|
||||||
|
if !p.is_mounted() {
|
||||||
|
if verbose {
|
||||||
|
info!("Choosing {} because it is not mounted", p.path().display());
|
||||||
|
info!("Header metainfo:");
|
||||||
|
print!("{}",String::from_utf8(p.header().metainfo_bytes())?);
|
||||||
|
}
|
||||||
|
return Ok(p.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(format_err!("No suitable install partition found"))
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user