1
0
mirror of https://github.com/containers/bootc.git synced 2026-02-05 15:45:53 +01:00

Drop systemd service

Fixes https://github.com/coreos/bootupd/issues/551

Get hints by https://github.com/coreos/bootupd/issues/551#issuecomment-2124477922,
and copy the comment here:
Basically we 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.

I think an important aspect of this is that we retain something
like `--unit bootupd` which acts as a lock - only one unit with
that name can run at a time to avoid two concurrent invocations
breaking things.
This commit is contained in:
HuijingHei
2024-05-21 22:09:08 +08:00
parent 372d4a097e
commit 261fb5e7f2
14 changed files with 79 additions and 423 deletions

View File

@@ -53,6 +53,7 @@ jobs:
device=$(losetup --list --noheadings --output NAME,BACK-FILE | grep myimage.raw | awk '{print $1}')
sudo mount "${device}p2" /mnt/
sudo ls /mnt/EFI/centos/{grub.cfg,shimx64.efi}
sudo umount /mnt
sudo losetup -D "${device}"
sudo rm -f myimage.raw
- name: bootc install to filesystem

View File

@@ -18,10 +18,8 @@ ifeq ($(CONTAINER_RUNTIME), podman)
IMAGE_PREFIX = localhost/
endif
units = $(addprefix systemd/, bootupd.service bootupd.socket)
.PHONY: all
all: $(units)
all:
cargo build ${CARGO_ARGS}
ln -f target/${PROFILE}/bootupd target/${PROFILE}/bootupctl
@@ -33,17 +31,11 @@ create-build-container:
build-in-container: create-build-container
${CONTAINER_RUNTIME} run -ti --rm -v .:/srv/bootupd:z ${IMAGE_PREFIX}${IMAGE_NAME} make
.PHONY: install-units
install-units: $(units)
for unit in $(units); do install -D -m 644 --target-directory=$(DESTDIR)$(PREFIX)/lib/systemd/system/ $$unit; done
.PHONY: install
install: install-units
install:
mkdir -p "${DESTDIR}$(PREFIX)/bin" "${DESTDIR}$(LIBEXECDIR)"
install -D -t "${DESTDIR}$(LIBEXECDIR)" target/${PROFILE}/bootupd
ln -f ${DESTDIR}$(LIBEXECDIR)/bootupd ${DESTDIR}$(PREFIX)/bin/bootupctl
install -d "${DESTDIR}$(PREFIX)/lib/systemd/system/multi-user.target.wants"
ln -s ../bootupd.socket "${DESTDIR}$(PREFIX)/lib/systemd/system/multi-user.target.wants"
install-grub-static:
install -m 644 -D -t ${DESTDIR}$(PREFIX)/lib/bootupd/grub2-static src/grub2/*.cfg

View File

@@ -52,15 +52,6 @@ cargo build --release
%make_install INSTALL="install -p -c"
make install-grub-static DESTDIR=%{?buildroot} INSTALL="%{__install} -p"
%post -n %{crate}
%systemd_post bootupd.service bootupd.socket
%preun -n %{crate}
%systemd_preun bootupd.service bootupd.socket
%postun -n %{crate}
%systemd_postun bootupd.service bootupd.socket
%changelog
* Tue Oct 18 2022 Colin Walters <walters@verbum.org> - 0.2.8-3
- Dummy changelog

View File

@@ -1,31 +1,18 @@
#[cfg(any(target_arch = "x86_64", target_arch = "powerpc64"))]
use crate::bios;
use crate::component;
use crate::component::{Component, ValidationResult};
use crate::coreos;
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
use crate::efi;
use crate::model::{ComponentStatus, ComponentUpdatable, ContentMetadata, SavedState, Status};
use crate::util;
use crate::{component, ipc};
use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::path::Path;
/// A message sent from client to server
#[derive(Debug, Serialize, Deserialize)]
pub(crate) enum ClientRequest {
/// Update a component
Update { component: String },
/// Update a component via adoption
AdoptAndUpdate { component: String },
/// Validate a component
Validate { component: String },
/// Print the current state
Status,
}
pub(crate) enum ConfigMode {
None,
Static,
@@ -408,8 +395,8 @@ pub(crate) fn print_status(status: &Status) -> Result<()> {
Ok(())
}
pub(crate) fn client_run_update(c: &mut ipc::ClientToDaemonConnection) -> Result<()> {
let status: Status = c.send(&ClientRequest::Status)?;
pub(crate) fn client_run_update() -> Result<()> {
let status: Status = status()?;
if status.components.is_empty() && status.adoptable.is_empty() {
println!("No components installed.");
return Ok(());
@@ -420,9 +407,7 @@ pub(crate) fn client_run_update(c: &mut ipc::ClientToDaemonConnection) -> Result
ComponentUpdatable::Upgradable => {}
_ => continue,
};
match c.send(&ClientRequest::Update {
component: name.to_string(),
})? {
match update(name)? {
ComponentUpdateResult::AtLatestVersion => {
// Shouldn't happen unless we raced with another client
eprintln!(
@@ -450,9 +435,7 @@ pub(crate) fn client_run_update(c: &mut ipc::ClientToDaemonConnection) -> Result
}
for (name, adoptable) in status.adoptable.iter() {
if adoptable.confident {
let r: ContentMetadata = c.send(&ClientRequest::AdoptAndUpdate {
component: name.to_string(),
})?;
let r: ContentMetadata = adopt_and_update(name)?;
println!("Adopted and updated: {}: {}", name, r.version);
updated = true;
} else {
@@ -465,32 +448,28 @@ pub(crate) fn client_run_update(c: &mut ipc::ClientToDaemonConnection) -> Result
Ok(())
}
pub(crate) fn client_run_adopt_and_update(c: &mut ipc::ClientToDaemonConnection) -> Result<()> {
let status: Status = c.send(&ClientRequest::Status)?;
pub(crate) fn client_run_adopt_and_update() -> Result<()> {
let status: Status = status()?;
if status.adoptable.is_empty() {
println!("No components are adoptable.");
} else {
for (name, _) in status.adoptable.iter() {
let r: ContentMetadata = c.send(&ClientRequest::AdoptAndUpdate {
component: name.to_string(),
})?;
let r: ContentMetadata = adopt_and_update(name)?;
println!("Adopted and updated: {}: {}", name, r.version);
}
}
Ok(())
}
pub(crate) fn client_run_validate(c: &mut ipc::ClientToDaemonConnection) -> Result<()> {
let status: Status = c.send(&ClientRequest::Status)?;
pub(crate) fn client_run_validate() -> Result<()> {
let status: Status = status()?;
if status.components.is_empty() {
println!("No components installed.");
return Ok(());
}
let mut caught_validation_error = false;
for (name, _) in status.components.iter() {
match c.send(&ClientRequest::Validate {
component: name.to_string(),
})? {
match validate(name)? {
ValidationResult::Valid => {
println!("Validated: {}", name);
}

View File

@@ -1,10 +1,23 @@
use crate::bootupd;
use crate::ipc::ClientToDaemonConnection;
use crate::model::Status;
use anyhow::Result;
use clap::Parser;
use log::LevelFilter;
use std::os::unix::process::CommandExt;
use std::process::Command;
static SYSTEMD_ARGS_BOOTUPD: &[&str] = &[
"--unit",
"bootupd",
"--property",
"PrivateNetwork=yes",
"--property",
"ProtectHome=yes",
"--property",
"MountFlags=slave",
"--pipe",
];
/// `bootupctl` sub-commands.
#[derive(Debug, Parser)]
#[clap(name = "bootupctl", about = "Bootupd client application", version)]
@@ -87,10 +100,8 @@ impl CtlCommand {
/// Runner for `status` verb.
fn run_status(opts: StatusOpts) -> Result<()> {
let mut client = ClientToDaemonConnection::new();
client.connect()?;
let r: Status = client.send(&bootupd::ClientRequest::Status)?;
ensure_running_in_systemd()?;
let r = bootupd::status()?;
if opts.json {
let stdout = std::io::stdout();
let mut stdout = stdout.lock();
@@ -101,38 +112,54 @@ impl CtlCommand {
bootupd::print_status(&r)?;
}
client.shutdown()?;
Ok(())
}
/// Runner for `update` verb.
fn run_update() -> Result<()> {
let mut client = ClientToDaemonConnection::new();
client.connect()?;
bootupd::client_run_update(&mut client)?;
client.shutdown()?;
Ok(())
ensure_running_in_systemd()?;
bootupd::client_run_update()
}
/// Runner for `update` verb.
fn run_adopt_and_update() -> Result<()> {
let mut client = ClientToDaemonConnection::new();
client.connect()?;
bootupd::client_run_adopt_and_update(&mut client)?;
client.shutdown()?;
Ok(())
ensure_running_in_systemd()?;
bootupd::client_run_adopt_and_update()
}
/// Runner for `validate` verb.
fn run_validate() -> Result<()> {
let mut client = ClientToDaemonConnection::new();
client.connect()?;
bootupd::client_run_validate(&mut client)?;
client.shutdown()?;
Ok(())
ensure_running_in_systemd()?;
bootupd::client_run_validate()
}
}
/// 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 !nix::unistd::Uid::effective().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 {
let r = Command::new("systemd-run")
.args(SYSTEMD_ARGS_BOOTUPD)
.args(std::env::args())
.exec();
// If we got here, it's always an error
return Err(r.into());
}
Ok(())
}

View File

@@ -31,8 +31,6 @@ impl DCommand {
/// CLI sub-commands.
#[derive(Debug, Parser)]
pub enum DVerb {
#[clap(name = "daemon", about = "Run service logic")]
Daemon,
#[clap(name = "generate-update-metadata", about = "Generate metadata")]
GenerateUpdateMetadata(GenerateOpts),
#[clap(name = "install", about = "Install components")]
@@ -88,7 +86,6 @@ impl DCommand {
/// Run CLI application.
pub fn run(self) -> Result<()> {
match self.cmd {
DVerb::Daemon => crate::daemon::run(),
DVerb::Install(opts) => Self::run_install(opts),
DVerb::GenerateUpdateMetadata(opts) => Self::run_generate_meta(opts),
}

View File

@@ -62,7 +62,10 @@ mod tests {
#[test]
fn test_multicall_dispatch() {
{
let d_argv = vec!["/usr/bin/bootupd".to_string(), "daemon".to_string()];
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),
@@ -89,12 +92,15 @@ mod tests {
#[test]
fn test_verbosity() {
let default = MultiCall::from_args(vec!["bootupd".to_string(), "daemon".to_string()]);
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(),
"daemon".to_string(),
"generate-update-metadata".to_string(),
"-v".to_string(),
]);
assert_eq!(info.loglevel(), LevelFilter::Info);

View File

@@ -1,130 +0,0 @@
//! Daemon logic.
use crate::component::ValidationResult;
use crate::model::Status;
use crate::{bootupd, ipc};
use anyhow::{bail, Context, Result};
use nix::sys::socket as nixsocket;
use std::os::unix::io::RawFd;
/// Accept a single client and then exit; we don't want to
/// persistently run as a daemon. The systemd unit is mostly
/// and implementation detail - it lets us use things like
/// systemd's built in sandboxing (ProtectHome=yes) etc. and also
/// ensures that only a single bootupd instance is running at
/// a time (i.e. don't support concurrent updates).
pub fn run() -> Result<()> {
let srvsock_fd = systemd_activation().context("systemd service activation error")?;
// Accept an incoming client.
let client = match accept_authenticate_client(srvsock_fd) {
Ok(auth_client) => auth_client,
Err(e) => {
log::error!("failed to authenticate client: {}", e);
return Ok(());
}
};
// Process all requests from this client.
if let Err(e) = process_client_requests(client) {
log::error!("failed to process request from client: {}", e);
}
// Sleep for a half second to avoid triggering systemd service
// restart limits.
std::thread::sleep(std::time::Duration::from_secs_f32(0.5));
Ok(())
}
/// Perform initialization steps required by systemd service activation.
///
/// This ensures that the system is running under systemd, then receives the
/// socket-FD for main IPC logic, and notifies systemd about ready-state.
fn systemd_activation() -> Result<RawFd> {
use libsystemd::daemon::{self, NotifyState};
use std::os::unix::io::IntoRawFd;
if !daemon::booted() {
bail!("daemon is not running as a systemd service");
}
let srvsock_fd = {
let mut fds = libsystemd::activation::receive_descriptors(true)
.map_err(|e| anyhow::anyhow!("failed to receive file-descriptors: {}", e))?;
let srvsock_fd = if let Some(fd) = fds.pop() {
fd
} else {
bail!("no socket-fd received on service activation");
};
srvsock_fd.into_raw_fd()
};
let sent = daemon::notify(true, &[NotifyState::Ready])
.map_err(|e| anyhow::anyhow!("failed to notify ready-state: {}", e))?;
if !sent {
log::warn!("failed to notify ready-state: service notifications not supported");
}
Ok(srvsock_fd)
}
/// Accept an incoming connection, then authenticate the client.
fn accept_authenticate_client(srvsock_fd: RawFd) -> Result<ipc::AuthenticatedClient> {
let accepted = nixsocket::accept4(srvsock_fd, nixsocket::SockFlag::SOCK_CLOEXEC)?;
let client = ipc::UnauthenticatedClient::new(accepted);
let authed = client.authenticate()?;
Ok(authed)
}
/// Process all requests from a given client.
///
/// This sequentially processes all requests from a client, until it
/// disconnects or a connection error is encountered.
fn process_client_requests(client: ipc::AuthenticatedClient) -> Result<()> {
use crate::bootupd::ClientRequest;
let mut buf = [0u8; ipc::MSGSIZE];
loop {
let n = nixsocket::recv(client.fd, &mut buf, nixsocket::MsgFlags::MSG_CMSG_CLOEXEC)?;
let buf = &buf[0..n];
if buf.is_empty() {
log::trace!("client disconnected");
break;
}
let msg = bincode::deserialize(buf)?;
log::trace!("processing request: {:?}", &msg);
let r = match msg {
ClientRequest::Update { component } => {
bincode::serialize(&match bootupd::update(component.as_str()) {
Ok(v) => ipc::DaemonToClientReply::Success::<bootupd::ComponentUpdateResult>(v),
Err(e) => ipc::DaemonToClientReply::Failure(format!("{:#}", e)),
})?
}
ClientRequest::AdoptAndUpdate { component } => {
bincode::serialize(&match bootupd::adopt_and_update(component.as_str()) {
Ok(v) => ipc::DaemonToClientReply::Success::<crate::model::ContentMetadata>(v),
Err(e) => ipc::DaemonToClientReply::Failure(format!("{:#}", e)),
})?
}
ClientRequest::Validate { component } => {
bincode::serialize(&match bootupd::validate(component.as_str()) {
Ok(v) => ipc::DaemonToClientReply::Success::<ValidationResult>(v),
Err(e) => ipc::DaemonToClientReply::Failure(format!("{:#}", e)),
})?
}
ClientRequest::Status => bincode::serialize(&match bootupd::status() {
Ok(v) => ipc::DaemonToClientReply::Success::<Status>(v),
Err(e) => ipc::DaemonToClientReply::Failure(format!("{:#}", e)),
})?,
};
let written = nixsocket::send(client.fd, &r, nixsocket::MsgFlags::MSG_CMSG_CLOEXEC)?;
if written != r.len() {
bail!("wrote {} bytes to client, expected {}", written, r.len());
}
}
Ok(())
}

View File

@@ -1,169 +0,0 @@
/*
* Copyright (C) 2020 Red Hat, Inc.
*
* SPDX-License-Identifier: Apache-2.0
*/
use anyhow::{bail, Context, Result};
use fn_error_context::context;
use nix::sys::socket as nixsocket;
use serde::{Deserialize, Serialize};
use std::os::unix::io::RawFd;
pub(crate) const BOOTUPD_SOCKET: &str = "/run/bootupd.sock";
pub(crate) const MSGSIZE: usize = 1_048_576;
/// Sent between processes along with SCM credentials
pub(crate) const BOOTUPD_HELLO_MSG: &str = "bootupd-hello\n";
#[derive(Debug, Serialize, Deserialize)]
pub(crate) enum DaemonToClientReply<T> {
Success(T),
Failure(String),
}
pub(crate) struct ClientToDaemonConnection {
fd: i32,
}
impl Drop for ClientToDaemonConnection {
fn drop(&mut self) {
if self.fd != -1 {
nix::unistd::close(self.fd).expect("close");
}
}
}
impl ClientToDaemonConnection {
pub(crate) fn new() -> Self {
Self { fd: -1 }
}
#[context("connecting to {}", BOOTUPD_SOCKET)]
pub(crate) fn connect(&mut self) -> Result<()> {
use nix::sys::uio::IoVec;
self.fd = nixsocket::socket(
nixsocket::AddressFamily::Unix,
nixsocket::SockType::SeqPacket,
nixsocket::SockFlag::SOCK_CLOEXEC,
None,
)?;
let addr = nixsocket::SockAddr::new_unix(BOOTUPD_SOCKET)?;
nixsocket::connect(self.fd, &addr)?;
let creds = libc::ucred {
pid: nix::unistd::getpid().as_raw(),
uid: nix::unistd::getuid().as_raw(),
gid: nix::unistd::getgid().as_raw(),
};
let creds = nixsocket::UnixCredentials::from(creds);
let creds = nixsocket::ControlMessage::ScmCredentials(&creds);
let _ = nixsocket::sendmsg(
self.fd,
&[IoVec::from_slice(BOOTUPD_HELLO_MSG.as_bytes())],
&[creds],
nixsocket::MsgFlags::MSG_CMSG_CLOEXEC,
None,
)?;
Ok(())
}
pub(crate) fn send<S: serde::ser::Serialize, T: serde::de::DeserializeOwned>(
&mut self,
msg: &S,
) -> Result<T> {
{
let serialized = bincode::serialize(msg)?;
let _ = nixsocket::send(self.fd, &serialized, nixsocket::MsgFlags::MSG_CMSG_CLOEXEC)
.context("client sending request")?;
}
let reply: DaemonToClientReply<T> = {
let mut buf = [0u8; MSGSIZE];
let n = nixsocket::recv(self.fd, &mut buf, nixsocket::MsgFlags::MSG_CMSG_CLOEXEC)
.context("client recv")?;
let buf = &buf[0..n];
if buf.is_empty() {
bail!("Server sent an empty reply");
}
bincode::deserialize(buf).context("client parsing reply")?
};
match reply {
DaemonToClientReply::Success::<T>(r) => Ok(r),
DaemonToClientReply::Failure(buf) => {
// For now we just prefix server
anyhow::bail!("internal error: {}", buf);
}
}
}
pub(crate) fn shutdown(&mut self) -> Result<()> {
nixsocket::shutdown(self.fd, nixsocket::Shutdown::Both)?;
Ok(())
}
}
pub(crate) struct UnauthenticatedClient {
fd: RawFd,
}
impl UnauthenticatedClient {
pub(crate) fn new(fd: RawFd) -> Self {
Self { fd }
}
pub(crate) fn authenticate(mut self) -> Result<AuthenticatedClient> {
use nix::sys::uio::IoVec;
let fd = self.fd;
let mut buf = [0u8; 1024];
nixsocket::setsockopt(fd, nix::sys::socket::sockopt::PassCred, &true)?;
let iov = IoVec::from_mut_slice(buf.as_mut());
let mut cmsgspace = nix::cmsg_space!(nixsocket::UnixCredentials);
let msg = nixsocket::recvmsg(
fd,
&[iov],
Some(&mut cmsgspace),
nixsocket::MsgFlags::MSG_CMSG_CLOEXEC,
)?;
let mut creds = None;
for cmsg in msg.cmsgs() {
if let nixsocket::ControlMessageOwned::ScmCredentials(c) = cmsg {
creds = Some(c);
break;
}
}
if let Some(creds) = creds {
if creds.uid() != 0 {
bail!("unauthorized pid:{} uid:{}", creds.pid(), creds.uid())
}
println!("Connection from pid:{}", creds.pid());
} else {
bail!("No SCM credentials provided");
}
let hello = String::from_utf8_lossy(&buf[0..msg.bytes]);
if hello != BOOTUPD_HELLO_MSG {
bail!("Didn't receive correct hello message, found: {:?}", &hello);
}
let r = AuthenticatedClient { fd: self.fd };
self.fd = -1;
Ok(r)
}
}
impl Drop for UnauthenticatedClient {
fn drop(&mut self) {
if self.fd != -1 {
nix::unistd::close(self.fd).expect("close");
}
}
}
pub(crate) struct AuthenticatedClient {
pub(crate) fd: RawFd,
}
impl Drop for AuthenticatedClient {
fn drop(&mut self) {
if self.fd != -1 {
nix::unistd::close(self.fd).expect("close");
}
}
}

View File

@@ -22,7 +22,6 @@ mod bootupd;
mod cli;
mod component;
mod coreos;
mod daemon;
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
mod efi;
mod filesystem;
@@ -33,7 +32,6 @@ mod filetree;
target_arch = "powerpc64"
))]
mod grubconfigs;
mod ipc;
mod model;
mod model_legacy;
mod ostreeutil;

View File

@@ -1,26 +0,0 @@
[Unit]
Description=bootloader update daemon
Documentation=https://github.com/coreos/bootupd
# Because the daemon currently agressively auto-exits
# and our test suite runs many requests, let's allow
# a lot of restarts before failing.
StartLimitIntervalSec=2s
StartLimitBurst=10
[Service]
Type=notify
Environment=BOOTUPD_VERBOSITY="-v"
ExecStart=/usr/libexec/bootupd daemon $BOOTUPD_VERBOSITY
# This way our working directory isn't writable by default.
WorkingDirectory=/usr
# Various hardening flags just on general principle. We need
# to run as root, but let's avoid accidental damage.
ProtectHome=yes
ReadOnlyPaths=/usr
PrivateTmp=yes
PrivateNetwork=yes
ProtectHostname=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
# So we can remount /boot writable
MountFlags=slave

View File

@@ -1,6 +0,0 @@
[Socket]
ListenSequentialPacket=/run/bootupd.sock
SocketMode=0600
[Install]
WantedBy=sockets.target

View File

@@ -16,7 +16,8 @@ if test -z "${COSA_DIR:-}"; then
fi
# Validate source directory
bootupd_git=$(cd ${dn} && git rev-parse --show-toplevel)
test -f ${bootupd_git}/systemd/bootupd.service
# https://github.com/coreos/bootupd/issues/551
! test -f ${bootupd_git}/systemd/bootupd.service
testtmp=$(mktemp -d -p /var/tmp bootupd-e2e.XXXXXXX)
export test_tmpdir=${testtmp}

View File

@@ -47,18 +47,13 @@ assert_file_has_content_literal out.txt 'Update: At latest version'
assert_file_has_content out.txt '^CoreOS aleph version:'
ok status
# Validate we auto-exited
sleep 2
systemctl show -p ActiveState bootupd > out.txt
assert_file_has_content_literal out.txt 'ActiveState=inactive'
bootupctl validate | tee out.txt
ok validate
if env LANG=C.UTF-8 runuser -u bin bootupctl status 2>err.txt; then
fatal "Was able to bootupctl status as non-root"
fi
assert_file_has_content err.txt 'error:.*: Permission denied'
assert_file_has_content err.txt 'error: This command requires root privileges'
# From here we'll fake updates
test -w /usr || rpm-ostree usroverlay