472 lines
15 KiB
Rust
472 lines
15 KiB
Rust
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());
|
|
util::copy_file(&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(())
|
|
}
|
|
util::read_directory(base_path, |dent| {
|
|
if let Some(fname) = dent.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)?;
|
|
util::write_file(self.path(), buffer)
|
|
}
|
|
|
|
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 util::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());
|
|
util::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;
|
|
}
|
|
util::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<()> {
|
|
util::remove_file(&self.path)
|
|
}
|
|
}
|
|
|
|
#[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));
|
|
} |