diff --git a/modules/bootc-hooks/README.md b/modules/bootc-hooks/README.md new file mode 100644 index 00000000..49b841f5 --- /dev/null +++ b/modules/bootc-hooks/README.md @@ -0,0 +1,103 @@ +# bootc-hooks Module + +The `bootc-hooks` module provides a flexible way to run custom scripts at different stages of the system lifecycle, especially in relation to `bootc` image updates and switches. + +## How It Works + +This module sets up two separate `systemd` services that run sequentially to execute scripts in different contexts: + +- `system-bootc-hooks.service`: Runs system-wide hooks as the `root` user. +- `user-bootc-hooks.service`: Runs user-specific hooks as the logged-in user. This service is configured to always run **after** the system service has completed. + +The core logic resides in two scripts: +- `/usr/libexec/bootc-hooks/run-system-bootc-hooks.sh` +- `/usr/libexec/bootc-hooks/run-user-bootc-hooks.sh` + +Here's the execution flow for each service: + +**1. System Hooks Execution (`run-system-bootc-hooks.sh`)** +- First, it rotates the versioning files. The existing `/var/lib/bootc-hooks/version.yaml` is renamed to `/var/lib/bootc-hooks/version.previous.yaml`. +- It then calls `bootc status` to get the current boot's image name and digest. +- This new information is written to a fresh `/var/lib/bootc-hooks/version.yaml`. +- Finally, it compares `version.yaml` (current) with `version.previous.yaml` (past) to determine if `update` or `switch` hooks need to be triggered. + +**2. User Hooks Execution (`run-user-bootc-hooks.sh`)** +- This script does **not** call `bootc status` because that command requires privileged permissions. Instead, it relies on the files generated by the system service to ensure consistency and work without elevated privileges. +- It reads the previous boot information from `/var/lib/bootc-hooks/version.previous.yaml`. +- It reads the current boot information from `/var/lib/bootc-hooks/version.yaml`. +- It compares the two versions to determine if its own `update` or `switch` hooks need to be triggered. + +This approach ensures that user hooks are not only executed after system hooks but also base their logic on the exact same version information. + +## Supported Hooks + +The module organizes hooks by **scope** (`system` or `user`) and then by **event**. The three supported events are: + +- **`boot`**: Scripts for this event are executed on every single boot (for system) or login (for user). +- **`update`**: These scripts run only when the digest of the `bootc` image has changed, indicating an image update has been applied. +- **`switch`**: These scripts are triggered when the image name itself has changed, which happens when rebasing to a different image entirely. + +Scripts are executed from the following directories within `/usr/libexec/bootc-hooks/`: +- `system/boot/` +- `system/update/` +- `system/switch/` +- `user/boot/` +- `user/update/` +- `user/switch/` + +## Usage + +To use this module, you need to enable it in your `recipe.yml` and configure which scripts to run for each hook. The module supports two contexts (`system` and `user`) and three events (`boot`, `update`, `switch`). + +All your hook scripts should be placed in the `files/scripts/` directory. In your `recipe.yml`, you will specify which script file to run for each hook event. + +### Example + +Let's say you want to run a system script on every boot and a user-specific script when a user logs in. + +1. Add your scripts to the `files/scripts/` directory. For example: + * `files/scripts/system_boot.sh` + * `files/scripts/user_boot.sh` + +2. Configure the `bootc-hooks` module in your `recipe.yml` to execute these scripts: + + ```yaml + modules: + - type: bootc-hooks + system: + boot: + # This script will run as root on every boot + - system_boot.sh + user: + boot: + # This script will run as the user on every login + - user_boot.sh + ``` + +During the image build process, the module will copy these scripts to the appropriate execution directories in `/usr/libexec/bootc-hooks/` and set up the `systemd` services to trigger them at the correct time. + +## Debugging + +The hooks are executed by the `system-bootc-hooks.service` and `user-bootc-hooks.service` `systemd` services. You can check their status and logs to debug your scripts. + +### System Hooks + +- **Check service status**: + ```bash + systemctl status system-bootc-hooks.service + ``` +- **View logs**: + ```bash + journalctl -u system-bootc-hooks.service + ``` + +### User Hooks + +- **Check service status**: + ```bash + systemctl --user status user-bootc-hooks.service + ``` +- **View logs**: + ```bash + journalctl --user -u user-bootc-hooks.service + ``` \ No newline at end of file diff --git a/modules/bootc-hooks/bootc-hooks.sh b/modules/bootc-hooks/bootc-hooks.sh new file mode 100644 index 00000000..86b097f8 --- /dev/null +++ b/modules/bootc-hooks/bootc-hooks.sh @@ -0,0 +1,84 @@ +#!/bin/bash +echo "Running bootc-hooks module" + +set -euo pipefail + +# Define paths +SYSTEM_SCRIPT_NAME="run-system-bootc-hooks.sh" +USER_SCRIPT_NAME="run-user-bootc-hooks.sh" +MODULE_DIR="$MODULE_DIRECTORY/bootc-hooks" +LIBEXEC_DIR="/usr/libexec/bootc-hooks" +SYSTEM_SERVICE_NAME="system-bootc-hooks.service" +USER_SERVICE_NAME="user-bootc-hooks.service" +SYSTEM_SERVICE_FILE="/usr/lib/systemd/system/$SYSTEM_SERVICE_NAME" +USER_SERVICE_FILE="/usr/lib/systemd/user/$USER_SERVICE_NAME" + +# Copy the scripts to /usr/libexec +echo "Copying script runners" +install -Dm 0755 "$MODULE_DIR/$SYSTEM_SCRIPT_NAME" "$LIBEXEC_DIR/$SYSTEM_SCRIPT_NAME" +install -Dm 0755 "$MODULE_DIR/$USER_SCRIPT_NAME" "$LIBEXEC_DIR/$USER_SCRIPT_NAME" + +# Create the system systemd service file +echo "Creating services" +cat <"$SYSTEM_SERVICE_FILE" +[Unit] +Description=Run bootc hooks after boot +Requires=network-online.target +After=network-online.target multi-user.target + +[Service] +Type=oneshot +ExecStart=$LIBEXEC_DIR/$SYSTEM_SCRIPT_NAME + +[Install] +WantedBy=multi-user.target +EOF + +# Create the user systemd service file +cat <"$USER_SERVICE_FILE" +[Unit] +Description=Run user bootc hooks after login +After=default.target system-bootc-hooks.service + +[Service] +Type=oneshot +ExecStart=$LIBEXEC_DIR/$USER_SCRIPT_NAME + +[Install] +WantedBy=default.target +EOF + +# Enable the service +systemctl -f enable $SYSTEM_SERVICE_NAME +systemctl -f --global enable $USER_SERVICE_NAME + +echo "Copying hooks" +SCRIPT_DIR="${CONFIG_DIRECTORY}/scripts" +mkdir -p /usr/libexec/bootc-hooks/{system,user}/{boot,switch,update} + +for scope in system user; do + for event in boot update switch; do + query=".${scope}.${event}[]?" + declare -a scripts_array=() + get_json_array scripts_array "${query}" "$1" + if [ ${#scripts_array[@]} -gt 0 ]; then + dest_dir="${LIBEXEC_DIR}/${scope}/${event}" + echo "Copying ${#scripts_array[@]} scripts for ${scope}/${event} hook..." + for script in "${scripts_array[@]}"; do + script_path="${SCRIPT_DIR}/${script}" + if [ -f "$script_path" ]; then + echo " - ${script}" + install -m 0755 "$script_path" "$dest_dir/" + else + echo "Warning: Script '${script}' for ${scope}/${event} hook not found in ${SCRIPT_DIR}" >&2 + fi + done + fi + done +done + +chmod -R +x /usr/libexec/bootc-hooks + +# Install dependencies +echo "Install yq dependency" +dnf -y install yq diff --git a/modules/bootc-hooks/bootc-hooks.tsp b/modules/bootc-hooks/bootc-hooks.tsp new file mode 100644 index 00000000..524f7796 --- /dev/null +++ b/modules/bootc-hooks/bootc-hooks.tsp @@ -0,0 +1,27 @@ +import "@typespec/json-schema"; +using TypeSpec.JsonSchema; + +model HookEvents { + /** Actions to run on boot. */ + boot?: string[]; + /** Actions to run on OS update. */ + update?: string[]; + /** Actions to run on OS switch (rollback). */ + switch?: string[]; +} + +@jsonSchema("/modules/bootc-hooks-latest.json") +model BootcHooksModuleLatest { + ...BootcHooksModuleV1 +} +@jsonSchema("/modules/bootc-hooks-v1.json") +model BootcHooksModuleV1 { + /** + * The bootc-hooks module adds a service that runs scripts based on bootc events. + */ + type: "bootc-hooks" | "bootc-hooks@v1" | "bootc-hooks@latest"; + /** Hooks to run in the system context. */ + system?: HookEvents; + /** Hooks to run in the user context. */ + user?: HookEvents; +} diff --git a/modules/bootc-hooks/module.yml b/modules/bootc-hooks/module.yml new file mode 100644 index 00000000..25a05e1e --- /dev/null +++ b/modules/bootc-hooks/module.yml @@ -0,0 +1,18 @@ +name: bootc-hooks +shortdesc: The bootc-hooks module allows to run scripts on boot, when image updated or switched +example: | + type: bootc-hooks + system: + boot: + - script-to-run-on-boot.sh + update: + - script-to-run-on-update.sh + switch: + - script-to-run-on-switch.sh + user: + boot: + - user-script-on-boot.sh + update: + - user-script-on-update.sh + switch: + - user-script-on-switch.sh diff --git a/modules/bootc-hooks/run-system-bootc-hooks.sh b/modules/bootc-hooks/run-system-bootc-hooks.sh new file mode 100644 index 00000000..28759014 --- /dev/null +++ b/modules/bootc-hooks/run-system-bootc-hooks.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -euo pipefail + +VERSION_DIR="/var/lib/bootc-hooks" +VERSION_FILE="$VERSION_DIR/version.yaml" +PREVIOUS_VERSION_FILE="$VERSION_DIR/version.previous.yaml" +SWITCH_HOOKS_DIR="/usr/libexec/bootc-hooks/system/switch" +UPDATE_HOOKS_DIR="/usr/libexec/bootc-hooks/system/update" +BOOT_HOOKS_DIR="/usr/libexec/bootc-hooks/system/boot" + +# Create the directory if it doesn't exist +sudo mkdir -p "$VERSION_DIR" + +# Rotate version files +if [ -f "$VERSION_FILE" ]; then + echo "Moving old version file to $PREVIOUS_VERSION_FILE" + sudo mv "$VERSION_FILE" "$PREVIOUS_VERSION_FILE" +fi + +old_image="" +old_digest="" +if [ -f "$PREVIOUS_VERSION_FILE" ]; then + echo "Reading previous image and digest from $PREVIOUS_VERSION_FILE" + old_image=$(yq e '.image' "$PREVIOUS_VERSION_FILE") + old_digest=$(yq e '.digest' "$PREVIOUS_VERSION_FILE") + echo "Previous Image: ${old_image}" + echo "Previous Digest: ${old_digest}" +fi + +output=$(bootc status --format yaml --booted) + +new_image=$(echo "$output" | yq e '.status.booted.image.image.image') +new_digest=$(echo "$output" | yq e '.status.booted.image.imageDigest') + +# Create the YAML content +yaml_content=$(cat < /dev/null + +if [ "${new_image}" != "${old_image}" ]; then + echo "Image has changed. Running switch hooks." + if [ -d "$SWITCH_HOOKS_DIR" ]; then + for hook in "$SWITCH_HOOKS_DIR"/*; do + if [ -x "$hook" ]; then + echo "Running hook: $hook" + "$hook" + fi + done + fi +fi + +if [ "${new_digest}" != "${old_digest}" ]; then + echo "Digest has changed. Running update hooks." + if [ -d "$UPDATE_HOOKS_DIR" ]; then + for hook in "$UPDATE_HOOKS_DIR"/*; do + if [ -x "$hook" ]; then + echo "Running hook: $hook" + "$hook" + fi + done + fi +fi + +echo "Running boot hooks." +if [ -d "$BOOT_HOOKS_DIR" ]; then + for hook in "$BOOT_HOOKS_DIR"/*; do + if [ -x "$hook" ]; then + echo "Running hook: $hook" + "$hook" + fi + done +fi diff --git a/modules/bootc-hooks/run-user-bootc-hooks.sh b/modules/bootc-hooks/run-user-bootc-hooks.sh new file mode 100644 index 00000000..b90eb568 --- /dev/null +++ b/modules/bootc-hooks/run-user-bootc-hooks.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Define paths +VERSION_FILE="/var/lib/bootc-hooks/version.yaml" +PREVIOUS_VERSION_FILE="/var/lib/bootc-hooks/version.previous.yaml" +SWITCH_HOOKS_DIR="/usr/libexec/bootc-hooks/user/switch" +UPDATE_HOOKS_DIR="/usr/libexec/bootc-hooks/user/update" +BOOT_HOOKS_DIR="/usr/libexec/bootc-hooks/user/boot" + +old_image="" +old_digest="" +if [ -f "$PREVIOUS_VERSION_FILE" ]; then + echo "Reading previous image and digest from $PREVIOUS_VERSION_FILE" + old_image=$(yq e '.image' "$PREVIOUS_VERSION_FILE") + old_digest=$(yq e '.digest' "$PREVIOUS_VERSION_FILE") + echo "Previous User Image: ${old_image}" + echo "Previous User Digest: ${old_digest}" +fi + +new_image="" +new_digest="" +if [ -f "$VERSION_FILE" ]; then + echo "Reading new image and digest from $VERSION_FILE" + new_image=$(yq e '.image' "$VERSION_FILE") + new_digest=$(yq e '.digest' "$VERSION_FILE") + echo "New User Image: ${new_image}" + echo "New User Digest: ${new_digest}" +fi + +# --- Run Hooks --- + +# Run switch hooks if the image has changed +if [ "${new_image}" != "${old_image}" ]; then + echo "Image has changed for user. Running switch hooks." + if [ -d "$SWITCH_HOOKS_DIR" ]; then + for hook in "$SWITCH_HOOKS_DIR"/*; do + if [ -x "$hook" ]; then + echo "Running user switch hook: $hook" + "$hook" + fi + done + fi +fi + +# Run update hooks if the digest has changed +if [ "${new_digest}" != "${old_digest}" ]; then + echo "Digest has changed for user. Running update hooks." + if [ -d "$UPDATE_HOOKS_DIR" ]; then + for hook in "$UPDATE_HOOKS_DIR"/*; do + if [ -x "$hook" ]; then + echo "Running user update hook: $hook" + "$hook" + fi + done + fi +fi + +# Always run boot hooks +echo "Running user boot hooks." +if [ -d "$BOOT_HOOKS_DIR" ]; then + for hook in "$BOOT_HOOKS_DIR"/*; do + if [ -x "$hook" ]; + then + echo "Running user boot hook: $hook" + "$hook" + fi + done +fi + +echo "User bootc hooks finished."