Skip to content

add qvm-restart - #470

Open
RandyTheOtter wants to merge 1 commit into
QubesOS:mainfrom
RandyTheOtter:restart
Open

RandyTheOtter wants to merge 1 commit into
QubesOS:mainfrom
RandyTheOtter:restart

Conversation

@RandyTheOtter

Copy link
Copy Markdown
Contributor

An attempt to finalize Ali's work: #387

Closes QubesOS/qubes-issues#4747

I have decided to not handle shutdown in situ because I would like to avoid duplication

@codecov

codecov Bot commented May 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.31579% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.85%. Comparing base (bb41ca5) to head (5dcd115).

Files with missing lines Patch % Lines
qubesadmin/tools/qvm_restart.py 72.72% 9 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #470   +/-   ##
=======================================
  Coverage   76.84%   76.85%           
=======================================
  Files          53       54    +1     
  Lines        9434     9472   +38     
=======================================
+ Hits         7250     7280   +30     
- Misses       2184     2192    +8     

☔ 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.

@RandyTheOtter

Copy link
Copy Markdown
Contributor Author

That should address the issues that are obvious-ish for me to fix.

Only thing left is the qvm-start issue. @marmarek it seems to me qvm-start can raise the error mid-loop only if it is executed with --drive option (which we don't pass here), or domain is already running. It also errors out if some domains don't exist, but that shoudn't be a concern for qvm-restart as qvm-shutdown fails the same way.

Am I missing something?

@marmarek

Copy link
Copy Markdown
Member

it seems to me qvm-start can raise the error mid-loop only

There is a bunch of cases where qvm-start can fail. Startup timeout, not enough memory, not enough disk space, and many more.

BTW #469 does refactor to make at least shutdown function reusable. You might want to wait for it to be merged, and then use the new function.

@RandyTheOtter

RandyTheOtter commented May 25, 2026

Copy link
Copy Markdown
Contributor Author

There is a bunch of cases where qvm-start can fail. Startup timeout, not enough memory, not enough disk space, and many more.

Shouldn't it get caught here?:
https://github.com/QubesOS/qubes-core-admin-client/blob/main/qubesadmin/tools/qvm_start.py#L201

        except (IOError, OSError, qubesadmin.exc.QubesException,
                ValueError) as e:
            if drive_assignment:
                try:
                    domain.devices['block'].detach(drive_assignment)
                except qubesadmin.exc.QubesException:
                    pass
            exit_code = 1
            parser.print_error(str(e))

    return exit_code

It returns status after the loop ends, so all start-able domains should start. They definitely do when I try a group with some of them having far too much memory to start.

@marmarek

Copy link
Copy Markdown
Member

Right, this case might be okay. But still, invoking other tool's main from another tool, parsing arguments again etc is not a good idea. This will also create several instances of Qubes() objects, which will cause issues with caching if that become relevant at some point...

@RandyTheOtter

Copy link
Copy Markdown
Contributor Author

Right, this case might be okay. But still, invoking other tool's main from another tool, parsing arguments again etc is not a good idea.

What's wrong with that? If anything using the same qvm-start every time a qube is started feels more reliable

This will also create several instances of Qubes() objects, which will cause issues with caching if that become relevant at some point...

Okay, should I create a separate pr for that, similar to Ben's qvm-shutdown refactor?

@ben-grande

Copy link
Copy Markdown
Contributor

Right, this case might be okay. But still, invoking other tool's main from another tool, parsing arguments again etc is not a good idea.

What's wrong with that? If anything using the same qvm-start every time a qube is started feels more reliable

  • main() is intended for script invocation from the shell, sometimes, it may raise SystemExit, sometimes indirectly via parser.error_runtime(). Even if not the case now for some tools, don't rely on it.
  • Most of that code is not necessary for a restart, only for a "start with drive for installation". You will end up just needing domain.start()

This will also create several instances of Qubes() objects, which will cause issues with caching if that become relevant at some point...

Okay, should I create a separate pr for that, similar to Ben's qvm-shutdown refactor?

No need for a separate PR, unless Github auto-closes this one when you rebase to a different upstream branch, and doesn't allow anyone to reopen it...

  • Add my fork as a remote
  • Rebase your PR on mine
  • Use qubesadmin.utils.(shutdown|start)
  • When my PR is merged, rebase your branch on the QubesOS remote back to the main branch

@RandyTheOtter RandyTheOtter reopened this Jun 8, 2026
@RandyTheOtter
RandyTheOtter marked this pull request as draft June 8, 2026 05:25
@marmarek marmarek mentioned this pull request Jun 10, 2026
@RandyTheOtter
RandyTheOtter marked this pull request as ready for review June 17, 2026 04:43

@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.

Looking better. Please see CI failures.

Comment thread qubesadmin/tools/qvm_restart.py
Comment thread qubesadmin/tools/qvm_restart.py Outdated
)
)
if failed['start']:
sys.stderr.write("Failed to start domains back up:\n")

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.

See parser.print_error.

@RandyTheOtter RandyTheOtter Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Wow that method looks useless, and requires making parser available. Why does it exist?

Regardless, I can't use it if I want to have output without "Error:" in each print. Adopted for pre-report messages.

Comment thread qubesadmin/tools/qvm_restart.py Outdated
Comment thread qubesadmin/tools/qvm_restart.py Outdated
@RandyTheOtter
RandyTheOtter force-pushed the restart branch 5 times, most recently from 46d45b5 to 626ae1f Compare June 19, 2026 01:50
@RandyTheOtter

Copy link
Copy Markdown
Contributor Author

Pylint is complaining that test lines too long, when broken apart they're less readable.
Tests are failing due to qubesadmin.vm failing to get_power_state(). Doesn't seem like my fault, not sure what to think of this.

@RandyTheOtter

Copy link
Copy Markdown
Contributor Author

Seems like get_power_state() needs something else in the mock response, I should look into this later on

@ben-grande

Copy link
Copy Markdown
Contributor

Pylint is complaining that test lines too long, when broken apart they're less readable.

Use black -l80 FILE.

Comment thread qubesadmin/tools/qvm_restart.py Outdated
if not args.start:
target_domains = [
vm for vm in target_domains
if vm.get_power_state() == "Running"

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.

Change this to if vm.is_running()?

@ben-grande

Copy link
Copy Markdown
Contributor

I have a use case for this to be in "utils" module (without expecting a command-line client), so I can easily use here (see how other actions were done):

@marmarek marmarek left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Besides comments below, can you squash the commits (and possibly add co-authored-by tag to the commit message to retain info about Ali)?

)
self.app.expected_calls[
("some-vm", "admin.vm.Shutdown", "wait", None)
] = b"0\x00"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be:

Suggested change
] = b"0\x00"
] = b"2\x00QubesVMNotStartedErrorDomain is powered off: 'some-vm'"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

CurrentState responses are too long even with black -l 80, looking at the other tests it seems like I can cheat a little and mock only power_state part of the response. It fixed some failing tests as well

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Whoops, I replied to the wrong conversation...

Anyway, QubesVMNotStartedErrorDomain is wrong, shutdown doesn't fail if target is down. Correct me if it's an upcoming change I haven't seen yet, but with current admin it replies 0\x00.

)
self.app.expected_calls[
("dormant-vm", "admin.vm.Shutdown", "force+wait", None)
] = b"0\x00"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

And similar here.

@RandyTheOtter
RandyTheOtter force-pushed the restart branch 2 times, most recently from 9e9793a to 79f3a32 Compare August 1, 2026 02:45
Comment thread qubesadmin/tests/tools/qvm_restart.py Outdated
b"0\x00some-vm class=AppVM state=Running\n"
)
self.app.expected_calls[("some-vm", "admin.vm.CurrentState", None, None)] = (
b"0\x00some-vm mem=42069 mem_static_max=42069 cputime=1337 power_state=Running\n"

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.

Example return type of admin.vm.CurrentState

% qubesd-query -e dom0 admin.vm.CurrentState dom0 | cat -A
0^@mem=4000000 mem_static_max=16000 cputime=440000000 power_state=Running% 

So know you know where the null is, just first character. My shell shows that there is no newline, by printing a % with reversevideo at the end.

If there were newline, it would have printed $:

% printf '\n' | cat -A
$

But on your shell, if it is not customized to handle output missing newline, your prompt will be on the same line as the output like this:

% bash --norc
bash-5.2$ printf '\0' | cat -A
^@bash-5.2$

If you want to see null representation and no newline without newline:

% printf '\0' | cat -A
^@%

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If you want to see precise bytes, I'd recommend xxd or hexdump -C.

@ben-grande

Copy link
Copy Markdown
Contributor

For the line-too-long pylint issue, use black formater with black -l80 FILE. Don't use black -l80 . as a lot of files on this repo are not formatted and there are open PRs that may be impacted.

@RandyTheOtter

Copy link
Copy Markdown
Contributor Author

For the line-too-long pylint issue, use black formater with black -l80 FILE. Don't use black -l80 . as a lot of files on this repo are not formatted and there are open PRs that may be impacted.

CurrentState responses are too long even with black -l 80, looking at the other tests it seems like I can cheat a little and mock only power_state part of the response. It have fixed some failing tests as well... Somehow...

@ben-grande

Copy link
Copy Markdown
Contributor

I was going to say that this is wrong, as you can break strings into multiple lines, but there are other tests, such as tests.tools.qvm_start.py, doing the same thing as only returning the necessary keys. It's still wrong, as it may cause problem in the future, but maybe it's fine for now?

Comment thread qubesadmin/utils/__init__.py Outdated
Comment thread qubesadmin/utils/__init__.py Outdated
Comment thread qubesadmin/tools/qvm_restart.py Outdated
Comment thread qubesadmin/utils/__init__.py Outdated
Comment thread qubesadmin/utils/__init__.py Outdated

if failed['shutdown']:
parser.print_error(
"Failed to restart domains:\n",

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 word "shutdown" fits better here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Why? Shutdown failure implies startup failure, thus it is a restart failure.

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.

Restart is made of "shutdown + start". Being specific that it is the "shutdown" is to be explicit and avoid ambiguous meaning.

Comment thread qubesadmin/utils/__init__.py Outdated
]
)
)
return failed

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.

We have to consider that every other method using generic_action has the same return type, which this functions changes from dict[QubesVM, BaseException] to dict[str, dict[QubesVM, BaseException]]. I am not sure what to do here, I don't like inconsistencies and would like that calling any of these actions returned the same type, so we don't need separate evaluation of the result depending on the method called, but I also understand that you use it for logging in qvm_restart. Maybe you could evaluate based on known exceptions for each call, and then do a safe fallback to generic logging when the exception is not known.

@RandyTheOtter RandyTheOtter Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Do we have a purpose for consistency here? Exception matching looks like avoidable maintenance burden considering that we already know which instruction returns what.

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.

#333

A single function that deals with all qube state wrappers. If the restart wrapper has a return signature, I'd need to create a separate handling just for that.

Ideally, there would be a parent class QubesVMShutdownError and QubesVMStartError, so that you could check if QubesVMShutdownTimeoutError or QubesMemoryError is part of which, so you would return with the same signature, and the command-line tool that intendes to log the information, would us isinstance to check hierarchy.

Co-Authored-By: Ali Mirjamali <ali@mirjamali.com>

@marmarek marmarek left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

CI complains about undocumented option:

Undocumented arguments for command 'qvm-restart': '--kill, -k'

*report_failure(failed_start),
end=""
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

restart generator needs to be closed:

await restart.aclose()

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feature request] qvm-restart

3 participants