Merge ~newell-jensen/maas:virsh-pod-driver-networking into maas:master
- Git
- lp:~newell-jensen/maas
- virsh-pod-driver-networking
- Merge into master
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) |
Related bugs: |
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.
Description of the change
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b virsh-pod-
STATUS: SUCCESS
COMMIT: 04fffe008b2e438
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.
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b virsh-pod-
STATUS: SUCCESS
COMMIT: 81db38278b4522f
- a2ad45a... by Newell Jensen
-
Clean up template XMLs for macvlans and bridges.
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b virsh-pod-
STATUS: SUCCESS
COMMIT: a2ad45ac531df66
- 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.
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b virsh-pod-
STATUS: SUCCESS
COMMIT: a790e0acc169c96
- 17065cc... by Newell Jensen
-
Review fix.
Mike Pontillo (mpontillo) wrote : | # |
Thanks for the fixes. Let's land this!
Preview Diff
1 | diff --git a/src/provisioningserver/drivers/pod/__init__.py b/src/provisioningserver/drivers/pod/__init__.py |
2 | index 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 | |
19 | diff --git a/src/provisioningserver/drivers/pod/tests/test_base.py b/src/provisioningserver/drivers/pod/tests/test_base.py |
20 | index 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 | ]), |
40 | diff --git a/src/provisioningserver/drivers/pod/tests/test_virsh.py b/src/provisioningserver/drivers/pod/tests/test_virsh.py |
41 | index 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( |
194 | diff --git a/src/provisioningserver/drivers/pod/virsh.py b/src/provisioningserver/drivers/pod/virsh.py |
195 | index 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) |
UNIT TESTS driver- networking lp:~newell-jensen/maas/+git/maas into -b master lp:~maas-committers/maas
-b virsh-pod-
STATUS: SUCCESS dbba9cad945f29d d0c07dd100
COMMIT: c1077fe6c86f788