Skip to content

vmupdate: add qvm-template-upgrade - #213

Open
nihalxkumar wants to merge 11 commits into
QubesOS:mainfrom
nihalxkumar:qvm-template-upgrade
Open

nihalxkumar wants to merge 11 commits into
QubesOS:mainfrom
nihalxkumar:qvm-template-upgrade

Conversation

@nihalxkumar

@nihalxkumar nihalxkumar commented May 25, 2026

Copy link
Copy Markdown
Contributor

This PR introduces the qvm-template-upgrade dom0 command-line utility, that performs an in-place N -> N+1 distribution upgrade of Debian and Fedora TemplateVM or StandaloneVM

fixes: QubesOS/qubes-issues#8605
GSoC 2026 project: Automate Template Version Upgrade

@ben-grande ben-grande left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yay, started. As I've done a review now, I will receive a notification every time you commit. Let me know when you need another look or have doubts by mentioning me.

Comment thread vmupdate/tests/test_template_upgrade.py
Comment thread vmupdate/template_upgrade.py Outdated
Comment thread vmupdate/template_upgrade.py Outdated
Comment thread vmupdate/template_upgrade.py Outdated
Comment thread vmupdate/template_upgrade.py Outdated
Comment thread vmupdate/template_upgrade.py Outdated
Comment thread vmupdate/template_upgrade.py Outdated
@codecov-commenter

codecov-commenter commented Jun 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.24556% with 59 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.60%. Comparing base (bbba4b0) to head (66580a6).

Files with missing lines Patch % Lines
vmupdate/agent/source/dnf/dnf5_api.py 85.14% 15 Missing ⚠️
vmupdate/agent/source/apt/apt_cli.py 89.18% 12 Missing ⚠️
vmupdate/agent/source/apt/apt_api.py 82.50% 7 Missing ⚠️
vmupdate/template_upgrade.py 98.05% 6 Missing ⚠️
vmupdate/agent/source/common/package_manager.py 90.56% 5 Missing ⚠️
vmupdate/agent/source/dnf/dnf_api.py 89.79% 5 Missing ⚠️
vmupdate/agent/source/common/progress_reporter.py 92.59% 4 Missing ⚠️
vmupdate/agent/source/dnf/dnf_cli.py 92.30% 3 Missing ⚠️
vmupdate/tests/test_template_upgrade.py 99.82% 1 Missing ⚠️
vmupdate/vmupdate.py 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #213      +/-   ##
==========================================
+ Coverage   71.36%   75.60%   +4.23%     
==========================================
  Files          12       34      +22     
  Lines        1383     4648    +3265     
==========================================
+ Hits          987     3514    +2527     
- Misses        396     1134     +738     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@nihalxkumar
nihalxkumar marked this pull request as ready for review June 2, 2026 05:29
@ben-grande

Copy link
Copy Markdown
Contributor

PipelineRetryFailed

@nihalxkumar

Copy link
Copy Markdown
Contributor Author

It's showing a successful run

image image

@ben-grande

Copy link
Copy Markdown
Contributor

Some tests are not enabled on this repo, such as mypy, black and pylint. See this as an example: https://github.com/QubesOS/qubes-core-admin/blob/main/.gitlab-ci.yml. Can you create a separate PR to enable those checks?

@nihalxkumar

Copy link
Copy Markdown
Contributor Author

Sure, will do

Comment thread vmupdate/template_upgrade.py Outdated
Comment thread vmupdate/template_upgrade.py Outdated
Comment thread vmupdate/template_upgrade.py Outdated
Comment thread vmupdate/template_upgrade.py Outdated
Comment thread vmupdate/template_upgrade.py Outdated
Comment thread vmupdate/template_upgrade.py Outdated
Comment thread vmupdate/template_upgrade.py Outdated
Comment thread vmupdate/template_upgrade.py Outdated
@nihalxkumar

Copy link
Copy Markdown
Contributor Author

We can squash here if it's looks fine. Will also have to rebase as this is 24 commits behind.

@ben-grande

Copy link
Copy Markdown
Contributor

Looks fine to squash.

@marmarek

Copy link
Copy Markdown
Member

Oh no, commit message in the other PR closed it ...

@marmarek marmarek reopened this Jun 10, 2026
@marmarek

Copy link
Copy Markdown
Member

I see a conflict here - CI will not run until it's resolved.

@nihalxkumar
nihalxkumar force-pushed the qvm-template-upgrade branch 6 times, most recently from 16b99f5 to f1f8fc1 Compare June 14, 2026 16:29
@nihalxkumar nihalxkumar changed the title qvm-template-upgrade: add CLI skeleton and orchestration flow qvm-template-upgrade: add orchestration workflow and in-VM agent Jun 14, 2026

@ben-grande ben-grande left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have taken a look through the new code. Thanks for the progress, I like to see it evolving. Just minor review though

Comment thread vmupdate/agent/source/common/package_manager.py
Comment thread vmupdate/agent/source/common/package_manager.py
Comment thread vmupdate/agent/source/dnf/dnf_cli.py Outdated
Comment thread vmupdate/agent/source/dnf/dnf_cli.py Outdated
@nihalxkumar
nihalxkumar force-pushed the qvm-template-upgrade branch from ad97dca to a0289a7 Compare June 15, 2026 17:11
@ben-grande

Copy link
Copy Markdown
Contributor

@nihalxkumar

Copy link
Copy Markdown
Contributor Author

When I tried upgrading fedora 41 -> 42 there was a cleanup failure which happened after the successful version upgrade.

logs as shared privately on tuesday:

https://gist.github.com/nihalxkumar/01fb990deab3960cb28d680773bb1089#file-upgrade42-log-L48-L55

we can see Complete! in the above log.

https://gist.github.com/nihalxkumar/0e4edccb4f4409b1a4daf5c44b6f576f

After version_upgrade() returns success, the agent still runs cleanup. The old code used the cleanup exit code when deciding the final agent exit code.
For dnf this includes DNFCLI.clean() (dnf clean packages), and the transport also removes /run/qubes-update/.

a90eee1 (last commit) is based on that failure boundary. After the release transaction has succeeded, a later cleanup failure should not cause rollback of the upgraded clone.

@ben-grande

Copy link
Copy Markdown
Contributor

But we can't ignore all cleanup failures. We don't know what is causing it, so this needs to be investigated.

@marmarek

Copy link
Copy Markdown
Member

I tried to test manually what openQA complained: I got fedora-43-xfce template installed, and then ran qvm-template-upgrade --template=fedora-43-xfce --new-name=fedora-44-upgraded and it failed too. Log has:

2026-08-29 18:47:19,948 [Agent] Run entrypoint with args: Namespace(log='DEBUG', no_refresh=False, f
orce_upgrade=False, no_cleanup=False, leave_obsolete=False, download_only=False, version_upgrade='44
', show_output=False, quiet=False, no_progress=False, just_print_progress=False)
2026-08-29 18:47:19,950 [Agent] Selecting package manager.
2026-08-29 18:47:19,955 [Agent] Add `deltarpm=False` to /etc/dnf/dnf.conf
2026-08-29 18:47:20,382 [Agent] Using dnf5.
2026-08-29 18:47:21,871 [Agent] Running distribution version upgrade to 44.
2026-08-29 18:47:21,873 [Agent] run command: dnf clean all
2026-08-29 18:47:22,571 [Agent] command exit code: 0
2026-08-29 18:47:40,968 [Agent] dnf5 repo load for release 44 took 18.375s
2026-08-29 18:47:46,514 [Agent] dnf5 dependency resolution for release 44 took 5.543s
2026-08-29 18:47:46,515 [Agent] Distro-sync found nothing to do.
2026-08-29 18:47:46,603 [Agent] release upgrade did not reach 44; os-release reports '43'.

I don't have any more details yet. Maybe setting releasever variable didn't work?

@marmarek

Copy link
Copy Markdown
Member

The output (also seen in the openqa report) is:

Plan: upgrade fedora-43-xfce (fedora 43) -> clone fedora-44-upgraded (fedora 44)
Cloning fedora-43-xfce -> fedora-44-upgraded
fedora-44-upgraded: Cloning private volume
fedora-44-upgraded: Cloning root volume
Running version-upgrade agent in fedora-44-upgraded (-> 44)
fedora-44-upgraded:out: Preparing distribution upgrade; dependency calculation may take some time...
fedora-44-upgraded:out: Fetching 6 packages [0.00 B]                                                
fedora-44-upgraded:out: Fetching Qubes VM Repository (devel testing) [3.38 KiB]                     
fedora-44-upgraded:out: Fetching Fedora 44 - x86_64 [18.21 KiB]                                     
fedora-44-upgraded:out: Fetching Fedora 44 openh264 (From Cisco) - x86_64 [986.00 B]                
fedora-44-upgraded:out: Fetching Fedora 44 - x86_64 - Updates [15.82 KiB]                           
fedora-44-upgraded:out: Fetching Qubes OS Repository for VM (updates) [1.97 KiB]                    
fedora-44-upgraded:out: Fetching qubes testing [1.97 KiB]                                           
fedora-44-upgraded:out: repomd.xml GPG signature verification error: Signing key not found          
fedora-44-upgraded:out: repomd.xml GPG signature verification error: Signing key not found          
fedora-44-upgraded:out: Fetching qubes testing [1.97 KiB]                                           
fedora-44-upgraded:out: Fetching Qubes OS Repository for VM (updates) [1.97 KiB]                    
fedora-44-upgraded:out: Calculating package changes...                                              
fedora-44-upgraded (fedora 43 -> 44)   0.0% |                                              | [01:13]
Upgrade failed: in-VM version-upgrade agent failed for fedora-44-upgraded (exit code 24); see /var/log/qubes/update-fedora-44-upgraded.log
Leaving clone fedora-44-upgraded in place (--keep-new-on-failure).
error: in-VM version-upgrade agent failed for fedora-44-upgraded (exit code 24); see /var/log/qubes/update-fedora-44-upgraded.log

So, it clearly did set releasever, as seen in the fedora repository names.

@marmarek

Copy link
Copy Markdown
Member

Running dnf distro-sync --relasever=44 manually got me this:

Failed to resolve the transaction:
Problem 1: installed package rpmfusion-free-release-43-1.noarch requires system-release(43), but none of the providers can be installed
  - fedora-release-43-27.noarch does not belong to a distupgrade repository
  - problem with installed package
 Problem 2: installed package fedora-release-43-27.noarch requires fedora-release-common = 43-27, but none of the providers can be installed
  - installed package rpmfusion-nonfree-release-43-1.noarch requires system-release(43), but none of the providers can be installed
  - fedora-release-common-43-27.noarch does not belong to a distupgrade repository
  - problem with installed package
You can try to add to command line:
  --skip-broken to skip uninstallable packages

So, there are two issues:

  1. The qvm-template-upgrade tool (or specifically its libdnf5 handling) does not report transaction resolving failure correctly.
  2. The upgrade fails due to rpmfusion release packages being installed, but related repositories being disabled.

Solving the second one might be tricky. Having rpmfusion-*-release packages installed is intentional, so users can easily enable it when needed, but it should still be up to the user to do that - that's why those repositories are disabled. The dependency there is strict, so just excluding rpmfusion packages from the upgrade won't help. What does work is temporarily enable the rpmfusion repositories (rpmfusion-free,rpmfusion-nonfree). The issue with this approach is that it risks fetching other packages from there too (on top of just rpmfusion-*-release packages), against user will. But maybe there is an option to avoid that, for example by using includepkgs option for those repositories? Or maybe by setting higher priority value for those repositories temporarily?
In any case, it would be good to also inspect transaction if no extra packages got pulled in from rpmfusion.

So, what I'd propose is:

  1. Check if rpmfusion-free-release package is installed.
  2. Check if rpmfusion-free repository is enabled.
  3. If the first is "yes" and the second is "no", then temporarily set rpmfusion-free repository to enabled=1 and includepkgs=rpmfusion-free-release (on the repository object, just after loading the configuration?)
  4. After resolving the goal, inspect transaction if really no unexpected packages got pulled in from rpmfusion repository (ofc only if it got enabled in step 3).

Do the same for rpmfusion-nonfree too.

@nihalxkumar

Copy link
Copy Markdown
Contributor Author

@marmarek I tried on minimal fedora-43 template
working perfectly https://asciinema.org/a/YJ7xl4xytytAZJ7f

@marmarek

Copy link
Copy Markdown
Member

Well, yes, it worked on minimal because it doesn't have rpmfusion included...

@nihalxkumar

Copy link
Copy Markdown
Contributor Author

right. I currently have a running upgrade for fedora-43-xfce
will share the final result and logs shortly

@nihalxkumar

Copy link
Copy Markdown
Contributor Author

successful https://asciinema.org/a/9xZ74qLWNNnRDdc2
however, the existing approach uninstalls rpmfusion-free-release-43 and rpmfusion-nonfree-release-43, instead of upgrading them

@marmarek

Copy link
Copy Markdown
Member

Ok, that's another approach, yes. Not perfect, but probably good enough for now.

In any case, it would be good to fix reporting error on transaction resolve step. I found another way you can simulate the failure - edit /etc/yum.repos.d/qubes-r4.repo and replace $releasever with hardcoded old version (like 43). This should result in an upgrade failure and you can see that no details are shown to the user.

@marmarek

marmarek commented Sep 1, 2026

Copy link
Copy Markdown
Member

Ok, now the transaction failure info is included in the log, which is good. But on the tool output it still have last status as Calculating package changes... and then the generic error message. Maybe add also some message there, like Found N problems when resolving packages dependencies, or something like this? It may be okay to leave full details only in log (they can be quite long in some cases).

Other than that, it seems to work now, and indeed removes rpmfusion package. openQA test says it works now :)
But, unit test and mypy fail in CI...

@marmarek

marmarek commented Sep 1, 2026

Copy link
Copy Markdown
Member

I see a conflict now, and that prevents CI from running :(

@nihalxkumar

Copy link
Copy Markdown
Contributor Author

Yes, I am fixing

Add a --version-upgrade mode to the in-VM agent so a qube can be moved
to the next major distribution release. The dnf backend distro-syncs
to the target releasever; the apt backend updates, rewrites source
codenames, and dist-upgrades. Both confirm os-release before reporting
success.
The agent emits a final explicit 100% even when callback progress
already reached it, causing a bogus "err: 100.00" line.

Drop the duplicate 100 milestone after progress has finished.
Upgrade a clone of a TemplateVM or StandaloneVM to the next
distribution release, leaving the original qube untouched.
Document the options, the clone name derivation, and which template-*
features the tool rewrites, including why the inherited epoch, version
and release make qvm-template list the upgraded clone as upgradeable.
Progress hit 100% before post-transaction scriptlets finished,
making the bar appear frozen for the remaining work.

Cap package progress at 97% and advance asymptotically through
post-transaction scriptlets. Report 100% after qubes.PostInstall
so the bar stays responsive until everything is done.
During release upgrades the root volume briefly holds both releases,
which can trigger a spurious disk-space warning toast.

Suppress disk-space-not-notify on the clone during the upgrade and
restore it afterwards.
Each Debian release-upgrade step drove the progress bar to 100%,
silencing later steps. Give each step its own weighted slice of the
bar so progress stays proportional throughout the upgrade.
@marmarek

marmarek commented Sep 1, 2026

Copy link
Copy Markdown
Member

@ben-grande are you happy with the current shape of this PR?

]
except OSError as exc:
return self._refuse(f"cannot read apt sources: {exc}.")
if not any(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the any here is fine. It shouldn't be all because some sources might not even have a proper suite, such as

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gives me an idea, a test that includes external repos, can even be a fake/localhost one, that doesn't have a codename in the sources file, just to guarantee that this any is not changed to all in the future.

# Update before switching sources and running dist-upgrade: a stale
# system risks unresolvable transactions across the release boundary.
# Each step gets its own slice of the bar, weighted by its share of
# a measured debian-12 to 13 template upgrade; without that, every

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any tips on how to choose the step number in case something else needs to be added here? Please include a comment about it. As in, how it was measured to set this step configuration.

encoding="utf-8",
newline="",
) as releases_file:
for release in csv.DictReader(releases_file):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if postinstall != 0:
self.log.warning(
"qubes.PostInstall exited with %d; dom0 metadata "
"(qvm-features) will refresh on next qube start.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please change "dom0 metadata (qvm-features)" to "qube features".

self, target: str, os_data: dict
) -> ProcessResult:
"""Verify the target is a valid single-step upgrade from the current release."""
if not (target.isascii() and target.isdigit()):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a digit that is not ASCII?

self.log.debug(
"dnf package download for release %s took %.3fs",
target,
time.monotonic() - download_started,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I counted 13 occurrences of the pattern:

  • record time
  • run action
  • log time difference

Can be made with contextlib.contextmanager.

"""Temporarily suppress qui-disk-space during upgrade.

The upgrade briefly holds both releases' packages, which can
trip the 90% threshold. This may bring unnecesarry panic for users.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm.. 90% is above what I expected. When we discussed about having enough space in the root file system for upgrades, I thought the threshold was bigger. You reached 90% while having how much total space in the root partition?


fedora-41-minimal, 41 -> 42 => fedora-42-minimal

custom, 41 -> 42 => custom-42

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR, but in the future, having an option to keep the same name when it doesn't have the version in the name, would be interesting for standalones and custom named templates.

description="Upgrade a TemplateVM or StandaloneVM to the next distro "
"version.",
# Avoid qubesadmin package metadata lookup when run from PYTHONPATH.
version="",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then show the version of the vmupdate?

raise ValidationError(
f"{self.source_vm.name} is a {distro} derivative; its own "
f"version numbering does not match {family}'s releases, so "
f"it cannot be upgraded with this tool."

@ben-grande ben-grande Sep 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Examples:

  • Ubuntu: has its own repos
  • Kicksecure: has debian repos
  • Whonix: has debian repos

I think Kicksecure and Whonix will break on this validation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think it is important to do a test run on Kicksecure and Whonix and let us know what happens. Whonix, different than other derivatives, doesn't mess with /etc/os-release for example.

Imagine a whonix-gateway-18, it has the following:

% cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
NAME="Debian GNU/Linux"
VERSION_ID="13"
VERSION="13 (trixie)"
VERSION_CODENAME=trixie
DEBIAN_VERSION_FULL=13.4
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

If upgrading from whonix-gateway-12, which was bookworm, will the template be renamed to whonix-gateway-13?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As upgrading Whonix and Kicksecure was out of the scope, if it fails, could you please open an issue for it to be tracked?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to do it, just sharing the Whonix release upgrade instructions: https://www.whonix.org/wiki/Release_Upgrade_17_to_18, not as simple as just upgrading base distributions.

raise ValidationError(
f"{self.source_vm.name} is a {distro} derivative; its own "
f"version numbering does not match {family}'s releases, so "
f"it cannot be upgraded with this tool."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As upgrading Whonix and Kicksecure was out of the scope, if it fails, could you please open an issue for it to be tracked?

# clone has already halted.
try:
self.cloned_qube.kill()
except qubesadmin.exc.QubesVMNotStartedError:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When does this happen? Please document/comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Contribution] qvm-upgrade-template (easy in-place upgrades for Debian and Fedora templates)

7 participants