install: Add --preserve-var and --merge-etc to to-existing-root - #2384
install: Add --preserve-var and --merge-etc to to-existing-root#2384ckyrouac wants to merge 2 commits into
Conversation
Two robustness fixes surfaced while exercising the 3-way merge against real package-mode `/etc` trees (see the upcoming `--merge-etc` install flag): - In merge_leaf(), when copying a modified file into the new /etc, skip (with a warning) rather than fail when the cap-std copy hits "a path led outside of the filesystem". This happens when a path component in the current /etc leads through an absolute symlink that escapes the cap-std root (e.g. /etc/alternatives symlinks); the image's existing content wins for that path instead of aborting the whole merge. Clarify the comment on the analogous lstat-based removed-path check for consistency. - Warn and skip, instead of bailing, when a modified host file/symlink newly defaults to a directory in the target image (e.g. /etc/ssl/certs is a symlink in Fedora package mode but a directory in the bootc base image). The image's directory wins and the host's customization is dropped for that path. Update the file-to-directory unit test to match the new behavior. Assisted-by: AI
Add two flags to `bootc install to-existing-root` that are most useful
when migrating a package-mode system to a bootc image built externally:
--preserve-var Copy the running system's /var data into the new
deployment, and write a package-mode GRUB rollback
boot entry.
--merge-etc Apply the running system's /etc customisations onto
the new deployment via a 3-way merge.
Typical usage:
podman run --rm --privileged --pid=host --user=root:root \
-v /dev:/dev --security-opt label=type:unconfined_t \
-v /:/target -v /var/lib/containers:/var/lib/containers \
<fleet-image> \
bootc install to-existing-root \
--preserve-var \
--merge-etc \
--acknowledge-destructive
The implementation lives in the new `install/migrate` module; see its
module-level docs for the on-disk layout and the /var and /etc
migration strategies (reflink copy vs. plain copy, the pkgmode-rollback
BLS entry, and the etc-merge 3-way merge). install_to_existing_root()
extracts the two flags before opts is consumed, stashes the running
kernel/initramfs ahead of install_to_filesystem() when --preserve-var
is set (since that wipes /boot), and runs the post-install migration
steps afterward.
migrate.rs includes unit tests covering the pkgmode-rollback path
layout and the new /var path derivation from the deployment dir.
Assisted-by: AI
| // "a path led outside of the filesystem". Warn and skip rather than aborting the | ||
| // whole merge; the image's existing content wins for that path. | ||
| if let Err(ref e) = copy_result { | ||
| if e.to_string().contains("a path led outside of the filesystem") { |
There was a problem hiding this comment.
Let's avoid string matching errors, we can use downcasts
But perhaps better we should be using https://docs.rs/cap-std-ext/5.1.2/cap_std_ext/struct.RootDir.html ? And yes we'd need a copy api there.
Perhaps simplest to open the parent dir rooted, then do a copy that way
| //! When `--preserve-var` is passed, the running kernel and initramfs are saved | ||
| //! to `<root_path>/var/lib/pkgmode-rollback/` **before** the install wipes | ||
| //! `/boot`. After the install a third BLS entry (`pkgmode-rollback.conf`) is | ||
| //! written into the active `loader.N/entries/` directory so GRUB presents a | ||
| //! "Previous OS" option. The entry is invisible to ostree (which only reads | ||
| //! `ostree-*.conf` files) and to bootupd (which does not touch `loader/entries/`). |
There was a problem hiding this comment.
This is kind of hacky, I think it'd be cleaner to just not delete the entries (and kernel/initramfs) at all which should be a separate option.
| .context("Locating new ostree deployment directory")?; | ||
| println!(" Deployment directory: {}", deploy_dir.display()); | ||
|
|
||
| // The deployment's var/ is two levels up from the deploy dir: |
| /// We enumerate `<root_path>/ostree/deploy/` and return the most recently | ||
| /// modified entry under each stateroot's `deploy/` subdirectory. | ||
| #[context("Locating new ostree deployment directory")] | ||
| fn find_deploy_dir(root_path: &Path) -> Result<PathBuf> { |
There was a problem hiding this comment.
There's APIs for htis too
| /// `root_path` is the host root as seen from inside the install container. | ||
| /// `kver` is the running kernel version string returned by `save_pkgmode_kernel`. | ||
| #[context("Running post-install /var preservation and rollback entry")] | ||
| pub(crate) fn preserve_var_and_write_rollback(root_path: &Path, kver: &str) -> Result<()> { |
There was a problem hiding this comment.
In general let's try to use cap-std
| /// Returns true if the filesystem hosting `new_var` supports reflinks. | ||
| /// | ||
| /// Probes by attempting a zero-byte reflink from `src_var` into `new_var`. | ||
| fn reflinks_supported(src_var: &Path, new_var: &Path) -> bool { |
There was a problem hiding this comment.
Our storage code already probes this I believe
| let src_entry = src_var.join(name_str.as_ref()); | ||
| let dst_entry = new_var.join(name_str.as_ref()); | ||
|
|
||
| // For `log/`, skip only the `journal` subdirectory (large; regenerated by journald). |
There was a problem hiding this comment.
Why is this special cased?
| continue; | ||
| } | ||
|
|
||
| // For `lib/`, skip `containers` (podman/docker storage — overlay mounts cannot |
There was a problem hiding this comment.
Ditto, I don't think we should have special cases buried in this code. We should make it configurable of course in general, via e.g. --preserve-var-skip=log/,lib/containers or something
This is AI generated with lots of guidance.
Add two flags to
bootc install to-existing-rootthat are most usefulwhen migrating a package-mode system to a bootc image built externally:
--preserve-var Copy the running system's /var data into the new
deployment, and write a package-mode GRUB rollback
boot entry.
--merge-etc Apply the running system's /etc customisations onto
the new deployment via a 3-way merge.
Typical usage:
podman run --rm --privileged --pid=host --user=root:root
-v /dev:/dev --security-opt label=type:unconfined_t
-v /:/target -v /var/lib/containers:/var/lib/containers
bootc install to-existing-root
--preserve-var
--merge-etc
--acknowledge-destructive