Skip to content

Commit 6051602

Browse files
committed
Make qvm-start drive assignment reusable
Qube Manager is calling qvm_start.main on 3 separate locations...
1 parent 209fd42 commit 6051602

3 files changed

Lines changed: 173 additions & 164 deletions

File tree

qubesadmin/exc.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ class QubesNoTemplateError(QubesVMError):
101101
"""Cannot start domain, because there is no template"""
102102

103103

104+
class QubesVMAlreadyStartedError(QubesVMError):
105+
"""Requested qube to start, but it's already running"""
106+
107+
104108
class QubesPoolInUseError(QubesException):
105109
"""VM is in use, cannot remove."""
106110

@@ -236,5 +240,6 @@ def __init__(self, prop: str):
236240
class QubesNotesError(QubesException):
237241
"""Some problem with qube notes."""
238242

243+
239244
# legacy name
240245
QubesDaemonNoResponseError = QubesDaemonAccessError

qubesadmin/tools/qvm_start.py

Lines changed: 9 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -20,39 +20,11 @@
2020

2121
"""qvm-start - start a domain"""
2222
import asyncio
23-
import argparse
24-
import string
2523
import sys
2624

27-
import subprocess
28-
29-
import time
30-
31-
from qubesadmin.device_protocol import DeviceAssignment, UnknownDevice
3225
import qubesadmin.exc
3326
import qubesadmin.tools
34-
35-
36-
class DriveAction(argparse.Action):
37-
"""Action for argument parser that stores drive image path."""
38-
39-
# pylint: disable=redefined-builtin
40-
def __init__(
41-
self,
42-
option_strings,
43-
dest="drive",
44-
*,
45-
prefix="cdrom:",
46-
metavar="IMAGE",
47-
required=False,
48-
help="Attach drive",
49-
):
50-
super().__init__(option_strings, dest, metavar=metavar, help=help)
51-
self.prefix = prefix
52-
53-
def __call__(self, parser, namespace, values, option_string=None):
54-
# pylint: disable=redefined-outer-name
55-
setattr(namespace, self.dest, self.prefix + values)
27+
import qubesadmin.utils
5628

5729

5830
parser = qubesadmin.tools.QubesArgumentParser(
@@ -77,7 +49,7 @@ def __call__(self, parser, namespace, values, option_string=None):
7749

7850
parser_drive.add_argument(
7951
"--hddisk",
80-
action=DriveAction,
52+
action=qubesadmin.utils.DriveAction,
8153
dest="drive",
8254
prefix="hd:",
8355
help="temporarily attach specified drive as hard disk",
@@ -86,7 +58,7 @@ def __call__(self, parser, namespace, values, option_string=None):
8658
parser_drive.add_argument(
8759
"--cdrom",
8860
metavar="IMAGE",
89-
action=DriveAction,
61+
action=qubesadmin.utils.DriveAction,
9062
dest="drive",
9163
prefix="cdrom:",
9264
help="temporarily attach specified drive as CD/DVD",
@@ -102,142 +74,16 @@ def __call__(self, parser, namespace, values, option_string=None):
10274
)
10375

10476

105-
def get_drive_assignment(app, drive_str):
106-
"""
107-
Prepare :py:class:`qubesadmin.device_protocol.DeviceAssignment` object for a
108-
given drive.
109-
110-
If running in dom0, it will also take care about creating the appropriate
111-
loop device (if necessary). Otherwise, only existing block devices are
112-
supported.
113-
114-
:param app: Qubes() instance
115-
:param drive_str: drive argument
116-
:return: DeviceAssignment matching *drive_str*
117-
"""
118-
devtype = "cdrom"
119-
if drive_str.startswith("cdrom:"):
120-
devtype = "cdrom"
121-
drive_str = drive_str[len("cdrom:") :]
122-
elif drive_str.startswith("hd:"):
123-
devtype = "disk"
124-
drive_str = drive_str[len("hd:") :]
125-
126-
try:
127-
backend_domain_name, port_id = drive_str.split(":", 1)
128-
except ValueError:
129-
raise ValueError(
130-
"Incorrect image name: image must be in the format "
131-
"of VMNAME:full_path, for example "
132-
"dom0:/home/user/test.iso"
133-
)
134-
try:
135-
backend_domain = app.domains[backend_domain_name]
136-
except KeyError:
137-
raise qubesadmin.exc.QubesVMNotFoundError(
138-
"No such VM: %s", backend_domain_name
139-
)
140-
if port_id.startswith("/"):
141-
# it is a path - if we're running in dom0, try to call losetup to
142-
# export the device, otherwise reject
143-
if app.qubesd_connection_type == "qrexec":
144-
raise qubesadmin.exc.QubesException(
145-
"Existing block device identifier needed when running from "
146-
"outside of dom0 (see qvm-block)"
147-
)
148-
try:
149-
if backend_domain.klass == "AdminVM":
150-
loop_name = subprocess.check_output(
151-
["sudo", "losetup", "-f", "--show", port_id]
152-
)
153-
loop_name = loop_name.strip()
154-
else:
155-
untrusted_loop_name, _ = backend_domain.run_with_args(
156-
"losetup", "-f", "--show", port_id, user="root"
157-
)
158-
untrusted_loop_name = untrusted_loop_name.strip()
159-
allowed_chars = string.ascii_lowercase + string.digits + "/"
160-
allowed_chars = allowed_chars.encode("ascii")
161-
if not all(c in allowed_chars for c in untrusted_loop_name):
162-
raise qubesadmin.exc.QubesException(
163-
"Invalid loop device name received from {}".format(
164-
backend_domain.name
165-
)
166-
)
167-
loop_name = untrusted_loop_name
168-
del untrusted_loop_name
169-
except subprocess.CalledProcessError:
170-
raise qubesadmin.exc.QubesException(
171-
"Failed to setup loop device for %s", port_id
172-
)
173-
assert loop_name.startswith(b"/dev/loop")
174-
port_id = loop_name.decode().split("/")[2]
175-
# wait for device to appear
176-
# FIXME: convert this to waiting for event
177-
timeout = 10
178-
while isinstance(
179-
backend_domain.devices["block"][port_id], UnknownDevice
180-
):
181-
if timeout == 0:
182-
raise qubesadmin.exc.QubesException(
183-
"Timeout waiting for {}:{} device to appear".format(
184-
backend_domain.name, port_id
185-
)
186-
)
187-
timeout -= 1
188-
time.sleep(1)
189-
190-
options = {"devtype": devtype, "read-only": devtype == "cdrom"}
191-
assignment = DeviceAssignment.new(
192-
backend_domain=backend_domain,
193-
port_id=port_id,
194-
devclass="block",
195-
options=options,
196-
mode="required",
197-
)
198-
199-
return assignment
200-
201-
202-
class QubesVMAlreadyRunningError(qubesadmin.exc.QubesVMError):
203-
"""Requested qube to start, but it's already running"""
204-
205-
206-
def startup(domain, args=None):
207-
# pylint: disable=missing-function-docstring
208-
if domain.is_running():
209-
if args.skip_if_running:
210-
return
211-
raise QubesVMAlreadyRunningError("Domain is already running")
212-
drive_assignment = None
213-
try:
214-
if args.drive:
215-
drive_assignment = get_drive_assignment(args.app, args.drive)
216-
try:
217-
domain.devices["block"].assign(drive_assignment)
218-
except Exception:
219-
drive_assignment = None
220-
raise
221-
222-
domain.start()
223-
224-
if drive_assignment:
225-
# don't reconnect this device after VM reboot
226-
domain.devices["block"].unassign(drive_assignment)
227-
except (IOError, OSError, qubesadmin.exc.QubesException, ValueError) as e:
228-
if drive_assignment:
229-
try:
230-
domain.devices["block"].detach(drive_assignment)
231-
except qubesadmin.exc.QubesException:
232-
pass
233-
raise e
234-
235-
23677
async def run_async(args=None, app=None):
23778
# pylint: disable=missing-function-docstring
23879
args = parser.parse_args(args, app=app)
23980
tasks = [
240-
asyncio.to_thread(startup, domain=qube, args=args)
81+
asyncio.to_thread(
82+
qubesadmin.utils.start_expert,
83+
domain=qube,
84+
skip_if_running=args.skip_if_running,
85+
drive=args.drive,
86+
)
24187
for qube in args.domains
24288
]
24389
results = await asyncio.gather(*tasks, return_exceptions=True)

0 commit comments

Comments
 (0)