Merge ~racb/uvtool:python3-packaging into uvtool:ubuntu/devel

Proposed by Robie Basak
Status: Merged
Merged at revision: e358612386352367209ce3612d16ea6419ff1618
Proposed branch: ~racb/uvtool:python3-packaging
Merge into: uvtool:ubuntu/devel
Diff against target: 881 lines (+267/-125)
13 files modified
bin/uvt-kvm (+2/-2)
bin/uvt-simplestreams-libvirt (+2/-2)
debian/changelog (+26/-0)
debian/control (+13/-15)
debian/rules (+3/-3)
man/uvt-kvm.1 (+27/-3)
template.xml (+6/-1)
uvtool/libvirt/__init__.py (+2/-2)
uvtool/libvirt/kvm.py (+150/-62)
uvtool/libvirt/simplestreams.py (+16/-12)
uvtool/ssh.py (+6/-7)
uvtool/tests/test_kvm.py (+7/-8)
uvtool/tests/test_simplestreams.py (+7/-8)
Reviewer Review Type Date Requested Status
Christian Ehrhardt  Approve
Canonical Server Pending
Review via email: mp+374591@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Christian Ehrhardt  (paelzer) wrote :

I didn't expect you to need an explicit --buildsystem=pybuild, but it is ok.
Content changes match what I have reviewed for the non packaging branch.

Packaging changes pull in quite some old changelogs that are correct, but not part of this merge.
But it matches what is in Eoan, so that should be fine as well.

Was that the base for the test PPA that we already tested or do you want testing again?

review: Approve
Revision history for this message
Robie Basak (racb) wrote :

On Wed, Oct 23, 2019 at 11:20:54AM -0000, Christian Ehrhardt  wrote:
> I didn't expect you to need an explicit --buildsystem=pybuild, but it is ok.

I am under the impression that --buildsystem=pybuild is necessary
currently because it is the recommended best practice but not the
debhelper default.

> Packaging changes pull in quite some old changelogs that are correct, but not part of this merge.
> But it matches what is in Eoan, so that should be fine as well.

I think that's a problem with the preview diff. I think the actual
commits are correct and don't include any unnecessary changes.

> Was that the base for the test PPA that we already tested or do you want testing again?

I updated ppa:racb/experimental2 for Eoan (only) with essentially the
same changes. The only differences are that this branch is rebased on to
my merge into master, rather than my development Python 3 branch, I
dropped uvtool-coverage, and the changelog is different.

If you'd like to see the changes, I have been tagging previous versions
of the packaging branch and pushing those tags - see
python3-packaging/v* in my repo.

I'm confident that it isn't necessary to test further, but if you'd like
to, then please do :)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/bin/uvt-kvm b/bin/uvt-kvm
index c96c50d..282e866 100755
--- a/bin/uvt-kvm
+++ b/bin/uvt-kvm
@@ -1,8 +1,8 @@
1#!/usr/bin/python1#!/usr/bin/python3
22
3# Wrapper around cloud-localds and libvirt3# Wrapper around cloud-localds and libvirt
44
5# Copyright (C) 2012-3 Canonical Ltd.5# Copyright (C) 2012-9 Canonical Ltd.
6# Author: Robie Basak <robie.basak@canonical.com>6# Author: Robie Basak <robie.basak@canonical.com>
7#7#
8# This program is free software: you can redistribute it and/or modify8# This program is free software: you can redistribute it and/or modify
diff --git a/bin/uvt-simplestreams-libvirt b/bin/uvt-simplestreams-libvirt
index 7693b02..b220da2 100755
--- a/bin/uvt-simplestreams-libvirt
+++ b/bin/uvt-simplestreams-libvirt
@@ -1,8 +1,8 @@
1#!/usr/bin/python1#!/usr/bin/python3
22
3# Keep Ubuntu Cloud images synced to a local libvirt storage pool.3# Keep Ubuntu Cloud images synced to a local libvirt storage pool.
44
5# Copyright (C) 2013 Canonical Ltd.5# Copyright (C) 2013-9 Canonical Ltd.
6# Author: Robie Basak <robie.basak@canonical.com>6# Author: Robie Basak <robie.basak@canonical.com>
7#7#
8# This program is free software: you can redistribute it and/or modify8# This program is free software: you can redistribute it and/or modify
diff --git a/debian/changelog b/debian/changelog
index af18485..3f77faa 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,29 @@
1uvtool (0~git162-0ubuntu1) UNRELEASED; urgency=medium
2
3 * Port to Python 3.
4
5 -- Robie Basak <robie.basak@ubuntu.com> Wed, 23 Oct 2019 11:30:04 +0100
6
7uvtool (0~git148-0ubuntu1) eoan; urgency=medium
8
9 [ Christian Ehrhardt ]
10 * kvm: add --machine-type option (LP: #1775645)
11 * man: fix rendering page of option after --machine-type
12 * man: describe implications and defaults of --machine-type
13
14 [ Robie Basak ]
15 * Add --mac option (LP: #1781727). Thanks to adamretter.
16
17 [ Christian Ehrhardt ]
18 * template (x86): switch the default video to QXL
19 * template (x86): add spice graphics backend
20
21 [ Robie Basak ]
22 * Add --network-config option (LP: #1781785). Thanks to adamretter.
23 * Default to plain HTTP for cloud image downloads (LP: #1409400)
24
25 -- Robie Basak <robie.basak@ubuntu.com> Tue, 30 Apr 2019 16:55:01 +0100
26
1uvtool (0~git140-0ubuntu1) bionic; urgency=medium27uvtool (0~git140-0ubuntu1) bionic; urgency=medium
228
3 [ Christian Ehrhardt ]29 [ Christian Ehrhardt ]
diff --git a/debian/control b/debian/control
index 9f509bf..8163583 100644
--- a/debian/control
+++ b/debian/control
@@ -4,15 +4,13 @@ Priority: extra
4Standards-Version: 3.9.44Standards-Version: 3.9.4
5Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>5Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
6Build-Depends: debhelper (>= 7),6Build-Depends: debhelper (>= 7),
7 python-all,7 dh-python,
8 python-setuptools,8 python3,
9 python-mock,9 python3-libvirt,
10 python-libvirt,10 python3-lxml,
11 python-lxml,11 python3-pyinotify,
12 python-pyinotify,12 python3-simplestreams,
13 python-simplestreams,13 python3-yaml
14 python-yaml
15X-Python-Version: >= 2.7
1614
17Package: uvtool15Package: uvtool
18Architecture: all16Architecture: all
@@ -30,18 +28,18 @@ Package: uvtool-libvirt
30Architecture: all28Architecture: all
31Depends: libvirt-daemon-system | libvirt-bin,29Depends: libvirt-daemon-system | libvirt-bin,
32 libvirt-clients | libvirt-bin,30 libvirt-clients | libvirt-bin,
33 python-libvirt,31 python3-libvirt,
34 python-simplestreams,32 python3-simplestreams,
35 python-lxml,33 python3-lxml,
36 python-pyinotify,34 python3-pyinotify,
37 python-yaml,35 python3-yaml,
38 distro-info,36 distro-info,
39 cloud-image-utils (>= 0.27),37 cloud-image-utils (>= 0.27),
40 qemu-utils,38 qemu-utils,
41 ubuntu-cloudimage-keyring,39 ubuntu-cloudimage-keyring,
42 socat,40 socat,
43 ${misc:Depends},41 ${misc:Depends},
44 ${python:Depends}42 ${python3:Depends}
45Recommends: qemu-kvm, cpu-checker43Recommends: qemu-kvm, cpu-checker
46Description: Library and tools for using Ubuntu Cloud Images with libvirt44Description: Library and tools for using Ubuntu Cloud Images with libvirt
47 This package provides libvirt-specific tools for consuming Ubuntu Cloud45 This package provides libvirt-specific tools for consuming Ubuntu Cloud
diff --git a/debian/rules b/debian/rules
index 25dccc9..82c03eb 100755
--- a/debian/rules
+++ b/debian/rules
@@ -1,13 +1,13 @@
1#!/usr/bin/make -f1#!/usr/bin/make -f
22
3%:3%:
4 dh $@ --with python24 dh $@ --with python3 --buildsystem=pybuild
55
6override_dh_auto_build:6override_dh_auto_build:
7 $(MAKE) -C uvtool/tests/streams7 $(MAKE) -C uvtool/tests/streams
8 dh_auto_build8 dh_auto_build
9 PYTHONPATH=$(CURDIR) python -m unittest uvtool.tests.test_kvm9 PYTHONPATH=$(CURDIR) python3 -m unittest uvtool.tests.test_kvm
10 PYTHONPATH=$(CURDIR) python -m unittest uvtool.tests.test_simplestreams10 PYTHONPATH=$(CURDIR) python3 -m unittest uvtool.tests.test_simplestreams
1111
12override_dh_auto_clean:12override_dh_auto_clean:
13 $(MAKE) -C uvtool/tests/streams clean13 $(MAKE) -C uvtool/tests/streams clean
diff --git a/man/uvt-kvm.1 b/man/uvt-kvm.1
index 7ad22e6..e626370 100644
--- a/man/uvt-kvm.1
+++ b/man/uvt-kvm.1
@@ -50,9 +50,11 @@ wrapping libvirt and cloud-init.
50is not intended to wrap all possible use cases. Where possible, it50is not intended to wrap all possible use cases. Where possible, it
51provides access to some more advanced cases using options to override51provides access to some more advanced cases using options to override
52entire sections of default operation, such as the ability to directly52entire sections of default operation, such as the ability to directly
53override the backing volume image used, the libvirt domain definition53override the backing volume image used, the libvirt domain definition,
54and cloud-init metadata and userdata. For yet more complex cases, it is54cloud-init metadata and userdata, and directly provide a network-config
55expected that the user will call libvirt directly (for example by using55file.
56For yet more complex cases, it is expected that the user will call
57libvirt directly (for example by using
56.BR virsh (1)),58.BR virsh (1)),
57and use uvt-kvm for only the simpler operations required on affected59and use uvt-kvm for only the simpler operations required on affected
58VMs. See ADVANCED OVERRIDE OPTIONS and ADVANCED USAGE for details.60VMs. See ADVANCED OVERRIDE OPTIONS and ADVANCED USAGE for details.
@@ -364,6 +366,11 @@ Replace the first defined NIC with one that connects to the given host
364bridge. Default: unaltered from the libvirt domain template.366bridge. Default: unaltered from the libvirt domain template.
365367
366.TP368.TP
369.BI --mac\ mac
370Use this MAC address for the first defined NIC. Can be used in
371conjunction with --bridge. Default unspecified.
372
373.TP
367.B --log-console-output374.B --log-console-output
368Log output to a disk file on the host instead of to a pty. With375Log output to a disk file on the host instead of to a pty. With
369libvirt's default configuration on Ubuntu, this log can be found in376libvirt's default configuration on Ubuntu, this log can be found in
@@ -381,6 +388,17 @@ Instead of the default cpu model - which mostly is a compatibility focused
381lowest denominator of cpu features - use host-passthrough which will try to388lowest denominator of cpu features - use host-passthrough which will try to
382make all of the hosts cpu features available in the guest.389make all of the hosts cpu features available in the guest.
383390
391.TP
392.BI --machine-type\ type
393Set the machine type to the specified string before instantiating the guest.
394See \fBkvm -M ?\fR for a list of types supported by your current system.
395If not set this section of the template is not altered before the guest is
396defined.
397If set this modifies the internal temporary libvirt domain template at
398the element \fBdomain->os->type\fR and sets the attribute \fBmachine\fR to the
399given value before defining the guest. This implies that libvirt will add the
400type-dependent default devices when defining the guest.
401
384.SH CLOUD-INIT CONFIGURATION OPTIONS402.SH CLOUD-INIT CONFIGURATION OPTIONS
385403
386Valid for: \fBuvt-kvm\ create\fR only.404Valid for: \fBuvt-kvm\ create\fR only.
@@ -487,6 +505,12 @@ not otherwise tunable.
487Default: minimal file with automatically generated instance-id.505Default: minimal file with automatically generated instance-id.
488506
489.TP507.TP
508.BI --network-config\ network_config_file
509Provide cloud-init network-config using the file supplied.
510
511Default: no network-config file.
512
513.TP
490.BI --backing-image-file\ image_file514.BI --backing-image-file\ image_file
491Specify the name of a local file that will be used to create the VM instead of515Specify the name of a local file that will be used to create the VM instead of
492relying on the volume storage pool. It must point to a qcow2 formatted file.516relying on the volume storage pool. It must point to a qcow2 formatted file.
diff --git a/template.xml b/template.xml
index 3c795c1..b76bcb8 100644
--- a/template.xml
+++ b/template.xml
@@ -20,6 +20,11 @@
20 <graphics type='vnc' autoport='yes' listen='127.0.0.1'>20 <graphics type='vnc' autoport='yes' listen='127.0.0.1'>
21 <listen type='address' address='127.0.0.1'/>21 <listen type='address' address='127.0.0.1'/>
22 </graphics>22 </graphics>
23 <video/>23 <graphics type='spice' autoport='yes' listen='127.0.0.1'>
24 <listen type='address' address='127.0.0.1'/>
25 </graphics>
26 <video>
27 <model type='qxl'/>
28 </video>
24 </devices>29 </devices>
25</domain>30</domain>
diff --git a/uvtool/libvirt/__init__.py b/uvtool/libvirt/__init__.py
index 2cd950c..3988e24 100644
--- a/uvtool/libvirt/__init__.py
+++ b/uvtool/libvirt/__init__.py
@@ -1,4 +1,4 @@
1# Copyright (C) 2013 Canonical Ltd.1# Copyright (C) 2013-9 Canonical Ltd.
2# Author: Robie Basak <robie.basak@canonical.com>2# Author: Robie Basak <robie.basak@canonical.com>
3#3#
4# This program is free software: you can redistribute it and/or modify4# This program is free software: you can redistribute it and/or modify
@@ -91,7 +91,7 @@ def _create_volume_from_fobj_with_size(new_volume_name, fobj, fobj_size,
91 E.target(E.format(type=image_type)),91 E.target(E.format(type=image_type)),
92 *extra92 *extra
93 )93 )
94 vol = pool.createXML(etree.tostring(new_vol), 0)94 vol = pool.createXML(etree.tostring(new_vol, encoding=str), 0)
9595
96 try:96 try:
97 stream = conn.newStream(0)97 stream = conn.newStream(0)
diff --git a/uvtool/libvirt/kvm.py b/uvtool/libvirt/kvm.py
index 5e1c15e..c5b10d7 100755
--- a/uvtool/libvirt/kvm.py
+++ b/uvtool/libvirt/kvm.py
@@ -1,8 +1,8 @@
1#!/usr/bin/python1#!/usr/bin/python3
22
3# Wrapper around cloud-localds and libvirt3# Wrapper around cloud-localds and libvirt
44
5# Copyright (C) 2012-3 Canonical Ltd.5# Copyright (C) 2012-9 Canonical Ltd.
6# Author: Robie Basak <robie.basak@canonical.com>6# Author: Robie Basak <robie.basak@canonical.com>
7#7#
8# This program is free software: you can redistribute it and/or modify8# This program is free software: you can redistribute it and/or modify
@@ -23,15 +23,16 @@ from __future__ import print_function
23from __future__ import unicode_literals23from __future__ import unicode_literals
2424
25import argparse25import argparse
26import base64
26import errno27import errno
27import functools28import functools
29import io
28import itertools30import itertools
29import os31import os
30import platform32import platform
31import shutil33import shutil
32import signal34import signal
33import string35import string
34import StringIO
35import subprocess36import subprocess
36import sys37import sys
37import tempfile38import tempfile
@@ -92,19 +93,19 @@ def subprocess_setup():
92def run_script_once_arg_to_config(arg, unique_id):93def run_script_once_arg_to_config(arg, unique_id):
93 with open(arg, 'rb') as f:94 with open(arg, 'rb') as f:
94 script = f.read()95 script = f.read()
95 encoded_script = script.encode('base64')96 encoded_script = base64.b64encode(script).decode('ascii')
96 return [97 return [
97 b'cloud-init-per',98 'cloud-init-per',
98 b'once',99 'once',
99 unique_id.encode('utf-8'),100 unique_id,
100 b'sh', b'-c',101 'sh', '-c',
101 (102 (
102 b'f=$(mktemp --tmpdir %s-XXXXXXXXXX) && ' +103 'f=$(mktemp --tmpdir %s-XXXXXXXXXX) && ' +
103 b'echo "%s" | base64 -d > "$f" && ' +104 'echo "%s" | base64 -d > "$f" && ' +
104 b'chmod 700 "$f" && ' +105 'chmod 700 "$f" && ' +
105 b'"$f" && ' +106 '"$f" && ' +
106 b'rm "$f"'107 'rm "$f"'
107 ) % (unique_id.encode('utf-8'), encoded_script)108 ) % (unique_id, encoded_script)
108 ]109 ]
109110
110111
@@ -122,7 +123,7 @@ def get_ssh_agent_public_keys():
122 output = subprocess.check_output(123 output = subprocess.check_output(
123 ['ssh-add', '-L'],124 ['ssh-add', '-L'],
124 stderr=fpnull125 stderr=fpnull
125 )126 ).decode()
126 except subprocess.CalledProcessError:127 except subprocess.CalledProcessError:
127 return None128 return None
128129
@@ -140,7 +141,7 @@ def read_ssh_public_key_file(filename):
140 filename = os.path.join(os.environ['HOME'], '.ssh', 'id_rsa.pub')141 filename = os.path.join(os.environ['HOME'], '.ssh', 'id_rsa.pub')
141142
142 try:143 try:
143 f = open(filename, 'rb')144 f = open(filename, 'r')
144 except IOError as e:145 except IOError as e:
145 if e.errno != errno.ENOENT:146 if e.errno != errno.ENOENT:
146 raise147 raise
@@ -178,51 +179,58 @@ def create_default_user_data(fobj, args, ssh_host_keys=None):
178179
179 """180 """
180181
181 ssh_authorized_keys = get_ssh_authorized_keys(args.ssh_public_key_file)182 ssh_authorized_keys = list(
183 get_ssh_authorized_keys(args.ssh_public_key_file),
184 )
182185
183 data = {186 data = {
184 b'hostname': args.hostname.encode('ascii'),187 'hostname': args.hostname,
185 b'manage_etc_hosts': b'localhost',188 'manage_etc_hosts': 'localhost',
186 b'ssh_keys': uvtool.ssh.generate_ssh_host_keys()[0],189 'ssh_keys': uvtool.ssh.generate_ssh_host_keys()[0],
187 }190 }
188191
189 if ssh_host_keys:192 if ssh_host_keys:
190 data[b'ssh_keys'] = ssh_host_keys193 data['ssh_keys'] = ssh_host_keys
191194
192 if ssh_authorized_keys:195 if ssh_authorized_keys:
193 data[b'ssh_authorized_keys'] = ssh_authorized_keys196 data['ssh_authorized_keys'] = ssh_authorized_keys
194197
195 if args.password:198 if args.password:
196 data[b'password'] = args.password.encode('utf-8')199 data['password'] = args.password
197 data[b'chpasswd'] = {b'expire': False}200 data['chpasswd'] = {'expire': False}
198 data[b'ssh_pwauth'] = True201 data['ssh_pwauth'] = True
199202
200 if args.run_script_once:203 if args.run_script_once:
201 data[b'runcmd'] = run_script_once_args_to_config(args.run_script_once)204 data['runcmd'] = run_script_once_args_to_config(args.run_script_once)
202205
203 if args.packages:206 if args.packages:
204 data[b'packages'] = [207 data['packages'] = list(
205 s.encode('ascii') # Debian Policy dictates a-z,0-9,+,-,.208 itertools.chain(*[p.split(',') for p in args.packages])
206 for s in itertools.chain(*[p.split(',') for p in args.packages])209 )
207 ]
208210
209 fobj.write("#cloud-config\n")211 fobj.write(b"#cloud-config\n")
210 fobj.write(yaml.dump(data))212 fobj.write(yaml.dump(data).encode())
211213
212214
213def create_default_meta_data(fobj, args):215def create_default_meta_data(fobj, args):
214 data = {216 data = {
215 b'instance-id': str(uuid.uuid1()).encode('ascii'),217 'instance-id': str(uuid.uuid1()),
216 }218 }
217 fobj.write(yaml.dump(data))219 fobj.write(yaml.dump(data).encode())
218220
219221
220def create_ds_image(temp_dir, hostname, user_data_fobj, meta_data_fobj):222def create_ds_image(
223 temp_dir,
224 hostname,
225 user_data_fobj,
226 meta_data_fobj,
227 network_config_fobj=None
228 ):
221 """Create a file called ds.img inside temp_dir that contains a useful229 """Create a file called ds.img inside temp_dir that contains a useful
222 cloud-init data source.230 cloud-init data source.
223231
224 Other temporary files created in temp_dir are currently metadata and232 Other temporary files created in temp_dir are currently metadata and
225 userdata and can be safely deleted.233 userdata and network-config and can be safely deleted.
226234
227 """235 """
228236
@@ -230,17 +238,35 @@ def create_ds_image(temp_dir, hostname, user_data_fobj, meta_data_fobj):
230 f.write(user_data_fobj.read())238 f.write(user_data_fobj.read())
231 with open(os.path.join(temp_dir, 'metadata'), 'wb') as f:239 with open(os.path.join(temp_dir, 'metadata'), 'wb') as f:
232 f.write(meta_data_fobj.read())240 f.write(meta_data_fobj.read())
233241 if network_config_fobj:
234 subprocess.check_call(242 with open(os.path.join(temp_dir, 'network-config'), 'wb') as f:
235 ['cloud-localds', '--disk-format=qcow2', 'ds.img', 'userdata', 'metadata'], cwd=temp_dir)243 f.write(network_config_fobj.read())
236244
237245 args = ['cloud-localds', '--disk-format=qcow2']
238def create_ds_volume(new_volume_name, hostname, user_data_fobj, meta_data_fobj):246 if network_config_fobj:
247 args.extend(['--network-config', 'network-config'])
248 args.extend(['ds.img', 'userdata', 'metadata'])
249 subprocess.check_call(args, cwd=temp_dir)
250
251
252def create_ds_volume(
253 new_volume_name,
254 hostname,
255 user_data_fobj,
256 meta_data_fobj,
257 network_config_fobj=None
258 ):
239 """Create a new libvirt cloud-init datasource volume."""259 """Create a new libvirt cloud-init datasource volume."""
240260
241 temp_dir = tempfile.mkdtemp(prefix='uvt-kvm-')261 temp_dir = tempfile.mkdtemp(prefix='uvt-kvm-')
242 try:262 try:
243 create_ds_image(temp_dir, hostname, user_data_fobj, meta_data_fobj)263 create_ds_image(
264 temp_dir=temp_dir,
265 hostname=hostname,
266 user_data_fobj=user_data_fobj,
267 meta_data_fobj=meta_data_fobj,
268 network_config_fobj=network_config_fobj,
269 )
244 with open(os.path.join(temp_dir, 'ds.img'), 'rb') as f:270 with open(os.path.join(temp_dir, 'ds.img'), 'rb') as f:
245 return uvtool.libvirt.create_volume_from_fobj(271 return uvtool.libvirt.create_volume_from_fobj(
246 new_volume_name, f, image_type='qcow2', pool_name=POOL_NAME)272 new_volume_name, f, image_type='qcow2', pool_name=POOL_NAME)
@@ -301,12 +327,12 @@ def create_cow_volume_by_path(backing_volume_path, new_volume_name,
301 E.format(type='qcow2'),327 E.format(type='qcow2'),
302 )328 )
303 )329 )
304 return pool.createXML(etree.tostring(new_vol), 0)330 return pool.createXML(etree.tostring(new_vol, encoding=str), 0)
305331
306332
307def compose_domain_xml(name, volumes, template_path, cpu=1, memory=512,333def compose_domain_xml(name, volumes, template_path, cpu=1, memory=512,
308 unsafe_caching=False, log_console_output=False, host_passthrough=False,334 unsafe_caching=False, log_console_output=False, host_passthrough=False,
309 bridge=None, ssh_known_hosts=None):335 machine_type=None, bridge=None, mac=None, ssh_known_hosts=None):
310 tree = etree.parse(template_path)336 tree = etree.parse(template_path)
311 domain = tree.getroot()337 domain = tree.getroot()
312 assert domain.tag == 'domain'338 assert domain.tag == 'domain'
@@ -351,11 +377,28 @@ def compose_domain_xml(name, volumes, template_path, cpu=1, memory=512,
351377
352 if bridge:378 if bridge:
353 etree.strip_elements(devices, 'interface')379 etree.strip_elements(devices, 'interface')
354 devices.append(E.interface(380 if mac:
355 E.source(bridge=bridge),381 devices.append(E.interface(
356 E.model(type='virtio'),382 E.mac(address=mac),
357 type='bridge'),383 E.source(bridge=bridge),
358 )384 E.model(type='virtio'),
385 type='bridge'),
386 )
387 else:
388 devices.append(E.interface(
389 E.source(bridge=bridge),
390 E.model(type='virtio'),
391 type='bridge'),
392 )
393 else:
394 if mac:
395 etree.strip_elements(devices, 'interface')
396 devices.append(E.interface(
397 E.mac(address=mac),
398 E.source(network='default'),
399 E.model(type='virtio'),
400 type='network'),
401 )
359402
360 if log_console_output:403 if log_console_output:
361 if ARCH == 's390x':404 if ARCH == 's390x':
@@ -378,6 +421,11 @@ def compose_domain_xml(name, volumes, template_path, cpu=1, memory=512,
378 else:421 else:
379 etree.SubElement(domain, 'cpu', mode='host-passthrough')422 etree.SubElement(domain, 'cpu', mode='host-passthrough')
380423
424 if machine_type:
425 domos = domain.find('os')
426 domtype = domos.find('type')
427 domtype.set('machine', machine_type)
428
381 if ssh_known_hosts:429 if ssh_known_hosts:
382 metadata = domain.find('metadata')430 metadata = domain.find('metadata')
383 if metadata is None:431 if metadata is None:
@@ -389,7 +437,7 @@ def compose_domain_xml(name, volumes, template_path, cpu=1, memory=512,
389 )437 )
390 metadata.append(EX.ssh_known_hosts(ssh_known_hosts))438 metadata.append(EX.ssh_known_hosts(ssh_known_hosts))
391439
392 return etree.tostring(tree)440 return etree.tostring(tree, encoding=str)
393441
394442
395def get_base_image(filters):443def get_base_image(filters):
@@ -403,11 +451,27 @@ def get_base_image(filters):
403 return result[0]451 return result[0]
404452
405453
406def create(hostname, filters, user_data_fobj, meta_data_fobj, template_path,454def create(
407 memory=512, cpu=1, disk=2, unsafe_caching=False,455 hostname,
408 log_console_output=False, host_passthrough=False, bridge=None,456 filters,
409 backing_image_file=None, start=True, ssh_known_hosts=None,457 user_data_fobj,
410 ephemeral_disks=None):458 meta_data_fobj,
459 network_config_fobj,
460 template_path,
461 memory=512,
462 cpu=1,
463 disk=2,
464 unsafe_caching=False,
465 log_console_output=False,
466 host_passthrough=False,
467 bridge=None,
468 mac=None,
469 backing_image_file=None,
470 start=True,
471 ssh_known_hosts=None,
472 ephemeral_disks=None,
473 machine_type=None
474 ):
411 if backing_image_file is None:475 if backing_image_file is None:
412 base_volume_name = get_base_image(filters)476 base_volume_name = get_base_image(filters)
413 if ephemeral_disks is None:477 if ephemeral_disks is None:
@@ -430,7 +494,12 @@ def create(hostname, filters, user_data_fobj, meta_data_fobj, template_path,
430 undo_volume_creation.append(main_vol)494 undo_volume_creation.append(main_vol)
431495
432 ds_vol = create_ds_volume(496 ds_vol = create_ds_volume(
433 "%s-ds.qcow" % hostname, hostname, user_data_fobj, meta_data_fobj)497 "%s-ds.qcow" % hostname,
498 hostname,
499 user_data_fobj,
500 meta_data_fobj,
501 network_config_fobj,
502 )
434 undo_volume_creation.append(ds_vol)503 undo_volume_creation.append(ds_vol)
435504
436 volumes = [main_vol, ds_vol]505 volumes = [main_vol, ds_vol]
@@ -443,6 +512,7 @@ def create(hostname, filters, user_data_fobj, meta_data_fobj, template_path,
443 xml = compose_domain_xml(512 xml = compose_domain_xml(
444 hostname, volumes=volumes,513 hostname, volumes=volumes,
445 bridge=bridge,514 bridge=bridge,
515 mac=mac,
446 cpu=cpu,516 cpu=cpu,
447 log_console_output=log_console_output,517 log_console_output=log_console_output,
448 host_passthrough=host_passthrough,518 host_passthrough=host_passthrough,
@@ -450,6 +520,7 @@ def create(hostname, filters, user_data_fobj, meta_data_fobj, template_path,
450 template_path=template_path,520 template_path=template_path,
451 unsafe_caching=unsafe_caching,521 unsafe_caching=unsafe_caching,
452 ssh_known_hosts=ssh_known_hosts,522 ssh_known_hosts=ssh_known_hosts,
523 machine_type=machine_type,
453 )524 )
454 conn = libvirt.open('qemu:///system')525 conn = libvirt.open('qemu:///system')
455 domain = conn.defineXML(xml)526 domain = conn.defineXML(xml)
@@ -510,7 +581,7 @@ def destroy(hostname):
510581
511def get_lts_series():582def get_lts_series():
512 output = subprocess.check_output(['distro-info', '--lts'], close_fds=True)583 output = subprocess.check_output(['distro-info', '--lts'], close_fds=True)
513 return output.strip()584 return output.strip().decode()
514585
515586
516def apply_default_fobj(args, key, create_default_data_fn):587def apply_default_fobj(args, key, create_default_data_fn):
@@ -528,7 +599,7 @@ def apply_default_fobj(args, key, create_default_data_fn):
528 if specified_fobj:599 if specified_fobj:
529 return specified_fobj600 return specified_fobj
530 else:601 else:
531 default_fobj = StringIO.StringIO()602 default_fobj = io.BytesIO()
532 create_default_data_fn(default_fobj, args)603 create_default_data_fn(default_fobj, args)
533 default_fobj.seek(0)604 default_fobj.seek(0)
534 return default_fobj605 return default_fobj
@@ -583,6 +654,7 @@ def ssh(name, login_name, arguments, stdin=None, checked=False, sysexit=True,
583 )654 )
584 if ssh_known_hosts:655 if ssh_known_hosts:
585 ssh_known_hosts_file = tempfile.NamedTemporaryFile(656 ssh_known_hosts_file = tempfile.NamedTemporaryFile(
657 mode='w',
586 prefix='uvt-kvm.known_hoststmp')658 prefix='uvt-kvm.known_hoststmp')
587 objects_to_close.append(ssh_known_hosts_file)659 objects_to_close.append(ssh_known_hosts_file)
588 ssh_known_hosts_file.write(ssh_known_hosts)660 ssh_known_hosts_file.write(ssh_known_hosts)
@@ -662,9 +734,14 @@ def main_create(parser, args):
662 else:734 else:
663 abs_image_backing_file = None735 abs_image_backing_file = None
664 create(736 create(
665 args.hostname, args.filters, user_data_fobj, meta_data_fobj,737 hostname=args.hostname,
738 filters=args.filters,
739 user_data_fobj=user_data_fobj,
740 meta_data_fobj=meta_data_fobj,
741 network_config_fobj=args.network_config,
666 backing_image_file=abs_image_backing_file,742 backing_image_file=abs_image_backing_file,
667 bridge=args.bridge,743 bridge=args.bridge,
744 mac=args.mac,
668 cpu=args.cpu,745 cpu=args.cpu,
669 disk=args.disk,746 disk=args.disk,
670 log_console_output=args.log_console_output,747 log_console_output=args.log_console_output,
@@ -675,6 +752,7 @@ def main_create(parser, args):
675 start=not args.no_start,752 start=not args.no_start,
676 ssh_known_hosts=ssh_known_hosts,753 ssh_known_hosts=ssh_known_hosts,
677 ephemeral_disks=args.ephemeral_disks,754 ephemeral_disks=args.ephemeral_disks,
755 machine_type=args.machine_type,
678 )756 )
679757
680758
@@ -789,10 +867,16 @@ class DeveloperOptionAction(argparse.Action):
789def main(args):867def main(args):
790 # Workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1063766868 # Workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1063766
791 # (LP: #1228231)869 # (LP: #1228231)
792 libvirt.registerErrorHandler(lambda _: None, None)870 def noop(*args, **kwargs): pass
871 libvirt.registerErrorHandler(noop, None)
793872
794 parser = argparse.ArgumentParser()873 parser = argparse.ArgumentParser()
795 subparsers = parser.add_subparsers()874 # Workarounds needed here: set dest and required
875 # https://bugs.python.org/issue9253#msg181855
876 # https://stackoverflow.com/q/22990977/478206
877 # https://stackoverflow.com/a/23354355/478206
878 subparsers = parser.add_subparsers(dest='subcommand')
879 subparsers.required = True
796 create_subparser = subparsers.add_parser('create')880 create_subparser = subparsers.add_parser('create')
797 create_subparser.set_defaults(func=main_create)881 create_subparser.set_defaults(func=main_create)
798 create_subparser.add_argument(882 create_subparser.add_argument(
@@ -805,16 +889,20 @@ def main(args):
805 '--ephemeral-disk', action='append', type=int, dest='ephemeral_disks',889 '--ephemeral-disk', action='append', type=int, dest='ephemeral_disks',
806 help='Add an empty disk of SIZE in GB', metavar='SIZE')890 help='Add an empty disk of SIZE in GB', metavar='SIZE')
807 create_subparser.add_argument('--bridge')891 create_subparser.add_argument('--bridge')
892 create_subparser.add_argument('--mac')
808 create_subparser.add_argument('--unsafe-caching', action='store_true')893 create_subparser.add_argument('--unsafe-caching', action='store_true')
809 create_subparser.add_argument(894 create_subparser.add_argument(
810 '--user-data', type=argparse.FileType('rb'))895 '--user-data', type=argparse.FileType('rb'))
811 create_subparser.add_argument(896 create_subparser.add_argument(
812 '--meta-data', type=argparse.FileType('rb'))897 '--meta-data', type=argparse.FileType('rb'))
898 create_subparser.add_argument(
899 '--network-config', type=argparse.FileType('rb'))
813 create_subparser.add_argument('--password')900 create_subparser.add_argument('--password')
814 create_subparser.add_argument('--guest-arch',901 create_subparser.add_argument('--guest-arch',
815 help='guest arch to select template, default is the host architecture')902 help='guest arch to select template, default is the host architecture')
816 create_subparser.add_argument('--log-console-output', action='store_true')903 create_subparser.add_argument('--log-console-output', action='store_true')
817 create_subparser.add_argument('--host-passthrough', action='store_true')904 create_subparser.add_argument('--host-passthrough', action='store_true')
905 create_subparser.add_argument('--machine-type')
818 create_subparser.add_argument('--backing-image-file')906 create_subparser.add_argument('--backing-image-file')
819 create_subparser.add_argument('--run-script-once', action='append')907 create_subparser.add_argument('--run-script-once', action='append')
820 create_subparser.add_argument('--ssh-public-key-file')908 create_subparser.add_argument('--ssh-public-key-file')
diff --git a/uvtool/libvirt/simplestreams.py b/uvtool/libvirt/simplestreams.py
index 93fd3fb..d4cb8bc 100755
--- a/uvtool/libvirt/simplestreams.py
+++ b/uvtool/libvirt/simplestreams.py
@@ -1,8 +1,8 @@
1#!/usr/bin/python1#!/usr/bin/python3
22
3# Keep Ubuntu Cloud images synced to a local libvirt storage pool.3# Keep Ubuntu Cloud images synced to a local libvirt storage pool.
44
5# Copyright (C) 2013 Canonical Ltd.5# Copyright (C) 2013-9 Canonical Ltd.
6# Author: Robie Basak <robie.basak@canonical.com>6# Author: Robie Basak <robie.basak@canonical.com>
7#7#
8# This program is free software: you can redistribute it and/or modify8# This program is free software: you can redistribute it and/or modify
@@ -107,7 +107,7 @@ BASE64_PREFIX = 'x-uvt-b64-'
107def _encode_libvirt_pool_name(product_name, version_name):107def _encode_libvirt_pool_name(product_name, version_name):
108 return BASE64_PREFIX + base64.b64encode(108 return BASE64_PREFIX + base64.b64encode(
109 (' '.join([product_name, version_name])).encode(), b'-_'109 (' '.join([product_name, version_name])).encode(), b'-_'
110 )110 ).decode()
111111
112112
113def _decode_libvirt_pool_name(encoded_pool_name):113def _decode_libvirt_pool_name(encoded_pool_name):
@@ -119,7 +119,7 @@ def _decode_libvirt_pool_name(encoded_pool_name):
119 return base64.b64decode(119 return base64.b64decode(
120 encoded_pool_name[len(BASE64_PREFIX):],120 encoded_pool_name[len(BASE64_PREFIX):],
121 altchars=b'-_'121 altchars=b'-_'
122 ).split(None, 1)122 ).decode().split(None, 1)
123123
124124
125def purge_pool(conn=None):125def purge_pool(conn=None):
@@ -164,16 +164,14 @@ def _load_products(path=None, content_id=None, clean=False):
164 def new_product():164 def new_product():
165 return {'versions': {}}165 return {'versions': {}}
166 products = collections.defaultdict(new_product)166 products = collections.defaultdict(new_product)
167 for encoded_libvirt_name_string, metadata in pool_metadata.items():167 for encoded_libvirt_name_string, metadata in list(pool_metadata.items()):
168 encoded_libvirt_name_bytes = encoded_libvirt_name_string.encode(
169 'utf-8')
170 if not uvtool.libvirt.have_volume_by_name(168 if not uvtool.libvirt.have_volume_by_name(
171 encoded_libvirt_name_bytes, pool_name=LIBVIRT_POOL_NAME):169 encoded_libvirt_name_string, pool_name=LIBVIRT_POOL_NAME):
172 if clean:170 if clean:
173 del pool_metadata[encoded_libvirt_name_string]171 del pool_metadata[encoded_libvirt_name_string]
174 continue172 continue
175 product, version = _decode_libvirt_pool_name(173 product, version = _decode_libvirt_pool_name(
176 encoded_libvirt_name_bytes)174 encoded_libvirt_name_string)
177 assert(product == metadata['product_name'])175 assert(product == metadata['product_name'])
178 assert(version == metadata['version_name'])176 assert(version == metadata['version_name'])
179 products[product]['versions'][version] = {177 products[product]['versions'][version] = {
@@ -292,13 +290,19 @@ def main_purge(args):
292def main(argv=None):290def main(argv=None):
293 # Workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1063766291 # Workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1063766
294 # (LP: #1228231)292 # (LP: #1228231)
295 libvirt.registerErrorHandler(lambda _: None, None)293 def noop(*args, **kwargs): pass
294 libvirt.registerErrorHandler(noop, None)
296295
297 system_arch = subprocess.check_output(296 system_arch = subprocess.check_output(
298 ['dpkg', '--print-architecture']).decode().strip()297 ['dpkg', '--print-architecture']).decode().strip()
299 parser = argparse.ArgumentParser()298 parser = argparse.ArgumentParser()
300 parser.add_argument('--verbose', '-v', action='store_true')299 parser.add_argument('--verbose', '-v', action='store_true')
301 subparsers = parser.add_subparsers()300 # Workarounds needed here: set dest and required
301 # https://bugs.python.org/issue9253#msg181855
302 # https://stackoverflow.com/q/22990977/478206
303 # https://stackoverflow.com/a/23354355/478206
304 subparsers = parser.add_subparsers(dest='subcommand')
305 subparsers.required = True
302306
303 sync_subparser = subparsers.add_parser('sync')307 sync_subparser = subparsers.add_parser('sync')
304 sync_subparser.set_defaults(func=main_sync)308 sync_subparser.set_defaults(func=main_sync)
@@ -312,7 +316,7 @@ def main(argv=None):
312 default='/usr/share/keyrings/ubuntu-cloudimage-keyring.gpg'316 default='/usr/share/keyrings/ubuntu-cloudimage-keyring.gpg'
313 )317 )
314 sync_subparser.add_argument('--source', dest='mirror_url',318 sync_subparser.add_argument('--source', dest='mirror_url',
315 default='https://cloud-images.ubuntu.com/releases/')319 default='http://cloud-images.ubuntu.com/releases/')
316 sync_subparser.add_argument('--no-authentication', action='store_true')320 sync_subparser.add_argument('--no-authentication', action='store_true')
317 sync_subparser.add_argument('filters', nargs='*', metavar='filter',321 sync_subparser.add_argument('filters', nargs='*', metavar='filter',
318 default=["arch=%s" % system_arch])322 default=["arch=%s" % system_arch])
diff --git a/uvtool/ssh.py b/uvtool/ssh.py
index bb6b6d4..0516603 100644
--- a/uvtool/ssh.py
+++ b/uvtool/ssh.py
@@ -1,6 +1,6 @@
1#!/usr/bin/python1#!/usr/bin/python3
22
3# Copyright (C) 2014 Canonical Ltd.3# Copyright (C) 2014-9 Canonical Ltd.
4# Author: Robie Basak <robie.basak@canonical.com>4# Author: Robie Basak <robie.basak@canonical.com>
5#5#
6# This program is free software: you can redistribute it and/or modify6# This program is free software: you can redistribute it and/or modify
@@ -36,7 +36,7 @@ def _keygen(key_type, private_path):
3636
3737
38def read_file(path):38def read_file(path):
39 with open(path, 'rb') as f:39 with open(path, 'r') as f:
40 return f.read()40 return f.read()
4141
4242
@@ -52,9 +52,8 @@ def generate_ssh_host_keys():
52 # ssh-keygen(1) defines that ".pub" is appended52 # ssh-keygen(1) defines that ".pub" is appended
53 public_path = private_path + ".pub"53 public_path = private_path + ".pub"
5454
55 key_type_utf8 = key_type.encode('utf-8')55 private_ci_key = key_type + '_private'
56 private_ci_key = key_type_utf8 + b'_private'56 public_ci_key = key_type + '_public'
57 public_ci_key = key_type_utf8 + b'_public'
5857
59 private_key = read_file(private_path)58 private_key = read_file(private_path)
60 public_key = read_file(public_path)59 public_key = read_file(public_path)
@@ -66,4 +65,4 @@ def generate_ssh_host_keys():
66 finally:65 finally:
67 shutil.rmtree(tmp_dir)66 shutil.rmtree(tmp_dir)
6867
69 return cloud_init_result, b''.join(known_hosts_result)68 return cloud_init_result, ''.join(known_hosts_result)
diff --git a/uvtool/tests/test_kvm.py b/uvtool/tests/test_kvm.py
index 7a96ebf..13d7da6 100644
--- a/uvtool/tests/test_kvm.py
+++ b/uvtool/tests/test_kvm.py
@@ -1,4 +1,4 @@
1# Copyright (C) 2014 Canonical Ltd.1# Copyright (C) 2014-9 Canonical Ltd.
2# Author: Robie Basak <robie.basak@canonical.com>2# Author: Robie Basak <robie.basak@canonical.com>
3#3#
4# This program is free software: you can redistribute it and/or modify4# This program is free software: you can redistribute it and/or modify
@@ -15,8 +15,7 @@
15# along with this program. If not, see <http://www.gnu.org/licenses/>.15# along with this program. If not, see <http://www.gnu.org/licenses/>.
1616
17import unittest17import unittest
1818import unittest.mock
19import mock
2019
21from uvtool.libvirt.kvm import main_ssh20from uvtool.libvirt.kvm import main_ssh
2221
@@ -25,18 +24,18 @@ class TestKVM(unittest.TestCase):
25 def check_ssh(self, args_hostname, args_login_name, expected_hostname,24 def check_ssh(self, args_hostname, args_login_name, expected_hostname,
26 expected_login_name):25 expected_login_name):
2726
28 parser = mock.Mock()27 parser = unittest.mock.Mock()
29 args = mock.Mock()28 args = unittest.mock.Mock()
30 args.login_name = args_login_name29 args.login_name = args_login_name
31 args.name = args_hostname30 args.name = args_hostname
32 args.ssh_arguments = mock.sentinel.ssh_arguments31 args.ssh_arguments = unittest.mock.sentinel.ssh_arguments
33 args.insecure = True32 args.insecure = True
34 with mock.patch('uvtool.libvirt.kvm.ssh') as ssh_mock:33 with unittest.mock.patch('uvtool.libvirt.kvm.ssh') as ssh_mock:
35 main_ssh(parser, args)34 main_ssh(parser, args)
36 ssh_mock.assert_called_with(35 ssh_mock.assert_called_with(
37 expected_hostname,36 expected_hostname,
38 expected_login_name,37 expected_login_name,
39 mock.sentinel.ssh_arguments,38 unittest.mock.sentinel.ssh_arguments,
40 insecure=True,39 insecure=True,
41 )40 )
4241
diff --git a/uvtool/tests/test_simplestreams.py b/uvtool/tests/test_simplestreams.py
index 211454d..883391e 100644
--- a/uvtool/tests/test_simplestreams.py
+++ b/uvtool/tests/test_simplestreams.py
@@ -1,4 +1,4 @@
1# Copyright (C) 2014 Canonical Ltd.1# Copyright (C) 2014-9 Canonical Ltd.
2# Author: Robie Basak <robie.basak@canonical.com>2# Author: Robie Basak <robie.basak@canonical.com>
3#3#
4# This program is free software: you can redistribute it and/or modify4# This program is free software: you can redistribute it and/or modify
@@ -15,8 +15,7 @@
15# along with this program. If not, see <http://www.gnu.org/licenses/>.15# along with this program. If not, see <http://www.gnu.org/licenses/>.
1616
17import unittest17import unittest
1818import unittest.mock
19import mock
2019
21import uvtool.libvirt.simplestreams as simplestreams20import uvtool.libvirt.simplestreams as simplestreams
2221
@@ -28,7 +27,7 @@ import uvtool.libvirt.simplestreams as simplestreams
28# NormalizedVersion as described in PEP-0386. As this is a temporary hack27# NormalizedVersion as described in PEP-0386. As this is a temporary hack
29# anyway, this will do for now, since we know that the mock version on Ubuntu28# anyway, this will do for now, since we know that the mock version on Ubuntu
30# Precise will never change.29# Precise will never change.
31ON_PRECISE = mock.__version__ == '0.7.2'30ON_PRECISE = unittest.mock.__version__ == '0.7.2'
3231
33FAKE_VOLUME_PRODUCT_NAME = 'com.ubuntu.cloud:server:12.04:amd64'32FAKE_VOLUME_PRODUCT_NAME = 'com.ubuntu.cloud:server:12.04:amd64'
34FAKE_VOLUME_VERSION_0 = '20131119'33FAKE_VOLUME_VERSION_0 = '20131119'
@@ -39,9 +38,9 @@ ENCODED_FAKE_VOLUME_PRODUCT_NAME_1 = simplestreams._encode_libvirt_pool_name(
39 FAKE_VOLUME_PRODUCT_NAME, FAKE_VOLUME_VERSION_1)38 FAKE_VOLUME_PRODUCT_NAME, FAKE_VOLUME_VERSION_1)
4039
41@unittest.skipIf(ON_PRECISE, 'mock version is too old')40@unittest.skipIf(ON_PRECISE, 'mock version is too old')
42@mock.patch('uvtool.libvirt.simplestreams.uvtool.libvirt')41@unittest.mock.patch('uvtool.libvirt.simplestreams.uvtool.libvirt')
43@mock.patch('uvtool.libvirt.simplestreams.pool_metadata', new={})42@unittest.mock.patch('uvtool.libvirt.simplestreams.pool_metadata', new={})
44@mock.patch('uvtool.libvirt.simplestreams.libvirt')43@unittest.mock.patch('uvtool.libvirt.simplestreams.libvirt')
45class TestSimpleStreams(unittest.TestCase):44class TestSimpleStreams(unittest.TestCase):
46 def testSync(self, libvirt, uvtool_libvirt):45 def testSync(self, libvirt, uvtool_libvirt):
47 uvtool_libvirt.have_volume_by_name.return_value = False46 uvtool_libvirt.have_volume_by_name.return_value = False
@@ -61,7 +60,7 @@ class TestSimpleStreams(unittest.TestCase):
61 # least once by uvtool.libvirt.simplestreams directly. This is more of60 # least once by uvtool.libvirt.simplestreams directly. This is more of
62 # an assertion about the test being correct than part of the test61 # an assertion about the test being correct than part of the test
63 # itself.62 # itself.
64 libvirt.assert_has_calls([mock.call.open(u'qemu:///system')])63 libvirt.assert_has_calls([unittest.mock.call.open(u'qemu:///system')])
6564
66 # create_volume_from_fobj should have been called exactly once to65 # create_volume_from_fobj should have been called exactly once to
67 # create the volume with the name that we expect66 # create the volume with the name that we expect

Subscribers

People subscribed via source and target branches