Merge ~adam-collard/maas:websocket-create-deployed-machine into maas:master

Proposed by Adam Collard
Status: Merged
Approved by: Adam Collard
Approved revision: e038d4d1b1cfb3659236e242f96bc48845536fbf
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~adam-collard/maas:websocket-create-deployed-machine
Merge into: maas:master
Diff against target: 518 lines (+170/-128)
9 files modified
src/maasserver/api/machines.py (+13/-19)
src/maasserver/forms/__init__.py (+20/-2)
src/maasserver/forms/ephemeral.py (+2/-30)
src/maasserver/forms/tests/test_ephemeral.py (+2/-58)
src/maasserver/forms/tests/test_machine.py (+23/-2)
src/maasserver/websockets/handlers/machine.py (+18/-10)
src/maasserver/websockets/handlers/tests/test_machine.py (+28/-0)
src/metadataserver/models/scriptset.py (+21/-6)
src/metadataserver/models/tests/test_scriptset.py (+43/-1)
Reviewer Review Type Date Requested Status
MAAS Lander Needs Fixing
Alberto Donato (community) Approve
Review via email: mp+407385@code.launchpad.net

Commit message

Allow creating a machine as deployed via websocket handler

To post a comment you must log in.
Revision history for this message
Alberto Donato (ack) wrote :

nice!

just one question inline

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

UNIT TESTS
-b websocket-create-deployed-machine lp:~adam-collard/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 1633be5330aca1675c83e97634f75ec918c97f5a

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

UNIT TESTS
-b websocket-create-deployed-machine lp:~adam-collard/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas/job/branch-tester/10713/console
COMMIT: 08c2fc6141b4500ebd4c582b27f9218f12989816

review: Needs Fixing
Revision history for this message
Alberto Donato (ack) wrote :

+1!

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

LANDING
-b websocket-create-deployed-machine lp:~adam-collard/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED BUILD
LOG: http://maas-ci.internal:8080/job/maas/job/branch-tester/10714/consoleText

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

UNIT TESTS
-b websocket-create-deployed-machine lp:~adam-collard/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas/job/branch-tester/10715/console
COMMIT: 6de8d707da29ec79c569e46de5b01d9cd4e5a7ec

review: Needs Fixing
e038d4d... by Adam Collard

Flaky test - we don't care about the order

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 fe60680..5c234f2 100644
3--- a/src/maasserver/api/machines.py
4+++ b/src/maasserver/api/machines.py
5@@ -71,10 +71,7 @@ from maasserver.forms import (
6 MachineForm,
7 )
8 from maasserver.forms.clone import CloneForm
9-from maasserver.forms.ephemeral import (
10- CommissionForm,
11- CreateScriptsForDeployedForm,
12-)
13+from maasserver.forms.ephemeral import CommissionForm
14 from maasserver.forms.filesystem import (
15 MountNonStorageFilesystemForm,
16 UnmountNonStorageFilesystemForm,
17@@ -1968,6 +1965,10 @@ class MachinesHandler(NodesHandler, PowersMixin):
18 time out. Machines created by administrators will be commissioned
19 unless set to false.
20
21+ @param (boolean) "deployed" [required=false,formatting=true] Request
22+ the newly created machine to be created with status set to
23+ DEPLOYED.
24+
25 @param (int) "enable_ssh" [required=false] Whether to enable SSH for
26 the commissioning environment using the user's SSH key(s). '1' == True,
27 '0' == False.
28@@ -2011,21 +2012,14 @@ class MachinesHandler(NodesHandler, PowersMixin):
29 request.data, "deployed", default=False, validator=StringBool
30 )
31 machine = create_machine(request)
32- if request.user.is_superuser:
33- if deployed:
34- form = CreateScriptsForDeployedForm(
35- instance=machine,
36- data=request.data,
37- )
38- form.save()
39- elif commission:
40- form = CommissionForm(
41- instance=machine, user=request.user, data=request.data
42- )
43- # Silently ignore errors to prevent 500 errors. The commissioning
44- # callbacks have their own logging. This fixes LP:1600328.
45- if form.is_valid():
46- machine = form.save()
47+ if request.user.is_superuser and commission and not deployed:
48+ form = CommissionForm(
49+ instance=machine, user=request.user, data=request.data
50+ )
51+ # Silently ignore errors to prevent 500 errors. The commissioning
52+ # callbacks have their own logging. This fixes LP:1600328.
53+ if form.is_valid():
54+ machine = form.save()
55
56 return machine
57
58diff --git a/src/maasserver/forms/__init__.py b/src/maasserver/forms/__init__.py
59index d4d35df..f5feb77 100644
60--- a/src/maasserver/forms/__init__.py
61+++ b/src/maasserver/forms/__init__.py
62@@ -748,6 +748,7 @@ class MachineForm(NodeForm):
63 "architecture", choices
64 )
65 required = requires_macs_and_architecture(self.data) or requires_arch
66+ self._need_boot_images = required
67 self.fields["architecture"] = forms.ChoiceField(
68 choices=choices,
69 required=required,
70@@ -853,7 +854,7 @@ class MachineForm(NodeForm):
71 is_valid = super().is_valid()
72 if not is_valid:
73 return False
74- if len(list_all_usable_architectures()) == 0:
75+ if self._need_boot_images and not list_all_usable_architectures():
76 set_form_error(self, "architecture", NO_ARCHITECTURES_AVAILABLE)
77 is_valid = False
78 return is_valid
79@@ -1172,6 +1173,23 @@ class AdminMachineForm(MachineForm, AdminNodeForm, WithPowerTypeMixin):
80 cleaned_data = WithPowerTypeMixin.check_driver(self, cleaned_data)
81 return cleaned_data
82
83+ def _setup_deployed_machine(self, machine):
84+ """Configure the Machine before it has been saved."""
85+ from metadataserver.models import NodeKey
86+ from metadataserver.models.scriptset import ScriptSet
87+
88+ machine.status = NODE_STATUS.DEPLOYED
89+ # Foreign relations need to have an id to relate to, have to
90+ # save here
91+ machine.save()
92+ script_set = ScriptSet.objects.create_deployed_machine_script_set(
93+ machine
94+ )
95+ machine.current_commissioning_script_set = script_set
96+
97+ # ensure a token is available for the machine
98+ NodeKey.objects.get_token_for_node(machine)
99+
100 def save(self, *args, **kwargs):
101 """Persist the node into the database."""
102 machine = super().save(commit=False)
103@@ -1182,7 +1200,7 @@ class AdminMachineForm(MachineForm, AdminNodeForm, WithPowerTypeMixin):
104 if pool:
105 machine.pool = pool
106 if self.cleaned_data.get("deployed"):
107- machine.status = NODE_STATUS.DEPLOYED
108+ self._setup_deployed_machine(machine)
109 WithPowerTypeMixin.set_values(self, machine)
110 if kwargs.get("commit", True):
111 machine.save(*args, **kwargs)
112diff --git a/src/maasserver/forms/ephemeral.py b/src/maasserver/forms/ephemeral.py
113index 39d28e2..b736eaa 100644
114--- a/src/maasserver/forms/ephemeral.py
115+++ b/src/maasserver/forms/ephemeral.py
116@@ -21,8 +21,8 @@ from django.http import QueryDict
117 from maasserver.enum import NODE_STATUS
118 from maasserver.node_action import compile_node_actions
119 from maasserver.utils.forms import set_form_error
120-from metadataserver.enum import RESULT_TYPE, SCRIPT_TYPE
121-from metadataserver.models import NodeKey, Script, ScriptSet
122+from metadataserver.enum import SCRIPT_TYPE
123+from metadataserver.models import Script
124
125
126 class TestForm(Form):
127@@ -322,31 +322,3 @@ class CommissionForm(TestForm):
128 ),
129 )
130 return self.instance
131-
132-
133-class CreateScriptsForDeployedForm(Form):
134- def __init__(self, instance, **kwargs):
135- super().__init__(**kwargs)
136- self.instance = instance
137-
138- def save(self):
139- node = self.instance
140- scripts = list(
141- Script.objects.filter(
142- script_type=SCRIPT_TYPE.COMMISSIONING,
143- default=True,
144- tags__contains=["deploy-info"],
145- )
146- )
147- script_set = ScriptSet.objects.create(
148- node=node,
149- result_type=RESULT_TYPE.COMMISSIONING,
150- requested_scripts=[script.name for script in scripts],
151- )
152- for script in scripts:
153- script_set.add_pending_script(script)
154- node.current_commissioning_script_set = script_set
155- node.save()
156- # ensure a token is available for the machine
157- NodeKey.objects.get_token_for_node(node)
158- return script_set
159diff --git a/src/maasserver/forms/tests/test_ephemeral.py b/src/maasserver/forms/tests/test_ephemeral.py
160index a5c2e56..fde9528 100644
161--- a/src/maasserver/forms/tests/test_ephemeral.py
162+++ b/src/maasserver/forms/tests/test_ephemeral.py
163@@ -12,16 +12,11 @@ from maasserver.enum import (
164 NODE_TYPE_CHOICES,
165 POWER_STATE,
166 )
167-from maasserver.forms.ephemeral import (
168- CommissionForm,
169- CreateScriptsForDeployedForm,
170- TestForm,
171-)
172+from maasserver.forms.ephemeral import CommissionForm, TestForm
173 from maasserver.testing.factory import factory
174 from maasserver.testing.testcase import MAASServerTestCase
175 from maastesting.matchers import MockCalledOnceWith
176-from metadataserver.enum import SCRIPT_STATUS, SCRIPT_TYPE
177-from metadataserver.models import NodeKey
178+from metadataserver.enum import SCRIPT_TYPE
179
180
181 class TestTestForm(MAASServerTestCase):
182@@ -988,54 +983,3 @@ class TestCommissionForm(MAASServerTestCase):
183 script_input={},
184 ),
185 )
186-
187-
188-class TestCreateScriptsForDeployedForm(MAASServerTestCase):
189- def test_create_scripts(self):
190- script1 = factory.make_Script(
191- script_type=SCRIPT_TYPE.COMMISSIONING,
192- default=True,
193- tags=["foo", "deploy-info"],
194- )
195- script2 = factory.make_Script(
196- script_type=SCRIPT_TYPE.COMMISSIONING,
197- default=True,
198- tags=["bar", "deploy-info"],
199- )
200- # other scripts that are not matched
201- factory.make_Script( # not for commissioning
202- script_type=SCRIPT_TYPE.TESTING,
203- default=True,
204- tags=["bar", "deploy-info"],
205- )
206- factory.make_Script( # not a builtin script
207- script_type=SCRIPT_TYPE.COMMISSIONING,
208- default=False,
209- tags=["deploy-info"],
210- )
211- factory.make_Script( # no deploy-info tag
212- script_type=SCRIPT_TYPE.COMMISSIONING,
213- default=True,
214- tags=["foo"],
215- )
216- node = factory.make_Node(status=NODE_STATUS.DEPLOYED)
217- form = CreateScriptsForDeployedForm(instance=node, data={})
218- self.assertTrue(form.is_valid())
219- script_set = form.save()
220- self.assertEqual(script_set.node, node)
221- script_results = list(script_set.scriptresult_set.order_by("id"))
222- self.assertEqual(
223- [script_result.script for script_result in script_results],
224- [script1, script2],
225- )
226- for script_result in script_results:
227- self.assertEqual(script_result.status, SCRIPT_STATUS.PENDING)
228- self.assertIs(node.current_commissioning_script_set, script_set)
229-
230- def test_create_node_token(self):
231- node = factory.make_Node(status=NODE_STATUS.DEPLOYED)
232- self.assertFalse(NodeKey.objects.filter(node=node).exists())
233- form = CreateScriptsForDeployedForm(instance=node, data={})
234- self.assertTrue(form.is_valid())
235- form.save()
236- self.assertTrue(NodeKey.objects.filter(node=node).exists())
237diff --git a/src/maasserver/forms/tests/test_machine.py b/src/maasserver/forms/tests/test_machine.py
238index cef7cbe..bf61313 100644
239--- a/src/maasserver/forms/tests/test_machine.py
240+++ b/src/maasserver/forms/tests/test_machine.py
241@@ -24,6 +24,7 @@ from maasserver.testing.osystems import (
242 patch_usable_osystems,
243 )
244 from maasserver.testing.testcase import MAASServerTestCase
245+from metadataserver.models import NodeKey
246 from provisioningserver.rpc.exceptions import (
247 NoConnectionsAvailable,
248 NoSuchOperatingSystem,
249@@ -409,11 +410,9 @@ class TestAdminMachineForm(MAASServerTestCase):
250
251 def test_AdminMachineForm_new_machine_deployed(self):
252 hostname = factory.make_string()
253- arch = make_usable_architecture(self)
254 form = AdminMachineForm(
255 data={
256 "hostname": hostname,
257- "architecture": arch,
258 "deployed": True,
259 },
260 )
261@@ -518,3 +517,25 @@ class TestAdminMachineForm(MAASServerTestCase):
262 ]
263 },
264 )
265+
266+ def test_AdminMachineForm_creates_scriptset_for_deployed(self):
267+ hostname = factory.make_string()
268+ form = AdminMachineForm(
269+ data={
270+ "hostname": hostname,
271+ "deployed": True,
272+ },
273+ )
274+ machine = form.save()
275+ self.assertIsNotNone(machine.current_commissioning_script_set)
276+
277+ def test_AdminMachineForm_creates_node_token_for_deployed(self):
278+ hostname = factory.make_string()
279+ form = AdminMachineForm(
280+ data={
281+ "hostname": hostname,
282+ "deployed": True,
283+ },
284+ )
285+ machine = form.save()
286+ self.assertTrue(NodeKey.objects.filter(node=machine).exists())
287diff --git a/src/maasserver/websockets/handlers/machine.py b/src/maasserver/websockets/handlers/machine.py
288index 3236647..4425c78 100644
289--- a/src/maasserver/websockets/handlers/machine.py
290+++ b/src/maasserver/websockets/handlers/machine.py
291@@ -22,6 +22,7 @@ from maasserver.enum import (
292 from maasserver.exceptions import NodeActionError, NodeStateViolation
293 from maasserver.forms import (
294 AddPartitionForm,
295+ AdminMachineForm,
296 AdminMachineWithMACAddressesForm,
297 CreateBcacheForm,
298 CreateCacheSetForm,
299@@ -246,6 +247,10 @@ class MachineHandler(NodeHandler):
300 edit_permission = NodePermission.admin
301 delete_permission = NodePermission.admin
302
303+ def __init__(self, *args, **kwargs):
304+ super().__init__(*args, **kwargs)
305+ self._deployed = False
306+
307 def get_queryset(self, for_list=False):
308 """Return `QuerySet` for devices only viewable by `user`."""
309 return Machine.objects.get_nodes(
310@@ -391,6 +396,8 @@ class MachineHandler(NodeHandler):
311
312 def get_form_class(self, action):
313 """Return the form class used for `action`."""
314+ if action == "create" and self._deployed:
315+ return AdminMachineForm
316 if action in ("create", "update"):
317 return AdminMachineWithMACAddressesForm
318 else:
319@@ -408,6 +415,7 @@ class MachineHandler(NodeHandler):
320 new_params["description"] = params.get("description")
321 new_params["power_type"] = params.get("power_type")
322 new_params["power_parameters"] = params.get("power_parameters")
323+ new_params["deployed"] = params.get("deployed")
324 if "zone" in params:
325 new_params["zone"] = params["zone"]["name"]
326 if params.get("pool"):
327@@ -427,18 +435,18 @@ class MachineHandler(NodeHandler):
328 return super().preprocess_form(action, new_params)
329
330 def create(self, params):
331- """Create the object from params."""
332+ self._deployed = bool(params.get("deployed", False))
333 data = super().create(params)
334- node_obj = Node.objects.get(system_id=data["system_id"])
335+ if not self._deployed:
336+ machine = Node.objects.get(system_id=data["system_id"])
337+ # Start the commissioning process right away, which has the
338+ # desired side effect of initializing the node's power state.
339+ d = machine.start_commissioning(self.user)
340+ # Silently ignore errors to prevent tracebacks. The commissioning
341+ # callbacks have their own logging. This fixes LP1600328.
342+ d.addErrback(lambda _: None)
343
344- # Start the commissioning process right away, which has the
345- # desired side effect of initializing the node's power state.
346- d = node_obj.start_commissioning(self.user)
347- # Silently ignore errors to prevent tracebacks. The commissioning
348- # callbacks have their own logging. This fixes LP1600328.
349- d.addErrback(lambda _: None)
350-
351- return self.full_dehydrate(node_obj)
352+ return data
353
354 def update(self, params):
355 """Update the object from params."""
356diff --git a/src/maasserver/websockets/handlers/tests/test_machine.py b/src/maasserver/websockets/handlers/tests/test_machine.py
357index 391127b..b13f953 100644
358--- a/src/maasserver/websockets/handlers/tests/test_machine.py
359+++ b/src/maasserver/websockets/handlers/tests/test_machine.py
360@@ -114,6 +114,7 @@ from metadataserver.enum import (
361 SCRIPT_STATUS_FAILED,
362 SCRIPT_TYPE,
363 )
364+from metadataserver.models.nodekey import NodeKey
365 from metadataserver.models.scriptset import get_status_from_qs
366 from provisioningserver.refresh.node_info_scripts import (
367 LIST_MODALIASES_OUTPUT_NAME,
368@@ -2416,6 +2417,33 @@ class TestMachineHandler(MAASServerTestCase):
369 )
370 self.assertThat(mock_start_commissioning, MockCalledOnceWith(user))
371
372+ def test_create_creates_deployed_node(self):
373+ user = factory.make_admin()
374+ handler = MachineHandler(user, {}, None)
375+ hostname = factory.make_name("hostname")
376+ description = factory.make_name("description")
377+ zone = factory.make_Zone()
378+
379+ mock_start_commissioning = self.patch(
380+ node_model, "start_commissioning"
381+ )
382+
383+ created_node = handler.create(
384+ {
385+ "hostname": hostname,
386+ "description": description,
387+ "zone": {"name": zone.name},
388+ "deployed": True,
389+ }
390+ )
391+ # the commissioning process is not started
392+ mock_start_commissioning.assert_not_called()
393+ self.assertEqual(created_node["status"], "Deployed")
394+ node = Node.objects.get(system_id=created_node["system_id"])
395+ self.assertIsNotNone(node.current_commissioning_script_set)
396+ self.assertTrue(NodeKey.objects.filter(node=node).exists())
397+ self.assertEqual(node.status, NODE_STATUS.DEPLOYED)
398+
399 def test_update_raise_permissions_error_for_non_admin(self):
400 user = factory.make_User()
401 node = factory.make_Node()
402diff --git a/src/metadataserver/models/scriptset.py b/src/metadataserver/models/scriptset.py
403index e9eed4a..2e8dea6 100644
404--- a/src/metadataserver/models/scriptset.py
405+++ b/src/metadataserver/models/scriptset.py
406@@ -227,6 +227,25 @@ class ScriptSetManager(Manager):
407 self._clean_old(node, RESULT_TYPE.INSTALLATION, script_set)
408 return script_set
409
410+ def create_deployed_machine_script_set(self, machine):
411+ """Setup ScriptSet for a brown-field deployment.
412+
413+ Built-in scripts with deploy-info tag are queued up.
414+ """
415+ scripts = list(
416+ Script.objects.filter(
417+ script_type=SCRIPT_TYPE.COMMISSIONING,
418+ default=True,
419+ tags__contains=["deploy-info"],
420+ )
421+ )
422+ script_set = machine.scriptset_set.create(
423+ requested_scripts=[script.name for script in scripts]
424+ )
425+ for script in scripts:
426+ script_set.add_pending_script(script)
427+ return script_set
428+
429 def _find_scripts(self, script_qs, hw_pairs, modaliases):
430 for script in script_qs:
431 # If a script is not for specific hardware, it is always included
432@@ -444,13 +463,10 @@ class ScriptSet(CleanSave, Model):
433 def add_pending_script(self, script, script_input=None):
434 """Create and add a new ScriptResult for the given Script.
435
436- Creates a new ScriptResult for the given script and assoicates it with
437+ Creates a new ScriptResult for the given script and associates it with
438 this ScriptSet. Raises a ValidationError if ParametersForm validation
439 fails.
440 """
441- # Avoid circular dependencies.
442- from metadataserver.models import ScriptResult
443-
444 if script_input is None:
445 script_input = {}
446 form = ParametersForm(
447@@ -461,8 +477,7 @@ class ScriptSet(CleanSave, Model):
448 if not form.is_valid():
449 raise ValidationError(form.errors)
450 for param in form.cleaned_data["input"]:
451- ScriptResult.objects.create(
452- script_set=self,
453+ self.scriptresult_set.create(
454 status=SCRIPT_STATUS.PENDING,
455 script=script,
456 script_name=script.name,
457diff --git a/src/metadataserver/models/tests/test_scriptset.py b/src/metadataserver/models/tests/test_scriptset.py
458index 68a5b9f..ad37878 100644
459--- a/src/metadataserver/models/tests/test_scriptset.py
460+++ b/src/metadataserver/models/tests/test_scriptset.py
461@@ -10,7 +10,7 @@ from unittest.mock import PropertyMock
462 from django.core.exceptions import ValidationError
463 from django.db.models import Q
464
465-from maasserver.enum import NODE_TYPE
466+from maasserver.enum import NODE_STATUS, NODE_TYPE
467 from maasserver.exceptions import NoScriptsFound
468 from maasserver.models import Config, Event, EventType, Node
469 from maasserver.preseed import CURTIN_INSTALL_LOG
470@@ -1012,6 +1012,48 @@ class TestScriptSetManager(MAASServerTestCase):
471 ).all(),
472 )
473
474+ def test_create_deployed_machine_script_set_scripts(self):
475+ # Avoid builtins muddying the test
476+ Script.objects.all().delete()
477+ script1 = factory.make_Script(
478+ script_type=SCRIPT_TYPE.COMMISSIONING,
479+ default=True,
480+ tags=["foo", "deploy-info"],
481+ )
482+ script2 = factory.make_Script(
483+ script_type=SCRIPT_TYPE.COMMISSIONING,
484+ default=True,
485+ tags=["bar", "deploy-info"],
486+ )
487+ # other scripts that are not matched
488+ factory.make_Script( # not for commissioning
489+ script_type=SCRIPT_TYPE.TESTING,
490+ default=True,
491+ tags=["bar", "deploy-info"],
492+ )
493+ factory.make_Script( # not a builtin script
494+ script_type=SCRIPT_TYPE.COMMISSIONING,
495+ default=False,
496+ tags=["deploy-info"],
497+ )
498+ factory.make_Script( # no deploy-info tag
499+ script_type=SCRIPT_TYPE.COMMISSIONING,
500+ default=True,
501+ tags=["foo"],
502+ )
503+ node = factory.make_Node(status=NODE_STATUS.DEPLOYED)
504+ script_set = ScriptSet.objects.create_deployed_machine_script_set(node)
505+ self.assertItemsEqual(
506+ [
507+ (script_result.script, script_result.status)
508+ for script_result in script_set.scriptresult_set.all()
509+ ],
510+ [
511+ (script1, SCRIPT_STATUS.PENDING),
512+ (script2, SCRIPT_STATUS.PENDING),
513+ ],
514+ )
515+
516
517 class TestScriptSet(MAASServerTestCase):
518 """Test the ScriptSet model."""

Subscribers

People subscribed via source and target branches