2020
2121"""qvm-start - start a domain"""
2222import asyncio
23- import argparse
24- import string
2523import sys
2624
27- import subprocess
28-
29- import time
30-
31- from qubesadmin .device_protocol import DeviceAssignment , UnknownDevice
3225import qubesadmin .exc
3326import 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
5830parser = qubesadmin .tools .QubesArgumentParser (
@@ -77,7 +49,7 @@ def __call__(self, parser, namespace, values, option_string=None):
7749
7850parser_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):
8658parser_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-
23677async 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