Merge ~newell-jensen/maas:virsh-pod-driver-networking into maas:master

Proposed by Newell Jensen
Status: Merged
Approved by: Newell Jensen
Approved revision: 17065cc431867986dad57d15d9d42aa7463eb75f
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~newell-jensen/maas:virsh-pod-driver-networking
Merge into: maas:master
Diff against target: 283 lines (+154/-18)
4 files modified
src/provisioningserver/drivers/pod/__init__.py (+2/-2)
src/provisioningserver/drivers/pod/tests/test_base.py (+4/-4)
src/provisioningserver/drivers/pod/tests/test_virsh.py (+91/-6)
src/provisioningserver/drivers/pod/virsh.py (+57/-6)
Reviewer Review Type Date Requested Status
Mike Pontillo (community) Approve
MAAS Lander Approve
Review via email: mp+349189@code.launchpad.net

Commit message

Update attach_interface method in virsh pod driver to deal with macvlan, bridge, and explicit network interface attachments. This will allow VMs to be composed with these different interface types.

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b virsh-pod-driver-networking lp:~newell-jensen/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: c1077fe6c86f788dbba9cad945f29dd0c07dd100

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b virsh-pod-driver-networking lp:~newell-jensen/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 04fffe008b2e438f85f15dd1a167cce7a4ead0e2

review: Approve
Revision history for this message
Mike Pontillo (mpontillo) wrote :

I'm going to E2E test this before I comment further.

One thing I noticed though: I'm not sure bridge attachment is going to belong in the same code path as network attachment (the current default).

That is, if I specify a bridge to attach to, I'm going to provide a bridge name that I'd like the VM to be attached to. That might be something like 'br0'. Whereas if I'm attaching to a /network/, in libvirt terms, that's going to be something like 'default' or 'maas', which may in turn attach to a bridge at the end of the operation (such as 'virbr0').

So I think we need an XML-attachment path similar to macvlan for the bridge use case.

review: Needs Information
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b virsh-pod-driver-networking lp:~newell-jensen/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 81db38278b4522f6372600f22fb26f51f4454525

review: Approve
a2ad45a... by Newell Jensen

Clean up template XMLs for macvlans and bridges.

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b virsh-pod-driver-networking lp:~newell-jensen/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: a2ad45ac531df66fdf64da15f866fe5c011bce79

review: Approve
a790e0a... by Newell Jensen

Add network attachment type. This is the default if nothing is specified but we should also be allowing network attachment types to be explicitly defined.

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b virsh-pod-driver-networking lp:~newell-jensen/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: a790e0acc169c9688fdd200d77b62cb9dacf0690

review: Approve
17065cc... by Newell Jensen

Review fix.

Revision history for this message
Mike Pontillo (mpontillo) wrote :

Thanks for the fixes. Let's land this!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/provisioningserver/drivers/pod/__init__.py b/src/provisioningserver/drivers/pod/__init__.py
2index ff413ce..602b893 100644
3--- a/src/provisioningserver/drivers/pod/__init__.py
4+++ b/src/provisioningserver/drivers/pod/__init__.py
5@@ -296,11 +296,11 @@ class RequestedMachineBlockDevice(AttrHelperMixin):
6 @attr.s
7 class RequestedMachineInterface(AttrHelperMixin):
8 """Requested machine interface information."""
9- attach_ifname = attr.ib(
10+ attach_name = attr.ib(
11 converter=converter_obj(str, optional=True), default=None)
12 attach_type = attr.ib(
13 converter=converter_obj(str, optional=True), default=None)
14- attach_type_mode = attr.ib(
15+ attach_options = attr.ib(
16 converter=converter_obj(str, optional=True), default=None)
17
18
19diff --git a/src/provisioningserver/drivers/pod/tests/test_base.py b/src/provisioningserver/drivers/pod/tests/test_base.py
20index b38b42b..866b7eb 100644
21--- a/src/provisioningserver/drivers/pod/tests/test_base.py
22+++ b/src/provisioningserver/drivers/pod/tests/test_base.py
23@@ -619,12 +619,12 @@ class TestRequestClasses(MAASTestCase):
24 "memory": Equals(memory),
25 "interfaces": MatchesListwise([
26 MatchesDict({
27- "attach_ifname": Equals(
28- interface.attach_ifname),
29+ "attach_name": Equals(
30+ interface.attach_name),
31 "attach_type": Equals(
32 interface.attach_type),
33- "attach_type_mode": Equals(
34- interface.attach_type_mode),
35+ "attach_options": Equals(
36+ interface.attach_options),
37 })
38 for interface in interfaces
39 ]),
40diff --git a/src/provisioningserver/drivers/pod/tests/test_virsh.py b/src/provisioningserver/drivers/pod/tests/test_virsh.py
41index a46f0a0..fe4a766 100644
42--- a/src/provisioningserver/drivers/pod/tests/test_virsh.py
43+++ b/src/provisioningserver/drivers/pod/tests/test_virsh.py
44@@ -39,6 +39,8 @@ from provisioningserver.drivers.pod.virsh import (
45 ARCH_FIX_REVERSE,
46 DOM_TEMPLATE_AMD64,
47 DOM_TEMPLATE_ARM64,
48+ DOM_TEMPLATE_BRIDGE_INTERFACE,
49+ DOM_TEMPLATE_MACVLAN_INTERFACE,
50 DOM_TEMPLATE_PPC64,
51 DOM_TEMPLATE_S390X,
52 VirshPodDriver,
53@@ -1236,19 +1238,102 @@ class TestVirshSSH(MAASTestCase):
54 self.patch(virsh.VirshSSH, "get_network_list").return_value = []
55 self.assertRaises(PodInvalidResources, conn.get_best_network)
56
57- def test_attach_interface(self):
58+ def test_attach_interface_defaults_to_network_attachment(self):
59 conn = self.configure_virshssh('')
60 domain = factory.make_name('domain')
61 network = factory.make_name('network')
62 mock_run = self.patch(virsh.VirshSSH, "run")
63 fake_mac = factory.make_mac_address()
64+ interface = RequestedMachineInterface()
65 self.patch(virsh, 'generate_mac_address').return_value = fake_mac
66- conn.attach_interface(domain, network)
67+ conn.attach_interface(interface, domain, network)
68 self.assertThat(mock_run, MockCalledOnceWith([
69 'attach-interface', domain, 'network', network,
70 '--mac', fake_mac,
71 '--model', 'virtio', '--config']))
72
73+ def test_attach_interface_calls_attaches_network(self):
74+ conn = self.configure_virshssh('')
75+ domain = factory.make_name('domain')
76+ network = factory.make_name('network')
77+ mock_run = self.patch(virsh.VirshSSH, "run")
78+ fake_mac = factory.make_mac_address()
79+ interface = RequestedMachineInterface(
80+ attach_name=network, attach_type='network')
81+ self.patch(virsh, 'generate_mac_address').return_value = fake_mac
82+ conn.attach_interface(interface, domain, network)
83+ self.assertThat(mock_run, MockCalledOnceWith([
84+ 'attach-interface', domain, 'network', network,
85+ '--mac', fake_mac,
86+ '--model', 'virtio', '--config']))
87+
88+ def test_attach_interface_attaches_macvlan(self):
89+ conn = self.configure_virshssh('')
90+ domain = factory.make_name('domain')
91+ network = factory.make_name('network')
92+ mock_run = self.patch(virsh.VirshSSH, "run")
93+ fake_mac = factory.make_mac_address()
94+ interface = RequestedMachineInterface(
95+ attach_name=factory.make_name('name'),
96+ attach_type='macvlan', attach_options=random.choice([
97+ 'private', 'vepa', 'bridge', 'passthru']))
98+ self.patch(virsh, 'generate_mac_address').return_value = fake_mac
99+ NamedTemporaryFile = self.patch(virsh_module, "NamedTemporaryFile")
100+ tmpfile = NamedTemporaryFile.return_value
101+ tmpfile.__enter__.return_value = tmpfile
102+ tmpfile.name = factory.make_name("filename")
103+ conn.attach_interface(interface, domain, network)
104+
105+ device_params = {
106+ 'mac_address': fake_mac,
107+ 'attach_name': interface.attach_name,
108+ 'attach_options': interface.attach_options,
109+ }
110+ self.assertThat(mock_run, MockCalledOnceWith([
111+ 'attach-device', domain, ANY, '--config']))
112+ self.assertThat(
113+ NamedTemporaryFile, MockCalledOnceWith())
114+ self.assertThat(tmpfile.__enter__, MockCalledOnceWith())
115+ self.assertThat(tmpfile.write, MockCallsMatch(
116+ call(DOM_TEMPLATE_MACVLAN_INTERFACE.format(
117+ **device_params).encode('utf-8')),
118+ call(b'\n')))
119+ self.assertThat(tmpfile.flush, MockCalledOnceWith())
120+ self.assertThat(tmpfile.__exit__, MockCalledOnceWith(None, None, None))
121+
122+ def test_attach_interface_attaches_bridge(self):
123+ conn = self.configure_virshssh('')
124+ domain = factory.make_name('domain')
125+ network = factory.make_name('network')
126+ mock_run = self.patch(virsh.VirshSSH, "run")
127+ fake_mac = factory.make_mac_address()
128+ interface = RequestedMachineInterface(
129+ attach_name=factory.make_name('ifname'),
130+ attach_type='bridge', attach_options=random.choice([
131+ 'private', 'vepa', 'bridge', 'passthru']))
132+ self.patch(virsh, 'generate_mac_address').return_value = fake_mac
133+ NamedTemporaryFile = self.patch(virsh_module, "NamedTemporaryFile")
134+ tmpfile = NamedTemporaryFile.return_value
135+ tmpfile.__enter__.return_value = tmpfile
136+ tmpfile.name = factory.make_name("filename")
137+ conn.attach_interface(interface, domain, network)
138+
139+ device_params = {
140+ 'mac_address': fake_mac,
141+ 'attach_name': interface.attach_name,
142+ }
143+ self.assertThat(mock_run, MockCalledOnceWith([
144+ 'attach-device', domain, ANY, '--config']))
145+ self.assertThat(
146+ NamedTemporaryFile, MockCalledOnceWith())
147+ self.assertThat(tmpfile.__enter__, MockCalledOnceWith())
148+ self.assertThat(tmpfile.write, MockCallsMatch(
149+ call(DOM_TEMPLATE_BRIDGE_INTERFACE.format(
150+ **device_params).encode('utf-8')),
151+ call(b'\n')))
152+ self.assertThat(tmpfile.flush, MockCalledOnceWith())
153+ self.assertThat(tmpfile.__exit__, MockCalledOnceWith(None, None, None))
154+
155 def test_get_domain_capabilities_for_kvm(self):
156 conn = self.configure_virshssh(SAMPLE_CAPABILITY_KVM)
157 self.assertEqual({
158@@ -1387,7 +1472,7 @@ class TestVirshSSH(MAASTestCase):
159 mock_attach_disk,
160 MockCalledOnceWith(ANY, disk_info[0], disk_info[1], 'vda'))
161 self.assertThat(
162- mock_attach_nic, MockCalledOnceWith(ANY, 'maas'))
163+ mock_attach_nic, MockCalledOnceWith(ANY, ANY, 'maas'))
164 self.assertThat(
165 mock_set_machine_autostart, MockCalledOnceWith(request.hostname))
166 self.assertThat(
167@@ -1446,7 +1531,7 @@ class TestVirshSSH(MAASTestCase):
168 mock_attach_disk,
169 MockCalledOnceWith(ANY, disk_info[0], disk_info[1], 'vda'))
170 self.assertThat(
171- mock_attach_nic, MockCalledOnceWith(ANY, 'maas'))
172+ mock_attach_nic, MockCalledOnceWith(ANY, ANY, 'maas'))
173 self.assertThat(
174 mock_set_machine_autostart, MockCalledOnceWith(request.hostname))
175 self.assertThat(
176@@ -1505,7 +1590,7 @@ class TestVirshSSH(MAASTestCase):
177 mock_attach_disk,
178 MockCalledOnceWith(ANY, disk_info[0], disk_info[1], 'vda'))
179 self.assertThat(
180- mock_attach_nic, MockCalledOnceWith(ANY, 'maas'))
181+ mock_attach_nic, MockCalledOnceWith(ANY, ANY, 'maas'))
182 self.assertThat(
183 mock_set_machine_autostart, MockCalledOnceWith(request.hostname))
184 self.assertThat(
185@@ -1564,7 +1649,7 @@ class TestVirshSSH(MAASTestCase):
186 mock_attach_disk,
187 MockCalledOnceWith(ANY, disk_info[0], disk_info[1], 'vda'))
188 self.assertThat(
189- mock_attach_nic, MockCalledOnceWith(ANY, 'maas'))
190+ mock_attach_nic, MockCalledOnceWith(ANY, ANY, 'maas'))
191 self.assertThat(
192 mock_set_machine_autostart, MockCalledOnceWith(request.hostname))
193 self.assertThat(
194diff --git a/src/provisioningserver/drivers/pod/virsh.py b/src/provisioningserver/drivers/pod/virsh.py
195index a19e0aa..42914c0 100644
196--- a/src/provisioningserver/drivers/pod/virsh.py
197+++ b/src/provisioningserver/drivers/pod/virsh.py
198@@ -64,6 +64,21 @@ XPATH_POOL_CAPACITY = "/pool/capacity"
199 XPATH_POOL_PATH = "/pool/target/path"
200 XPATH_POOL_UUID = "/pool/uuid"
201
202+DOM_TEMPLATE_MACVLAN_INTERFACE = dedent("""\
203+ <interface type='direct'>
204+ <source dev='{attach_name}' mode='{attach_options}'/>
205+ <mac address='{mac_address}'/>
206+ <model type='virtio'/>
207+ </interface>
208+ """)
209+
210+DOM_TEMPLATE_BRIDGE_INTERFACE = dedent("""\
211+ <interface type='bridge'>
212+ <source bridge='{attach_name}'/>
213+ <mac address='{mac_address}'/>
214+ <model type='virtio'/>
215+ </interface>
216+ """)
217
218 DOM_TEMPLATE_AMD64 = dedent("""\
219 <domain type='{type}'>
220@@ -834,12 +849,48 @@ class VirshSSH(pexpect.spawn):
221
222 return networks[0]
223
224- def attach_interface(self, domain, network):
225+ def attach_interface(self, interface, domain, network):
226 """Attach new network interface on `domain` to `network`."""
227 mac = generate_mac_address()
228- self.run([
229- 'attach-interface', domain, 'network', network,
230- '--mac', mac, '--model', 'virtio', '--config'])
231+ # If attachment type is not specified, default to network.
232+ if interface.attach_type in (None, 'network'):
233+ # Set the network if we are explicity attaching a network
234+ # specified by the user.
235+ if interface.attach_type is not None:
236+ network = interface.attach_name
237+ return self.run([
238+ 'attach-interface', domain, 'network', network,
239+ '--mac', mac, '--model', 'virtio', '--config'])
240+
241+ # For macvlans and bridges, we need to pass an XML template to
242+ # virsh's attach-device command since the attach-interface
243+ # command doesn't have a flag for setting the macvlan's mode.
244+ device_params = {
245+ 'mac_address': mac,
246+ 'attach_name': interface.attach_name,
247+ }
248+ if interface.attach_type == 'macvlan':
249+ device_params['attach_options'] = interface.attach_options
250+ device_xml = DOM_TEMPLATE_MACVLAN_INTERFACE.format(**device_params)
251+ if interface.attach_type == 'bridge':
252+ device_xml = DOM_TEMPLATE_BRIDGE_INTERFACE.format(**device_params)
253+
254+ # Rewrite the XML in a temporary file to use with 'virsh define'.
255+ with NamedTemporaryFile() as f:
256+ f.write(device_xml.encode('utf-8'))
257+ f.write(b'\n')
258+ f.flush()
259+ output = self.run([
260+ 'attach-device', domain, f.name, '--config'])
261+ if output.startswith('error:'):
262+ maaslog.error(
263+ "%s: Failed to attach network device %s" % (
264+ domain, interface.attach_name))
265+ return False
266+ maaslog.info(
267+ "%s: Successfully attached network device %s" % (
268+ domain, interface.attach_name))
269+ return True
270
271 def get_domain_capabilities(self):
272 """Return the domain capabilities.
273@@ -965,8 +1016,8 @@ class VirshSSH(pexpect.spawn):
274
275 # Attach new interfaces to the best possible network.
276 best_network = self.get_best_network()
277- for _ in request.interfaces:
278- self.attach_interface(request.hostname, best_network)
279+ for interface in request.interfaces:
280+ self.attach_interface(interface, request.hostname, best_network)
281
282 # Set machine to autostart.
283 self.set_machine_autostart(request.hostname)

Subscribers

People subscribed via source and target branches