Merge ~ltrager/maas:lp1851622 into maas:master

Proposed by Lee Trager
Status: Merged
Approved by: Lee Trager
Approved revision: 3ac1df98cd7957d6e4bfd6dea44a195ec99d4371
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~ltrager/maas:lp1851622
Merge into: maas:master
Diff against target: 437 lines (+199/-34)
5 files modified
src/maasserver/api/machines.py (+1/-3)
src/maasserver/api/tests/test_machine.py (+1/-2)
src/maasserver/models/node.py (+77/-17)
src/maasserver/models/tests/test_node.py (+115/-11)
src/metadataserver/tests/test_vendor_data.py (+5/-1)
Reviewer Review Type Date Requested Status
Newell Jensen (community) Approve
MAAS Lander Needs Fixing
Review via email: mp+375462@code.launchpad.net

Commit message

LP: #1851622 - Validate netplan usage before use in ephemeral environments.

Netplan is only supported in Bionic+. MAAS now prevents ephemeral deployments
and testing in Xenial. This also fixes a bug where MAAS attempted to apply
network configuration when commissioning a machine that had all of its
disks deleted.

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

UNIT TESTS
-b lp1851622 lp:~ltrager/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/6973/console
COMMIT: 21ccdd41d667e119f71e5b1eaac9a4756356f13f

review: Needs Fixing
~ltrager/maas:lp1851622 updated
75c3d57... by Lee Trager

Fix lint

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

UNIT TESTS
-b lp1851622 lp:~ltrager/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/6974/console
COMMIT: 75c3d57e3ae9ffbb0470587587de3cb0c9418556

review: Needs Fixing
Revision history for this message
Newell Jensen (newell-jensen) wrote :

LGTM

review: Approve
~ltrager/maas:lp1851622 updated
3ac1df9... by Lee Trager

Fix broken tests

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/api/machines.py b/src/maasserver/api/machines.py
2index 4f71844..5444773 100644
3--- a/src/maasserver/api/machines.py
4+++ b/src/maasserver/api/machines.py
5@@ -725,9 +725,7 @@ class MachineHandler(NodeHandler, OwnerDataMixin, PowerMixin):
6 NodePermission.admin, machine
7 ):
8 raise PermissionDenied()
9- if options.install_kvm and (
10- machine.ephemeral_deployment or options.ephemeral_deploy
11- ):
12+ if options.install_kvm and machine.ephemeral_deployment:
13 raise MAASAPIBadRequest(
14 "Cannot install KVM host for ephemeral deployments."
15 )
16diff --git a/src/maasserver/api/tests/test_machine.py b/src/maasserver/api/tests/test_machine.py
17index faea37a..8b62ea3 100644
18--- a/src/maasserver/api/tests/test_machine.py
19+++ b/src/maasserver/api/tests/test_machine.py
20@@ -554,9 +554,8 @@ class TestMachineAPI(APITestCase.ForUser):
21 distro_series=distro_series,
22 osystem=osystem,
23 architecture=make_usable_architecture(self),
24+ ephemeral_deploy=True,
25 )
26- for bd in machine.blockdevice_set.all():
27- bd.delete()
28 response = self.client.post(
29 self.get_machine_uri(machine),
30 {"op": "deploy", "install_kvm": True},
31diff --git a/src/maasserver/models/node.py b/src/maasserver/models/node.py
32index b679963..d087f37 100644
33--- a/src/maasserver/models/node.py
34+++ b/src/maasserver/models/node.py
35@@ -16,7 +16,7 @@ from collections import defaultdict, namedtuple, OrderedDict
36 import copy
37 from datetime import datetime, timedelta
38 from functools import partial
39-from itertools import count
40+from itertools import chain, count
41 import json
42 import logging
43 from operator import attrgetter
44@@ -1463,6 +1463,16 @@ class Node(CleanSave, TimestampedModel):
45 # Devices should always local boot.
46 if self.is_device:
47 return False
48+ # Only machines which are deploying can be deployed ephemerally. Allow
49+ # ALLOCATED and READY as this is checked before transitioning in the
50+ # API.
51+ if self.status not in {
52+ NODE_STATUS.DEPLOYING,
53+ NODE_STATUS.DEPLOYED,
54+ NODE_STATUS.ALLOCATED,
55+ NODE_STATUS.READY,
56+ }:
57+ return False
58 return self.is_diskless or self.ephemeral_deploy
59
60 def retrieve_storage_layout_issues(
61@@ -2216,21 +2226,55 @@ class Node(CleanSave, TimestampedModel):
62 )
63
64 # Create a new ScriptSet for this commissioning run.
65- script_set = ScriptSet.objects.create_commissioning_script_set(
66+ commis_script_set = ScriptSet.objects.create_commissioning_script_set(
67 self, commissioning_scripts, script_input
68 )
69- self.current_commissioning_script_set = script_set
70
71 # Create a new ScriptSet for any tests to be run after commissioning.
72 try:
73- script_set = ScriptSet.objects.create_testing_script_set(
74+ testing_script_set = ScriptSet.objects.create_testing_script_set(
75 self, testing_scripts, script_input
76 )
77- self.current_testing_script_set = script_set
78 except NoScriptsFound:
79 # Commissioning can run without running tests after.
80+ testing_script_set = []
81 pass
82
83+ config = Config.objects.get_configs(
84+ [
85+ "commissioning_osystem",
86+ "commissioning_distro_series",
87+ "default_osystem",
88+ "default_distro_series",
89+ "default_min_hwe_kernel",
90+ ]
91+ )
92+
93+ # Testing configured networking requires netplan which is only
94+ # available in 18.04+
95+ if config["commissioning_distro_series"] == "xenial":
96+ apply_configured_networking_found = False
97+ for script_result in chain(commis_script_set, testing_script_set):
98+ if (
99+ script_result.script
100+ and script_result.script.apply_configured_networking
101+ ):
102+ apply_configured_networking_found = True
103+ break
104+ if apply_configured_networking_found:
105+ commis_script_set.delete()
106+ if testing_script_set:
107+ testing_script_set.delete()
108+ raise ValidationError(
109+ "Unable to run script which configures custom networking "
110+ "when using Ubuntu Xenial 16.04 as the ephemeral "
111+ "operating system."
112+ )
113+
114+ self.current_commissioning_script_set = commis_script_set
115+ if testing_script_set:
116+ self.current_testing_script_set = testing_script_set
117+
118 # Clear the current storage configuration if networking is not being
119 # skipped during commissioning.
120 if not self.skip_storage:
121@@ -2247,15 +2291,7 @@ class Node(CleanSave, TimestampedModel):
122 old_status = self.status
123 self.status = NODE_STATUS.COMMISSIONING
124 self.owner = user
125- config = Config.objects.get_configs(
126- [
127- "commissioning_osystem",
128- "commissioning_distro_series",
129- "default_osystem",
130- "default_distro_series",
131- "default_min_hwe_kernel",
132- ]
133- )
134+
135 # Set min_hwe_kernel to default_min_hwe_kernel.
136 # This makes sure that the min_hwe_kernel is up to date
137 # with what is stored in the settings.
138@@ -2377,6 +2413,9 @@ class Node(CleanSave, TimestampedModel):
139 script_set = ScriptSet.objects.create_testing_script_set(
140 self, testing_scripts, script_input
141 )
142+ commissioning_distro_series = Config.objects.get_config(
143+ "commissioning_distro_series"
144+ )
145 # Additional validation for when starting testing and not
146 # commissioning + testing.
147 for script_result in script_set:
148@@ -2401,6 +2440,17 @@ class Node(CleanSave, TimestampedModel):
149 raise ValidationError(
150 "Unable to run destructive test while deployed!"
151 )
152+ if (
153+ commissioning_distro_series == "xenial"
154+ and script_result.script
155+ and script_result.script.apply_configured_networking
156+ ):
157+ script_set.delete()
158+ raise ValidationError(
159+ "Unable to run script which configures custom networking "
160+ "when using Ubuntu Xenial 16.04 as the ephemeral "
161+ "operating system."
162+ )
163
164 self.current_testing_script_set = script_set
165
166@@ -3612,13 +3662,13 @@ class Node(CleanSave, TimestampedModel):
167
168 def set_ephemeral_deploy(self, on=False):
169 """Set ephermal deploy on or off."""
170+ # self.ephemeral_deployment returns True when the system is diskless.
171+ self.ephemeral_deploy = on or self.is_diskless
172 log.info(
173 "{hostname}: Turning ephemeral deploy %s for node"
174- % ("off" if on is False else "on"),
175+ % ("on" if self.ephemeral_deploy else "off"),
176 hostname=self.hostname,
177 )
178- self.ephemeral_deploy = on
179- self.save()
180
181 def split_arch(self):
182 """Return architecture and subarchitecture, as a tuple."""
183@@ -4986,6 +5036,7 @@ class Node(CleanSave, TimestampedModel):
184 # If ephemeral is in purposes, this comes from Caringo OS.
185 # Set the ephemeral_deploy field on the node.
186 self.set_ephemeral_deploy(True)
187+ self.save()
188 return "ephemeral"
189 else:
190 return "xinstall"
191@@ -5160,6 +5211,9 @@ class Node(CleanSave, TimestampedModel):
192 bridge_stp=None,
193 bridge_fd=None,
194 ):
195+ # Avoid circular imports
196+ from maasserver.utils.osystems import release_a_newer_than_b
197+
198 if not user.has_perm(NodePermission.edit, self):
199 # You can't start a node you don't own unless you're an admin.
200 raise PermissionDenied()
201@@ -5227,6 +5281,12 @@ class Node(CleanSave, TimestampedModel):
202 raise ValidationError(
203 "Cannot install KVM host for ephemeral deployments."
204 )
205+ if self.ephemeral_deployment and not release_a_newer_than_b(
206+ self.distro_series, "bionic"
207+ ):
208+ raise ValidationError(
209+ "Ephemeral deployments only supported on Ubuntu Bionic 18.04+"
210+ )
211 self._register_request_event(
212 user, event, action="start", comment=comment
213 )
214diff --git a/src/maasserver/models/tests/test_node.py b/src/maasserver/models/tests/test_node.py
215index 2f0ec78..e98fa74 100644
216--- a/src/maasserver/models/tests/test_node.py
217+++ b/src/maasserver/models/tests/test_node.py
218@@ -1096,33 +1096,62 @@ class TestNode(MAASServerTestCase):
219 self.assertFalse(node.is_diskless)
220
221 def test_no_ephemeral_deploy(self):
222- node = factory.make_Node(ephemeral_deploy=False)
223+ node = factory.make_Node(
224+ ephemeral_deploy=False, status=NODE_STATUS.DEPLOYING
225+ )
226 self.assertFalse(node.ephemeral_deploy)
227
228 def test_ephemeral_deploy(self):
229- node = factory.make_Node(ephemeral_deploy=True)
230+ node = factory.make_Node(
231+ ephemeral_deploy=True, status=NODE_STATUS.DEPLOYING
232+ )
233 self.assertTrue(node.ephemeral_deploy)
234
235 def test_ephemeral_deployment_checks_diskless(self):
236- node = factory.make_Node(with_boot_disk=True)
237+ node = factory.make_Node(
238+ with_boot_disk=True, status=NODE_STATUS.DEPLOYING
239+ )
240 self.assertFalse(node.ephemeral_deployment)
241
242 def test_ephemeral_deployment_checks_not_diskless(self):
243- node = factory.make_Node(with_boot_disk=False)
244+ node = factory.make_Node(
245+ with_boot_disk=False, status=NODE_STATUS.DEPLOYING
246+ )
247 self.assertTrue(node.ephemeral_deployment)
248
249 def test_ephemeral_deployment_checks_ephemeral_deploy(self):
250- node = factory.make_Node(ephemeral_deploy=True)
251+ node = factory.make_Node(
252+ ephemeral_deploy=True, status=NODE_STATUS.DEPLOYING
253+ )
254 self.assertTrue(node.ephemeral_deployment)
255
256 def test_ephemeral_deployment_checks_no_ephemeral_deploy(self):
257- node = factory.make_Node(ephemeral_deploy=False)
258+ node = factory.make_Node(
259+ ephemeral_deploy=False, status=NODE_STATUS.DEPLOYING
260+ )
261 self.assertFalse(node.ephemeral_deployment)
262
263 def test_ephemeral_deployment_checks_is_device(self):
264- device = factory.make_Device()
265+ device = factory.make_Device(
266+ ephemeral_deploy=True, status=NODE_STATUS.DEPLOYING
267+ )
268 self.assertFalse(device.ephemeral_deployment)
269
270+ def test_ephemeral_deployment_no_if_not_deploying(self):
271+ node = factory.make_Node(
272+ status=factory.pick_choice(
273+ NODE_STATUS_CHOICES,
274+ but_not=[
275+ NODE_STATUS.DEPLOYING,
276+ NODE_STATUS.DEPLOYED,
277+ NODE_STATUS.ALLOCATED,
278+ NODE_STATUS.READY,
279+ ],
280+ ),
281+ ephemeral_deploy=True,
282+ )
283+ self.assertFalse(node.ephemeral_deployment)
284+
285 def test_system_id_is_a_valid_znum(self):
286 node = factory.make_Node()
287 self.assertThat(
288@@ -3456,6 +3485,39 @@ class TestNode(MAASServerTestCase):
289 )
290 self.assertEqual(events[1].type.name, EVENT_TYPES.COMMISSIONING)
291
292+ def test_start_commissioning_fails_on_xenial_with_network_testing_c(self):
293+ Config.objects.set_config("commissioning_distro_series", "xenial")
294+ node = factory.make_Node(status=NODE_STATUS.NEW)
295+ admin = factory.make_admin()
296+ script = factory.make_Script(
297+ script_type=SCRIPT_TYPE.COMMISSIONING,
298+ apply_configured_networking=True,
299+ )
300+ self.assertRaises(
301+ ValidationError,
302+ node.start_commissioning,
303+ user=admin,
304+ commissioning_scripts=[script.name],
305+ )
306+ self.assertEquals(0, ScriptSet.objects.count())
307+ self.assertEquals(0, ScriptResult.objects.count())
308+
309+ def test_start_commissioning_fails_on_xenial_with_network_testing_t(self):
310+ Config.objects.set_config("commissioning_distro_series", "xenial")
311+ node = factory.make_Node(status=NODE_STATUS.NEW)
312+ admin = factory.make_admin()
313+ script = factory.make_Script(
314+ script_type=SCRIPT_TYPE.TESTING, apply_configured_networking=True
315+ )
316+ self.assertRaises(
317+ ValidationError,
318+ node.start_commissioning,
319+ user=admin,
320+ testing_scripts=[script.name],
321+ )
322+ self.assertEquals(0, ScriptSet.objects.count())
323+ self.assertEquals(0, ScriptResult.objects.count())
324+
325 def test_abort_commissioning_reverts_to_sane_state_on_error(self):
326 # If abort commissioning hits an error when trying to stop the
327 # node, it will revert the node to the state it was in before
328@@ -3920,6 +3982,21 @@ class TestNode(MAASServerTestCase):
329 )
330 self.assertFalse(ScriptSet.objects.filter(node=node).exists())
331
332+ def test_start_testing_prevents_network_testing_with_xenial(self):
333+ script = factory.make_Script(
334+ script_type=SCRIPT_TYPE.TESTING, apply_configured_networking=True
335+ )
336+ admin = factory.make_admin()
337+ node = factory.make_Node(status=NODE_STATUS.READY)
338+ self.assertRaises(
339+ ValidationError,
340+ node.start_testing,
341+ admin,
342+ testing_scripts=[script.name],
343+ )
344+ self.assertEquals(0, ScriptSet.objects.count())
345+ self.assertEquals(0, ScriptResult.objects.count())
346+
347 def test_save_logs_node_status_transition(self):
348 self.disable_node_query()
349 node = factory.make_Node(
350@@ -5212,7 +5289,11 @@ class TestNode(MAASServerTestCase):
351
352 def test_storage_layout_issues_is_valid_when_ephemeral_deployment(self):
353 # A diskless node is one that it is ephemerally deployed.
354- node = factory.make_Node(with_boot_disk=False, osystem="ubuntu")
355+ node = factory.make_Node(
356+ with_boot_disk=False,
357+ osystem="ubuntu",
358+ status=NODE_STATUS.DEPLOYING,
359+ )
360 self.assertEqual([], node.storage_layout_issues())
361
362 def test_storage_layout_issues_is_invalid_when_no_disks_non_ubuntu(self):
363@@ -6397,6 +6478,11 @@ class NodeManagerTest(MAASServerTestCase):
364 node.set_ephemeral_deploy(True)
365 self.assertTrue(node.ephemeral_deploy)
366
367+ def test_ephemeral_deploy_auto_on_when_diskless(self):
368+ node = factory.make_Node(ephemeral_deploy=False, with_boot_disk=False)
369+ node.set_ephemeral_deploy(False)
370+ self.assertTrue(node.ephemeral_deploy)
371+
372 def test_ephemeral_deploy_off(self):
373 node = factory.make_Node(ephemeral_deploy=True)
374 node.set_ephemeral_deploy(False)
375@@ -8013,6 +8099,8 @@ class TestNode_Start(MAASTransactionServerTestCase):
376 power_state=POWER_STATE.OFF,
377 network=None,
378 with_boot_disk=True,
379+ install_kvm=False,
380+ ephemeral_deploy=False,
381 ):
382 if network is None:
383 network = factory.make_ip4_or_6_network()
384@@ -8041,6 +8129,8 @@ class TestNode_Start(MAASTransactionServerTestCase):
385 cidr=cidr,
386 osystem=osystem,
387 distro_series=distro_series,
388+ install_kvm=install_kvm,
389+ ephemeral_deploy=ephemeral_deploy,
390 )
391 node.acquire(user)
392 return node
393@@ -8073,10 +8163,24 @@ class TestNode_Start(MAASTransactionServerTestCase):
394 def test__raises_ValidationError_if_ephemeral_deploy_and_install_kvm(self):
395 admin = factory.make_admin()
396 node = self.make_acquired_node_with_interface(
397- admin, power_type="manual", with_boot_disk=False
398+ admin,
399+ power_type="manual",
400+ with_boot_disk=False,
401+ ephemeral_deploy=True,
402+ install_kvm=True,
403 )
404- node.install_kvm = True
405- node.save()
406+ with ExpectedException(ValidationError):
407+ node.start(admin)
408+
409+ def test__raises_ValidationError_if_ephemeral_deployment_less_bionic(self):
410+ admin = factory.make_admin()
411+ node = self.make_acquired_node_with_interface(
412+ admin,
413+ power_type="manual",
414+ with_boot_disk=False,
415+ ephemeral_deploy=True,
416+ )
417+ node.distro_series = "xenial"
418 with ExpectedException(ValidationError):
419 node.start(admin)
420
421diff --git a/src/metadataserver/tests/test_vendor_data.py b/src/metadataserver/tests/test_vendor_data.py
422index 24f1175..d57282f 100644
423--- a/src/metadataserver/tests/test_vendor_data.py
424+++ b/src/metadataserver/tests/test_vendor_data.py
425@@ -384,7 +384,11 @@ class TestGenerateEphemeralDeploymentNetworkConfiguration(MAASServerTestCase):
426 self.assertThat(dict(configuration), Equals({}))
427
428 def test_yields_configuration_when_node_is_ephemeral_deployment(self):
429- node = factory.make_Node(with_boot_disk=False)
430+ node = factory.make_Node(
431+ with_boot_disk=False,
432+ ephemeral_deploy=True,
433+ status=NODE_STATUS.DEPLOYING,
434+ )
435 configuration = get_vendor_data(node, None)
436 config = dict(configuration)
437 self.assertThat(

Subscribers

People subscribed via source and target branches