Restructure project layout for better CI/CD integration
- Flattened nested bootupd/bootupd/ structure to root level - Moved all core project files to root directory - Added proper Debian packaging structure (debian/ directory) - Created build scripts and CI configuration - Improved project organization for CI/CD tools - All Rust source, tests, and configuration now at root level - Added GitHub Actions workflow for automated testing - Maintained all original functionality while improving structure
This commit is contained in:
parent
5e8730df43
commit
aaf662d5b1
87 changed files with 1334 additions and 570 deletions
222
src/cli/bootupctl.rs
Executable file
222
src/cli/bootupctl.rs
Executable file
|
|
@ -0,0 +1,222 @@
|
|||
use crate::bootupd;
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use log::LevelFilter;
|
||||
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
static SYSTEMD_ARGS_BOOTUPD: &[&str] = &["--unit", "bootupd", "--pipe"];
|
||||
|
||||
/// Keep these properties (isolation/runtime state) in sync with
|
||||
/// the systemd units in contrib/packaging/*.service
|
||||
static SYSTEMD_PROPERTIES: &[&str] = &[
|
||||
"PrivateNetwork=yes",
|
||||
"ProtectHome=yes",
|
||||
// While only our main process during update catches SIGTERM, we don't
|
||||
// want systemd to send it to other processes.
|
||||
"KillMode=mixed",
|
||||
"MountFlags=slave",
|
||||
];
|
||||
|
||||
/// `bootupctl` sub-commands.
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(name = "bootupctl", about = "Bootupd client application", version)]
|
||||
pub struct CtlCommand {
|
||||
/// Verbosity level (higher is more verbose).
|
||||
#[clap(short = 'v', action = clap::ArgAction::Count, global = true)]
|
||||
verbosity: u8,
|
||||
|
||||
/// CLI sub-command.
|
||||
#[clap(subcommand)]
|
||||
pub cmd: CtlVerb,
|
||||
}
|
||||
|
||||
impl CtlCommand {
|
||||
/// Return the log-level set via command-line flags.
|
||||
pub(crate) fn loglevel(&self) -> LevelFilter {
|
||||
match self.verbosity {
|
||||
0 => LevelFilter::Warn,
|
||||
1 => LevelFilter::Info,
|
||||
2 => LevelFilter::Debug,
|
||||
_ => LevelFilter::Trace,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// CLI sub-commands.
|
||||
#[derive(Debug, Parser)]
|
||||
pub enum CtlVerb {
|
||||
// FIXME(lucab): drop this after refreshing
|
||||
// https://github.com/coreos/fedora-coreos-config/pull/595
|
||||
#[clap(name = "backend", hide = true, subcommand)]
|
||||
Backend(CtlBackend),
|
||||
#[clap(name = "status", about = "Show components status")]
|
||||
Status(StatusOpts),
|
||||
#[clap(name = "update", about = "Update all components")]
|
||||
Update,
|
||||
#[clap(name = "adopt-and-update", about = "Update all adoptable components")]
|
||||
AdoptAndUpdate(AdoptAndUpdateOpts),
|
||||
#[clap(name = "validate", about = "Validate system state")]
|
||||
Validate,
|
||||
#[clap(
|
||||
name = "migrate-static-grub-config",
|
||||
hide = true,
|
||||
about = "Migrate a system to a static GRUB config"
|
||||
)]
|
||||
MigrateStaticGrubConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub enum CtlBackend {
|
||||
#[clap(name = "generate-update-metadata", hide = true)]
|
||||
Generate(super::bootupd::GenerateOpts),
|
||||
#[clap(name = "install", hide = true)]
|
||||
Install(super::bootupd::InstallOpts),
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct StatusOpts {
|
||||
/// If there are updates available, output `Updates available: ` to standard output;
|
||||
/// otherwise output nothing. Avoid parsing this, just check whether or not
|
||||
/// the output is empty.
|
||||
#[clap(long, action)]
|
||||
print_if_available: bool,
|
||||
|
||||
/// Output JSON
|
||||
#[clap(long, action)]
|
||||
json: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AdoptAndUpdateOpts {
|
||||
/// Install the static GRUB config files
|
||||
#[clap(long, action)]
|
||||
with_static_config: bool,
|
||||
}
|
||||
|
||||
impl CtlCommand {
|
||||
/// Run CLI application.
|
||||
pub fn run(self) -> Result<()> {
|
||||
match self.cmd {
|
||||
CtlVerb::Status(opts) => Self::run_status(opts),
|
||||
CtlVerb::Update => Self::run_update(),
|
||||
CtlVerb::AdoptAndUpdate(opts) => Self::run_adopt_and_update(opts),
|
||||
CtlVerb::Validate => Self::run_validate(),
|
||||
CtlVerb::Backend(CtlBackend::Generate(opts)) => {
|
||||
super::bootupd::DCommand::run_generate_meta(opts)
|
||||
}
|
||||
CtlVerb::Backend(CtlBackend::Install(opts)) => {
|
||||
super::bootupd::DCommand::run_install(opts)
|
||||
}
|
||||
CtlVerb::MigrateStaticGrubConfig => Self::run_migrate_static_grub_config(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Runner for `status` verb.
|
||||
fn run_status(opts: StatusOpts) -> Result<()> {
|
||||
if crate::util::running_in_container() {
|
||||
return run_status_in_container(opts.json);
|
||||
}
|
||||
ensure_running_in_systemd()?;
|
||||
let r = bootupd::status()?;
|
||||
if opts.json {
|
||||
let stdout = std::io::stdout();
|
||||
let mut stdout = stdout.lock();
|
||||
serde_json::to_writer_pretty(&mut stdout, &r)?;
|
||||
} else if opts.print_if_available {
|
||||
bootupd::print_status_avail(&r)?;
|
||||
} else {
|
||||
bootupd::print_status(&r)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runner for `update` verb.
|
||||
fn run_update() -> Result<()> {
|
||||
ensure_running_in_systemd()?;
|
||||
bootupd::client_run_update()
|
||||
}
|
||||
|
||||
/// Runner for `update` verb.
|
||||
fn run_adopt_and_update(opts: AdoptAndUpdateOpts) -> Result<()> {
|
||||
ensure_running_in_systemd()?;
|
||||
bootupd::client_run_adopt_and_update(opts.with_static_config)
|
||||
}
|
||||
|
||||
/// Runner for `validate` verb.
|
||||
fn run_validate() -> Result<()> {
|
||||
ensure_running_in_systemd()?;
|
||||
bootupd::client_run_validate()
|
||||
}
|
||||
|
||||
/// Runner for `migrate-static-grub-config` verb.
|
||||
fn run_migrate_static_grub_config() -> Result<()> {
|
||||
ensure_running_in_systemd()?;
|
||||
bootupd::client_run_migrate_static_grub_config()
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the current process is (apparently at least)
|
||||
/// running under systemd.
|
||||
fn running_in_systemd() -> bool {
|
||||
std::env::var_os("INVOCATION_ID").is_some()
|
||||
}
|
||||
|
||||
/// Require root permission
|
||||
fn require_root_permission() -> Result<()> {
|
||||
if !rustix::process::getuid().is_root() {
|
||||
anyhow::bail!("This command requires root privileges")
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Detect if we're running in systemd; if we're not, we re-exec ourselves via
|
||||
/// systemd-run. Then we can just directly run code in what is now the daemon.
|
||||
fn ensure_running_in_systemd() -> Result<()> {
|
||||
require_root_permission()?;
|
||||
let running_in_systemd = running_in_systemd();
|
||||
if !running_in_systemd {
|
||||
// Clear any failure status that may have happened previously
|
||||
let _r = Command::new("systemctl")
|
||||
.arg("reset-failed")
|
||||
.arg("bootupd.service")
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()?
|
||||
.wait()?;
|
||||
let r = Command::new("systemd-run")
|
||||
.args(SYSTEMD_ARGS_BOOTUPD)
|
||||
.args(
|
||||
SYSTEMD_PROPERTIES
|
||||
.into_iter()
|
||||
.flat_map(|&v| ["--property", v]),
|
||||
)
|
||||
.args(std::env::args())
|
||||
.exec();
|
||||
// If we got here, it's always an error
|
||||
return Err(r.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// If running in container, just print the available payloads
|
||||
fn run_status_in_container(json_format: bool) -> Result<()> {
|
||||
let all_components = crate::bootupd::get_components();
|
||||
if all_components.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let avail: Vec<_> = all_components.keys().cloned().collect();
|
||||
if json_format {
|
||||
let stdout = std::io::stdout();
|
||||
let mut stdout = stdout.lock();
|
||||
let output: serde_json::Value = serde_json::json!({
|
||||
"components": avail
|
||||
});
|
||||
serde_json::to_writer(&mut stdout, &output)?;
|
||||
} else {
|
||||
println!("Available components: {}", avail.join(" "));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
125
src/cli/bootupd.rs
Executable file
125
src/cli/bootupd.rs
Executable file
|
|
@ -0,0 +1,125 @@
|
|||
use crate::bootupd::{self, ConfigMode};
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use log::LevelFilter;
|
||||
|
||||
/// `bootupd` sub-commands.
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(name = "bootupd", about = "Bootupd backend commands", version)]
|
||||
pub struct DCommand {
|
||||
/// Verbosity level (higher is more verbose).
|
||||
#[clap(short = 'v', action = clap::ArgAction::Count, global = true)]
|
||||
verbosity: u8,
|
||||
|
||||
/// CLI sub-command.
|
||||
#[clap(subcommand)]
|
||||
pub cmd: DVerb,
|
||||
}
|
||||
|
||||
impl DCommand {
|
||||
/// Return the log-level set via command-line flags.
|
||||
pub(crate) fn loglevel(&self) -> LevelFilter {
|
||||
match self.verbosity {
|
||||
0 => LevelFilter::Warn,
|
||||
1 => LevelFilter::Info,
|
||||
2 => LevelFilter::Debug,
|
||||
_ => LevelFilter::Trace,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// CLI sub-commands.
|
||||
#[derive(Debug, Parser)]
|
||||
pub enum DVerb {
|
||||
#[clap(name = "generate-update-metadata", about = "Generate metadata")]
|
||||
GenerateUpdateMetadata(GenerateOpts),
|
||||
#[clap(name = "install", about = "Install components")]
|
||||
Install(InstallOpts),
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct InstallOpts {
|
||||
/// Source root
|
||||
#[clap(long, value_parser, default_value_t = String::from("/"))]
|
||||
src_root: String,
|
||||
/// Target root
|
||||
#[clap(value_parser)]
|
||||
dest_root: String,
|
||||
|
||||
/// Target device, used by bios bootloader installation
|
||||
#[clap(long)]
|
||||
device: Option<String>,
|
||||
|
||||
/// Enable installation of the built-in static config files
|
||||
#[clap(long)]
|
||||
with_static_configs: bool,
|
||||
|
||||
/// Implies `--with-static-configs`. When present, this also writes a
|
||||
/// file with the UUID of the target filesystems.
|
||||
#[clap(long)]
|
||||
write_uuid: bool,
|
||||
|
||||
/// On EFI systems, invoke `efibootmgr` to update the firmware.
|
||||
#[clap(long)]
|
||||
update_firmware: bool,
|
||||
|
||||
#[clap(long = "component", conflicts_with = "auto")]
|
||||
/// Only install these components
|
||||
components: Option<Vec<String>>,
|
||||
|
||||
/// Automatically choose components based on booted host state.
|
||||
///
|
||||
/// For example on x86_64, if the host system is booted via EFI,
|
||||
/// then only enable installation to the ESP.
|
||||
#[clap(long)]
|
||||
auto: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct GenerateOpts {
|
||||
/// Physical root mountpoint
|
||||
#[clap(value_parser)]
|
||||
sysroot: Option<String>,
|
||||
}
|
||||
|
||||
impl DCommand {
|
||||
/// Run CLI application.
|
||||
pub fn run(self) -> Result<()> {
|
||||
match self.cmd {
|
||||
DVerb::Install(opts) => Self::run_install(opts),
|
||||
DVerb::GenerateUpdateMetadata(opts) => Self::run_generate_meta(opts),
|
||||
}
|
||||
}
|
||||
|
||||
/// Runner for `generate-install-metadata` verb.
|
||||
pub(crate) fn run_generate_meta(opts: GenerateOpts) -> Result<()> {
|
||||
let sysroot = opts.sysroot.as_deref().unwrap_or("/");
|
||||
if sysroot != "/" {
|
||||
anyhow::bail!("Using a non-default sysroot is not supported: {}", sysroot);
|
||||
}
|
||||
bootupd::generate_update_metadata(sysroot).context("generating metadata failed")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runner for `install` verb.
|
||||
pub(crate) fn run_install(opts: InstallOpts) -> Result<()> {
|
||||
let configmode = if opts.write_uuid {
|
||||
ConfigMode::WithUUID
|
||||
} else if opts.with_static_configs {
|
||||
ConfigMode::Static
|
||||
} else {
|
||||
ConfigMode::None
|
||||
};
|
||||
bootupd::install(
|
||||
&opts.src_root,
|
||||
&opts.dest_root,
|
||||
opts.device.as_deref(),
|
||||
configmode,
|
||||
opts.update_firmware,
|
||||
opts.components.as_deref(),
|
||||
opts.auto,
|
||||
)
|
||||
.context("boot data installation failed")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
107
src/cli/mod.rs
Executable file
107
src/cli/mod.rs
Executable file
|
|
@ -0,0 +1,107 @@
|
|||
//! Command-line interface (CLI) logic.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use log::LevelFilter;
|
||||
mod bootupctl;
|
||||
mod bootupd;
|
||||
|
||||
/// Top-level multicall CLI.
|
||||
#[derive(Debug, Parser)]
|
||||
pub enum MultiCall {
|
||||
Ctl(bootupctl::CtlCommand),
|
||||
D(bootupd::DCommand),
|
||||
}
|
||||
|
||||
impl MultiCall {
|
||||
pub fn from_args(args: Vec<String>) -> Self {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
// This is a multicall binary, dispatched based on the introspected
|
||||
// filename found in argv[0].
|
||||
let exe_name = {
|
||||
let arg0 = args.get(0).cloned().unwrap_or_default();
|
||||
let exe_path = std::path::PathBuf::from(arg0);
|
||||
exe_path.file_name().unwrap_or_default().to_os_string()
|
||||
};
|
||||
#[allow(clippy::wildcard_in_or_patterns)]
|
||||
match exe_name.as_bytes() {
|
||||
b"bootupctl" => MultiCall::Ctl(bootupctl::CtlCommand::parse_from(args)),
|
||||
b"bootupd" | _ => MultiCall::D(bootupd::DCommand::parse_from(args)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(self) -> Result<()> {
|
||||
match self {
|
||||
MultiCall::Ctl(ctl_cmd) => ctl_cmd.run(),
|
||||
MultiCall::D(d_cmd) => d_cmd.run(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the log-level set via command-line flags.
|
||||
pub fn loglevel(&self) -> LevelFilter {
|
||||
match self {
|
||||
MultiCall::Ctl(cmd) => cmd.loglevel(),
|
||||
MultiCall::D(cmd) => cmd.loglevel(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn clap_apps() {
|
||||
use clap::CommandFactory;
|
||||
bootupctl::CtlCommand::command().debug_assert();
|
||||
bootupd::DCommand::command().debug_assert();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multicall_dispatch() {
|
||||
{
|
||||
let d_argv = vec![
|
||||
"/usr/bin/bootupd".to_string(),
|
||||
"generate-update-metadata".to_string(),
|
||||
];
|
||||
let cli = MultiCall::from_args(d_argv);
|
||||
match cli {
|
||||
MultiCall::Ctl(cmd) => panic!("{:?}", cmd),
|
||||
MultiCall::D(_) => {}
|
||||
};
|
||||
}
|
||||
{
|
||||
let ctl_argv = vec!["/usr/bin/bootupctl".to_string(), "validate".to_string()];
|
||||
let cli = MultiCall::from_args(ctl_argv);
|
||||
match cli {
|
||||
MultiCall::Ctl(_) => {}
|
||||
MultiCall::D(cmd) => panic!("{:?}", cmd),
|
||||
};
|
||||
}
|
||||
{
|
||||
let ctl_argv = vec!["/bin-mount/bootupctl".to_string(), "validate".to_string()];
|
||||
let cli = MultiCall::from_args(ctl_argv);
|
||||
match cli {
|
||||
MultiCall::Ctl(_) => {}
|
||||
MultiCall::D(cmd) => panic!("{:?}", cmd),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verbosity() {
|
||||
let default = MultiCall::from_args(vec![
|
||||
"bootupd".to_string(),
|
||||
"generate-update-metadata".to_string(),
|
||||
]);
|
||||
assert_eq!(default.loglevel(), LevelFilter::Warn);
|
||||
|
||||
let info = MultiCall::from_args(vec![
|
||||
"bootupd".to_string(),
|
||||
"generate-update-metadata".to_string(),
|
||||
"-v".to_string(),
|
||||
]);
|
||||
assert_eq!(info.loglevel(), LevelFilter::Info);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue