Merge ~mpontillo/maas:install-kvm into maas:master

Proposed by Mike Pontillo
Status: Merged
Approved by: Mike Pontillo
Approved revision: 30d78646ac17054d9f3a8b52ad58c5b98733a463
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~mpontillo/maas:install-kvm
Merge into: maas:master
Diff against target: 851 lines (+417/-36)
16 files modified
src/maasserver/api/machines.py (+53/-19)
src/maasserver/api/tests/test_machines.py (+36/-0)
src/maasserver/forms/__init__.py (+6/-0)
src/maasserver/forms/pods.py (+8/-2)
src/maasserver/forms/tests/test_machine.py (+2/-0)
src/maasserver/migrations/builtin/maasserver/0173_add_node_install_kvm.py (+23/-0)
src/maasserver/models/node.py (+6/-0)
src/maasserver/testing/factory.py (+16/-4)
src/maasserver/websockets/handlers/device.py (+1/-0)
src/maasserver/websockets/handlers/machine.py (+1/-0)
src/metadataserver/api.py (+14/-1)
src/metadataserver/api_twisted.py (+62/-6)
src/metadataserver/tests/test_api.py (+11/-0)
src/metadataserver/tests/test_api_twisted.py (+95/-2)
src/metadataserver/tests/test_vendor_data.py (+22/-1)
src/metadataserver/vendor_data.py (+61/-1)
Reviewer Review Type Date Requested Status
Lee Trager (community) Approve
Blake Rouse (community) Approve
Andres Rodriguez (community) Approve
Review via email: mp+355115@code.launchpad.net

Commit message

Allow KVM pod deployment in MAAS using install_kvm option.

To post a comment you must log in.
Revision history for this message
Andres Rodriguez (andreserl) wrote :

Does agent_name get cleared out on release?

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

Yes, agent_name is cleared on release (I checked that yesterday).

Revision history for this message
Andres Rodriguez (andreserl) wrote :

lgtm!

review: Approve
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looks good.

review: Approve
Revision history for this message
Lee Trager (ltrager) wrote :

LGTM!

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
~mpontillo/maas:install-kvm updated
30d7864... by Mike Pontillo

Fix test failures.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/src/maasserver/api/machines.py b/src/maasserver/api/machines.py
index 9a95cc8..12cbc69 100644
--- a/src/maasserver/api/machines.py
+++ b/src/maasserver/api/machines.py
@@ -8,6 +8,7 @@ __all__ = [
8 "get_storage_layout_params",8 "get_storage_layout_params",
9]9]
1010
11from collections import namedtuple
11import json12import json
12import re13import re
1314
@@ -193,6 +194,19 @@ DISPLAYED_ANON_MACHINE_FIELDS = (
193)194)
194195
195196
197AllocationOptions = namedtuple(
198 'AllocationOptions', (
199 'agent_name',
200 'bridge_all',
201 'bridge_fd',
202 'bridge_stp',
203 'comment',
204 'install_rackd',
205 'install_kvm',
206 )
207)
208
209
196def get_storage_layout_params(request, required=False, extract_params=False):210def get_storage_layout_params(request, required=False, extract_params=False):
197 """Return and validate the storage_layout parameter."""211 """Return and validate the storage_layout parameter."""
198 form = StorageLayoutForm(required=required, data=request.data)212 form = StorageLayoutForm(required=required, data=request.data)
@@ -218,11 +232,18 @@ def get_storage_layout_params(request, required=False, extract_params=False):
218 return storage_layout, params232 return storage_layout, params
219233
220234
221def get_allocation_parameters(request):235def get_allocation_options(request) -> AllocationOptions:
222 """Returns shared parameters for deploy and allocate operations."""236 """Parses optional parameters for allocation and deployment."""
223 comment = get_optional_param(request.POST, 'comment')237 comment = get_optional_param(request.POST, 'comment')
238 default_bridge_all = False
239 install_rackd = get_optional_param(
240 request.POST, 'install_rackd', default=False, validator=StringBool)
241 install_kvm = get_optional_param(
242 request.POST, 'install_kvm', default=False, validator=StringBool)
243 if install_kvm:
244 default_bridge_all = True
224 bridge_all = get_optional_param(245 bridge_all = get_optional_param(
225 request.POST, 'bridge_all', default=False,246 request.POST, 'bridge_all', default=default_bridge_all,
226 validator=StringBool)247 validator=StringBool)
227 bridge_stp = get_optional_param(248 bridge_stp = get_optional_param(
228 request.POST, 'bridge_stp', default=False,249 request.POST, 'bridge_stp', default=False,
@@ -230,7 +251,15 @@ def get_allocation_parameters(request):
230 bridge_fd = get_optional_param(251 bridge_fd = get_optional_param(
231 request.POST, 'bridge_fd', default=0, validator=Int)252 request.POST, 'bridge_fd', default=0, validator=Int)
232 agent_name = request.data.get('agent_name', '')253 agent_name = request.data.get('agent_name', '')
233 return agent_name, bridge_all, bridge_fd, bridge_stp, comment254 return AllocationOptions(
255 agent_name,
256 bridge_all,
257 bridge_fd,
258 bridge_stp,
259 comment,
260 install_rackd,
261 install_kvm
262 )
234263
235264
236def get_allocated_composed_machine(265def get_allocated_composed_machine(
@@ -556,6 +585,9 @@ class MachineHandler(NodeHandler, OwnerDataMixin, PowerMixin):
556 :param install_rackd: If True, the Rack Controller will be installed on585 :param install_rackd: If True, the Rack Controller will be installed on
557 this machine.586 this machine.
558 :type install_rackd: boolean587 :type install_rackd: boolean
588 :param install_kvm: If True, KVM will be installed on this machine and
589 added to MAAS.
590 :type install_kvm: boolean
559591
560 Ideally we'd have MIME multipart and content-transfer-encoding etc.592 Ideally we'd have MIME multipart and content-transfer-encoding etc.
561 deal with the encapsulation of binary data, but couldn't make it work593 deal with the encapsulation of binary data, but couldn't make it work
@@ -571,27 +603,24 @@ class MachineHandler(NodeHandler, OwnerDataMixin, PowerMixin):
571 series = request.POST.get('distro_series', None)603 series = request.POST.get('distro_series', None)
572 license_key = request.POST.get('license_key', None)604 license_key = request.POST.get('license_key', None)
573 hwe_kernel = request.POST.get('hwe_kernel', None)605 hwe_kernel = request.POST.get('hwe_kernel', None)
574 install_rackd = get_optional_param(
575 request.POST, 'install_rackd', default=False, validator=StringBool)
576 # Acquiring a node requires VIEW permissions.606 # Acquiring a node requires VIEW permissions.
577 machine = self.model.objects.get_node_or_404(607 machine = self.model.objects.get_node_or_404(
578 system_id=system_id, user=request.user,608 system_id=system_id, user=request.user,
579 perm=NODE_PERMISSION.VIEW)609 perm=NODE_PERMISSION.VIEW)
610 options = get_allocation_options(request)
580 if machine.status == NODE_STATUS.READY:611 if machine.status == NODE_STATUS.READY:
581 with locks.node_acquire:612 with locks.node_acquire:
582 if machine.owner is not None and machine.owner != request.user:613 if machine.owner is not None and machine.owner != request.user:
583 raise NodeStateViolation(614 raise NodeStateViolation(
584 "Can't allocate a machine belonging to another user.")615 "Can't allocate a machine belonging to another user.")
585 agent_name, bridge_all, bridge_fd, bridge_stp, comment = (
586 get_allocation_parameters(request))
587 maaslog.info(616 maaslog.info(
588 "Request from user %s to acquire machine: %s (%s)",617 "Request from user %s to acquire machine: %s (%s)",
589 request.user.username, machine.fqdn, machine.system_id)618 request.user.username, machine.fqdn, machine.system_id)
590 machine.acquire(619 machine.acquire(
591 request.user, get_oauth_token(request),620 request.user, get_oauth_token(request),
592 agent_name=agent_name, comment=comment,621 agent_name=options.agent_name, comment=options.comment,
593 bridge_all=bridge_all, bridge_stp=bridge_stp,622 bridge_all=options.bridge_all,
594 bridge_fd=bridge_fd)623 bridge_stp=options.bridge_stp, bridge_fd=options.bridge_fd)
595 if NODE_STATUS.DEPLOYING not in NODE_TRANSITIONS[machine.status]:624 if NODE_STATUS.DEPLOYING not in NODE_TRANSITIONS[machine.status]:
596 raise NodeStateViolation(625 raise NodeStateViolation(
597 "Can't deploy a machine that is in the '{}' state".format(626 "Can't deploy a machine that is in the '{}' state".format(
@@ -600,7 +629,11 @@ class MachineHandler(NodeHandler, OwnerDataMixin, PowerMixin):
600 if not request.user.has_perm(NODE_PERMISSION.EDIT, machine):629 if not request.user.has_perm(NODE_PERMISSION.EDIT, machine):
601 raise PermissionDenied()630 raise PermissionDenied()
602 # Deploying with 'install_rackd' requires ADMIN permissions.631 # Deploying with 'install_rackd' requires ADMIN permissions.
603 if (install_rackd and not632 if (options.install_rackd and not
633 request.user.has_perm(NODE_PERMISSION.ADMIN, machine)):
634 raise PermissionDenied()
635 # Deploying with 'install_kvm' requires ADMIN permissions.
636 if (options.install_kvm and not
604 request.user.has_perm(NODE_PERMISSION.ADMIN, machine)):637 request.user.has_perm(NODE_PERMISSION.ADMIN, machine)):
605 raise PermissionDenied()638 raise PermissionDenied()
606 if not machine.distro_series and not series:639 if not machine.distro_series and not series:
@@ -613,8 +646,10 @@ class MachineHandler(NodeHandler, OwnerDataMixin, PowerMixin):
613 form.set_license_key(license_key=license_key)646 form.set_license_key(license_key=license_key)
614 if hwe_kernel is not None:647 if hwe_kernel is not None:
615 form.set_hwe_kernel(hwe_kernel=hwe_kernel)648 form.set_hwe_kernel(hwe_kernel=hwe_kernel)
616 if install_rackd:649 if options.install_rackd:
617 form.set_install_rackd(install_rackd=install_rackd)650 form.set_install_rackd(install_rackd=options.install_rackd)
651 if options.install_kvm:
652 form.set_install_kvm(install_kvm=options.install_kvm)
618 if form.is_valid():653 if form.is_valid():
619 form.save()654 form.save()
620 else:655 else:
@@ -1747,8 +1782,7 @@ class MachinesHandler(NodesHandler, PowersMixin):
1747 maaslog.info(1782 maaslog.info(
1748 "Request from user %s to acquire a machine with constraints: %s",1783 "Request from user %s to acquire a machine with constraints: %s",
1749 request.user.username, str(input_constraints))1784 request.user.username, str(input_constraints))
1750 agent_name, bridge_all, bridge_fd, bridge_stp, comment = (1785 options = get_allocation_options(request)
1751 get_allocation_parameters(request))
1752 verbose = get_optional_param(1786 verbose = get_optional_param(
1753 request.POST, 'verbose', default=False, validator=StringBool)1787 request.POST, 'verbose', default=False, validator=StringBool)
1754 dry_run = get_optional_param(1788 dry_run = get_optional_param(
@@ -1814,9 +1848,9 @@ class MachinesHandler(NodesHandler, PowersMixin):
1814 if not dry_run:1848 if not dry_run:
1815 machine.acquire(1849 machine.acquire(
1816 request.user, get_oauth_token(request),1850 request.user, get_oauth_token(request),
1817 agent_name=agent_name, comment=comment,1851 agent_name=options.agent_name, comment=options.comment,
1818 bridge_all=bridge_all, bridge_stp=bridge_stp,1852 bridge_all=options.bridge_all,
1819 bridge_fd=bridge_fd)1853 bridge_stp=options.bridge_stp, bridge_fd=options.bridge_fd)
1820 machine.constraint_map = storage.get(machine.id, {})1854 machine.constraint_map = storage.get(machine.id, {})
1821 machine.constraints_by_type = {}1855 machine.constraints_by_type = {}
1822 # Need to get the interface constraints map into the proper format1856 # Need to get the interface constraints map into the proper format
diff --git a/src/maasserver/api/tests/test_machines.py b/src/maasserver/api/tests/test_machines.py
index 3ef0831..ddb305e 100644
--- a/src/maasserver/api/tests/test_machines.py
+++ b/src/maasserver/api/tests/test_machines.py
@@ -16,6 +16,10 @@ from maasserver import (
16 middleware,16 middleware,
17)17)
18from maasserver.api import machines as machines_module18from maasserver.api import machines as machines_module
19from maasserver.api.machines import (
20 AllocationOptions,
21 get_allocation_options,
22)
19from maasserver.enum import (23from maasserver.enum import (
20 INTERFACE_TYPE,24 INTERFACE_TYPE,
21 NODE_STATUS,25 NODE_STATUS,
@@ -2774,3 +2778,35 @@ class TestPowerState(APITransactionTestCase.ForUser):
2774 self.assertEqual({"state": random_state}, response)2778 self.assertEqual({"state": random_state}, response)
2775 # The machine's power state is now `random_state`.2779 # The machine's power state is now `random_state`.
2776 self.assertPowerState(machine, random_state)2780 self.assertPowerState(machine, random_state)
2781
2782
2783class TestGetAllocationOptions(MAASTestCase):
2784
2785 def test_defaults(self):
2786 request = factory.make_fake_request(method="POST", data={})
2787 options = get_allocation_options(request)
2788 expected_options = AllocationOptions(
2789 agent_name='', bridge_all=False, bridge_fd=0, bridge_stp=False,
2790 comment=None, install_rackd=False, install_kvm=False)
2791 self.assertThat(options, Equals(expected_options))
2792
2793 def test_sets_bridge_all_if_install_kvm(self):
2794 request = factory.make_fake_request(
2795 method="POST", data=dict(install_kvm='true'))
2796 options = get_allocation_options(request)
2797 expected_options = AllocationOptions(
2798 agent_name='', bridge_all=True, bridge_fd=0, bridge_stp=False,
2799 comment=None, install_rackd=False, install_kvm=True)
2800 self.assertThat(options, Equals(expected_options))
2801
2802 def test_non_defaults(self):
2803 request = factory.make_fake_request(method="POST", data=dict(
2804 install_rackd="true", install_kvm="true", bridge_all="true",
2805 bridge_stp="true", bridge_fd="42", agent_name="maas",
2806 comment="don't panic"
2807 ))
2808 options = get_allocation_options(request)
2809 expected_options = AllocationOptions(
2810 agent_name='maas', bridge_all=True, bridge_fd=42, bridge_stp=True,
2811 comment="don't panic", install_rackd=True, install_kvm=True)
2812 self.assertThat(options, Equals(expected_options))
diff --git a/src/maasserver/forms/__init__.py b/src/maasserver/forms/__init__.py
index 69440fa..9ad9129 100644
--- a/src/maasserver/forms/__init__.py
+++ b/src/maasserver/forms/__init__.py
@@ -842,6 +842,11 @@ class MachineForm(NodeForm):
842 self.is_bound = True842 self.is_bound = True
843 self.data['install_rackd'] = install_rackd843 self.data['install_rackd'] = install_rackd
844844
845 def set_install_kvm(self, install_kvm=False):
846 """Sets whether to deploy the rack alongside this machine."""
847 self.is_bound = True
848 self.data['install_kvm'] = install_kvm
849
845 class Meta:850 class Meta:
846 model = Machine851 model = Machine
847852
@@ -853,6 +858,7 @@ class MachineForm(NodeForm):
853 'min_hwe_kernel',858 'min_hwe_kernel',
854 'hwe_kernel',859 'hwe_kernel',
855 'install_rackd',860 'install_rackd',
861 'install_kvm',
856 )862 )
857863
858864
diff --git a/src/maasserver/forms/pods.py b/src/maasserver/forms/pods.py
index 9b457b3..4f5fa73 100644
--- a/src/maasserver/forms/pods.py
+++ b/src/maasserver/forms/pods.py
@@ -133,9 +133,11 @@ class PodForm(MAASModelForm):
133 label="Default MACVLAN mode", required=False,133 label="Default MACVLAN mode", required=False,
134 choices=MACVLAN_MODE_CHOICES, initial=MACVLAN_MODE_CHOICES[0])134 choices=MACVLAN_MODE_CHOICES, initial=MACVLAN_MODE_CHOICES[0])
135135
136 def __init__(self, data=None, instance=None, request=None, **kwargs):136 def __init__(
137 self, data=None, instance=None, request=None, user=None, **kwargs):
137 self.is_new = instance is None138 self.is_new = instance is None
138 self.request = request139 self.request = request
140 self.user = user
139 super(PodForm, self).__init__(141 super(PodForm, self).__init__(
140 data=data, instance=instance, **kwargs)142 data=data, instance=instance, **kwargs)
141 if data is None:143 if data is None:
@@ -273,7 +275,11 @@ class PodForm(MAASModelForm):
273 # also create it in the database.275 # also create it in the database.
274 if not self.instance.name:276 if not self.instance.name:
275 self.instance.set_random_name()277 self.instance.set_random_name()
276 self.instance.sync(discovered_pod, self.request.user)278 if self.request is not None:
279 user = self.request.user
280 else:
281 user = self.user
282 self.instance.sync(discovered_pod, user)
277283
278 # Save which rack controllers can route and which cannot.284 # Save which rack controllers can route and which cannot.
279 discovered_rack_ids = [285 discovered_rack_ids = [
diff --git a/src/maasserver/forms/tests/test_machine.py b/src/maasserver/forms/tests/test_machine.py
index 4bca075..c8ed726 100644
--- a/src/maasserver/forms/tests/test_machine.py
+++ b/src/maasserver/forms/tests/test_machine.py
@@ -50,6 +50,7 @@ class TestMachineForm(MAASServerTestCase):
50 'min_hwe_kernel',50 'min_hwe_kernel',
51 'hwe_kernel',51 'hwe_kernel',
52 'install_rackd',52 'install_rackd',
53 'install_kvm',
53 ], list(form.fields))54 ], list(form.fields))
5455
55 def test_accepts_usable_architecture(self):56 def test_accepts_usable_architecture(self):
@@ -366,6 +367,7 @@ class TestAdminMachineForm(MAASServerTestCase):
366 'min_hwe_kernel',367 'min_hwe_kernel',
367 'hwe_kernel',368 'hwe_kernel',
368 'install_rackd',369 'install_rackd',
370 'install_kvm',
369 'cpu_count',371 'cpu_count',
370 'memory',372 'memory',
371 'zone',373 'zone',
diff --git a/src/maasserver/migrations/builtin/maasserver/0173_add_node_install_kvm.py b/src/maasserver/migrations/builtin/maasserver/0173_add_node_install_kvm.py
372new file mode 100644374new file mode 100644
index 0000000..5cce8a1
--- /dev/null
+++ b/src/maasserver/migrations/builtin/maasserver/0173_add_node_install_kvm.py
@@ -0,0 +1,23 @@
1# -*- coding: utf-8 -*-
2# Generated by Django 1.11.11 on 2018-09-15 00:04
3from __future__ import unicode_literals
4
5from django.db import (
6 migrations,
7 models,
8)
9
10
11class Migration(migrations.Migration):
12
13 dependencies = [
14 ('maasserver', '0172_partition_tags'),
15 ]
16
17 operations = [
18 migrations.AddField(
19 model_name='node',
20 name='install_kvm',
21 field=models.BooleanField(default=False),
22 ),
23 ]
diff --git a/src/maasserver/models/node.py b/src/maasserver/models/node.py
index a763b16..a46bacb 100644
--- a/src/maasserver/models/node.py
+++ b/src/maasserver/models/node.py
@@ -864,6 +864,8 @@ class Node(CleanSave, TimestampedModel):
864 :ivar objects: The :class:`GeneralManager`.864 :ivar objects: The :class:`GeneralManager`.
865 :ivar install_rackd: An optional flag to indicate if this node should be865 :ivar install_rackd: An optional flag to indicate if this node should be
866 deployed with the rack controller.866 deployed with the rack controller.
867 :ivar install_kvm: An optional flag to indicate if this node should be
868 deployed with KVM and added to MAAS.
867 :ivar enable_ssh: An optional flag to indicate if this node can have869 :ivar enable_ssh: An optional flag to indicate if this node can have
868 ssh enabled during commissioning, allowing the user to ssh into the870 ssh enabled during commissioning, allowing the user to ssh into the
869 machine's commissioning environment using the user's SSH key.871 machine's commissioning environment using the user's SSH key.
@@ -1046,6 +1048,9 @@ class Node(CleanSave, TimestampedModel):
1046 # Used to deploy the rack controller on a installation machine.1048 # Used to deploy the rack controller on a installation machine.
1047 install_rackd = BooleanField(default=False)1049 install_rackd = BooleanField(default=False)
10481050
1051 # Used to deploy the rack controller on a installation machine.
1052 install_kvm = BooleanField(default=False)
1053
1049 # Used to determine whether to:1054 # Used to determine whether to:
1050 # 1. Import the SSH Key during commissioning and keep power on.1055 # 1. Import the SSH Key during commissioning and keep power on.
1051 # 2. Skip reconfiguring networking when a node is commissioned.1056 # 2. Skip reconfiguring networking when a node is commissioned.
@@ -2916,6 +2921,7 @@ class Node(CleanSave, TimestampedModel):
2916 self.hwe_kernel = None2921 self.hwe_kernel = None
2917 self.current_installation_script_set = None2922 self.current_installation_script_set = None
2918 self.install_rackd = False2923 self.install_rackd = False
2924 self.install_kvm = False
2919 self.save()2925 self.save()
29202926
2921 # Clear the nodes acquired filesystems.2927 # Clear the nodes acquired filesystems.
diff --git a/src/maasserver/testing/factory.py b/src/maasserver/testing/factory.py
index 7359e1f..1d91a62 100644
--- a/src/maasserver/testing/factory.py
+++ b/src/maasserver/testing/factory.py
@@ -203,17 +203,29 @@ class Messages:
203203
204class Factory(maastesting.factory.Factory):204class Factory(maastesting.factory.Factory):
205205
206 def make_fake_request(self, path, method="GET", cookies={}):206 def make_fake_request(
207 self, path="/", method="GET", cookies=None, data=None):
207 """Create a fake request.208 """Create a fake request.
208209
209 :param path: The path to which to make the request.210 :param path: The path to which to make the request.
210 :param method: The method to use for the request211 :param method: The method to use for the request
211 ('GET' or 'POST').212 ('GET' or 'POST').
212 :param cookies: A `dict` with the cookies for the request.213 :param cookies: Optional `dict` with the cookies for the request.
214 :param data: Optional `dict` of parameters.
213 """215 """
214 rf = MAASSensibleRequestFactory()216 rf = MAASSensibleRequestFactory()
215 request = rf.get(path)217 if data is None:
216 request.method = method218 data = {}
219 if cookies is None:
220 cookies = {}
221 if method == "GET":
222 request = rf.get(path, data=data)
223 elif method == "POST":
224 request = rf.post(path, data=data)
225 else:
226 request = rf.get(path, data=data)
227 request.method = method
228 request.data = data
217 request._messages = Messages()229 request._messages = Messages()
218 request.COOKIES = cookies.copy()230 request.COOKIES = cookies.copy()
219 return request231 return request
diff --git a/src/maasserver/websockets/handlers/device.py b/src/maasserver/websockets/handlers/device.py
index c3cf5c1..4c964a8 100644
--- a/src/maasserver/websockets/handlers/device.py
+++ b/src/maasserver/websockets/handlers/device.py
@@ -131,6 +131,7 @@ class DeviceHandler(NodeHandler):
131 "last_image_sync",131 "last_image_sync",
132 "default_user",132 "default_user",
133 "install_rackd",133 "install_rackd",
134 "install_kvm",
134 ]135 ]
135 list_fields = [136 list_fields = [
136 "id",137 "id",
diff --git a/src/maasserver/websockets/handlers/machine.py b/src/maasserver/websockets/handlers/machine.py
index f53a2f3..a73da82 100644
--- a/src/maasserver/websockets/handlers/machine.py
+++ b/src/maasserver/websockets/handlers/machine.py
@@ -178,6 +178,7 @@ class MachineHandler(NodeHandler):
178 "managing_process",178 "managing_process",
179 "last_image_sync",179 "last_image_sync",
180 "install_rackd",180 "install_rackd",
181 "install_kvm",
181 ]182 ]
182 list_fields = [183 list_fields = [
183 "id",184 "id",
diff --git a/src/metadataserver/api.py b/src/metadataserver/api.py
index b786954..e76a651 100644
--- a/src/metadataserver/api.py
+++ b/src/metadataserver/api.py
@@ -870,7 +870,20 @@ class UserDataHandler(MetadataViewHandler):
870 # for user-data is when MAAS hands the node870 # for user-data is when MAAS hands the node
871 # off to a user.871 # off to a user.
872 if node.status == NODE_STATUS.DEPLOYING:872 if node.status == NODE_STATUS.DEPLOYING:
873 node.end_deployment()873 if node.install_kvm:
874 # Rather than ending deployment here, note that we're
875 # installing a KVM pod.
876 node.agent_name = "maas-kvm-pod"
877 node.save()
878 else:
879 # MAAS currently considers a machine "Deployed" when the
880 # cloud-init user data is requested. Note that this doesn't
881 # mean the machine is ready for use yet; cloud-init will
882 # also send a 'finish' event for the 'modules-final'
883 # activity name. However, that check is ambiguous because
884 # it occurs both when curtin is installing, and when
885 # the machine reboots to finish its deployment.
886 node.end_deployment()
874 # If this node is supposed to be powered off, serve the887 # If this node is supposed to be powered off, serve the
875 # 'poweroff' userdata.888 # 'poweroff' userdata.
876 if node.get_boot_purpose() == 'poweroff':889 if node.get_boot_purpose() == 'poweroff':
diff --git a/src/metadataserver/api_twisted.py b/src/metadataserver/api_twisted.py
index 08a26d3..0659d30 100644
--- a/src/metadataserver/api_twisted.py
+++ b/src/metadataserver/api_twisted.py
@@ -14,7 +14,11 @@ from maasserver.enum import (
14 NODE_STATUS,14 NODE_STATUS,
15 NODE_TYPE,15 NODE_TYPE,
16)16)
17from maasserver.models.node import Node17from maasserver.forms.pods import PodForm
18from maasserver.models import (
19 Node,
20 NodeMetadata,
21)
18from maasserver.preseed import CURTIN_INSTALL_LOG22from maasserver.preseed import CURTIN_INSTALL_LOG
19from maasserver.utils.orm import (23from maasserver.utils.orm import (
20 in_transaction,24 in_transaction,
@@ -175,6 +179,47 @@ class StatusHandlerResource(Resource):
175 return NOT_DONE_YET179 return NOT_DONE_YET
176180
177181
182POD_CREATION_ERROR = (
183 "Internal error while creating KVM pod. (See regiond.log for details.)"
184)
185
186
187def _create_pod_for_deployment(node):
188 virsh_password_meta = NodeMetadata.objects.filter(
189 node=node, key="virsh_password").first()
190 if virsh_password_meta is None:
191 node.mark_failed(
192 comment="Failed to deploy KVM: Password not found.", commit=False)
193 else:
194 virsh_password = virsh_password_meta.value
195 virsh_password_meta.delete()
196 # XXX: Should find the best IP to communicate with, given what the rack
197 # controller can access, or use the boot interface IP address.
198 ip = node.ip_addresses()[0]
199 if ':' in ip:
200 ip = "[%s]" % ip
201 power_address = "qemu+ssh://virsh@%s/system" % ip
202 pod_form = PodForm(data=dict(
203 type="virsh",
204 name=node.hostname,
205 power_address=power_address,
206 power_pass=virsh_password,
207 zone=node.zone.name,
208 pool=node.pool.name,
209 ), user=node.owner)
210 if pod_form.is_valid():
211 try:
212 pod_form.save()
213 except Exception:
214 node.mark_failed(comment=POD_CREATION_ERROR, commit=False)
215 log.err(None, "Exception while saving pod form.")
216 else:
217 node.status = NODE_STATUS.DEPLOYED
218 else:
219 node.mark_failed(comment=POD_CREATION_ERROR, commit=False)
220 log.msg("Error while creating KVM pod: %s" % dict(pod_form.errors))
221
222
178class StatusWorkerService(TimerService, object):223class StatusWorkerService(TimerService, object):
179 """Service to update nodes from recieved status messages."""224 """Service to update nodes from recieved status messages."""
180225
@@ -257,7 +302,8 @@ class StatusWorkerService(TimerService, object):
257 # LP:1701352 - If no exit code is given by the client default to302 # LP:1701352 - If no exit code is given by the client default to
258 # 0(pass) unless the signal is fail then set to 1(failure). This allows303 # 0(pass) unless the signal is fail then set to 1(failure). This allows
259 # a Curtin failure to cause the ScriptResult to fail.304 # a Curtin failure to cause the ScriptResult to fail.
260 default_exit_status = 1 if result in ['FAIL', 'FAILURE'] else 0305 failed = result in ['FAIL', 'FAILURE']
306 default_exit_status = 1 if failed else 0
261307
262 # Add this event to the node event log.308 # Add this event to the node event log.
263 add_event_to_node_event_log(309 add_event_to_node_event_log(
@@ -310,7 +356,7 @@ class StatusWorkerService(TimerService, object):
310 # cloud-init may send a failure message if a script reboots356 # cloud-init may send a failure message if a script reboots
311 # the system. If a script is running which may_reboot ignore357 # the system. If a script is running which may_reboot ignore
312 # the signal.358 # the signal.
313 if result in ['FAIL', 'FAILURE']:359 if failed:
314 script_set = node.current_commissioning_script_set360 script_set = node.current_commissioning_script_set
315 if (script_set is None or not361 if (script_set is None or not
316 script_set.scriptresult_set.filter(362 script_set.scriptresult_set.filter(
@@ -323,18 +369,28 @@ class StatusWorkerService(TimerService, object):
323 script_result_status=SCRIPT_STATUS.ABORTED)369 script_result_status=SCRIPT_STATUS.ABORTED)
324 save_node = True370 save_node = True
325 elif node.status == NODE_STATUS.DEPLOYING:371 elif node.status == NODE_STATUS.DEPLOYING:
326 if result in ['FAIL', 'FAILURE']:372 # XXX: when activity_name == moudles-config, this currently
373 # /always/ fails, since MAAS passes two different versions
374 # for the apt configuration. The only reason why we don't
375 # see additional issues because of this is due to the node
376 # already being marked "Deployed". Right now this is prevented
377 # only in the install_kvm case, but we should make this check
378 # more general when time allows.
379 if failed and not node.install_kvm:
327 node.mark_failed(380 node.mark_failed(
328 comment="Installation failed (refer to the "381 comment="Installation failed (refer to the "
329 "installation log for more information).",382 "installation log for more information).",
330 commit=False)383 commit=False)
331 save_node = True384 save_node = True
385 elif (not failed and activity_name == "modules-final" and
386 node.install_kvm and node.agent_name == "maas-kvm-pod"):
387 save_node = True
388 _create_pod_for_deployment(node)
332 elif node.status == NODE_STATUS.DISK_ERASING:389 elif node.status == NODE_STATUS.DISK_ERASING:
333 if result in ['FAIL', 'FAILURE']:390 if failed:
334 node.mark_failed(391 node.mark_failed(
335 comment="Failed to erase disks.", commit=False)392 comment="Failed to erase disks.", commit=False)
336 save_node = True393 save_node = True
337
338 # Deallocate the node if we enter any terminal state.394 # Deallocate the node if we enter any terminal state.
339 if node.node_type == NODE_TYPE.MACHINE and node.status in [395 if node.node_type == NODE_TYPE.MACHINE and node.status in [
340 NODE_STATUS.READY,396 NODE_STATUS.READY,
diff --git a/src/metadataserver/tests/test_api.py b/src/metadataserver/tests/test_api.py
index 998df04..2e7b6cb 100644
--- a/src/metadataserver/tests/test_api.py
+++ b/src/metadataserver/tests/test_api.py
@@ -948,6 +948,17 @@ class TestMetadataUserDataStateChanges(MAASServerTestCase):
948 self.assertEqual(http.client.OK, response.status_code)948 self.assertEqual(http.client.OK, response.status_code)
949 self.assertEqual(NODE_STATUS.DEPLOYED, reload_object(node).status)949 self.assertEqual(NODE_STATUS.DEPLOYED, reload_object(node).status)
950950
951 def test_skips_status_change_if_installing_kvm_and_sets_agent_name(self):
952 node = factory.make_Node(
953 status=NODE_STATUS.DEPLOYING, install_kvm=True)
954 NodeUserData.objects.set_user_data(node, sample_binary_data)
955 client = make_node_client(node)
956 response = client.get(reverse('metadata-user-data', args=['latest']))
957 self.assertEqual(http.client.OK, response.status_code)
958 self.assertEqual(NODE_STATUS.DEPLOYING, reload_object(node).status)
959 node = reload_object(node)
960 self.assertEqual(node.agent_name, "maas-kvm-pod")
961
951962
952class TestCurtinMetadataUserData(963class TestCurtinMetadataUserData(
953 PreseedRPCMixin, MAASTransactionServerTestCase):964 PreseedRPCMixin, MAASTransactionServerTestCase):
diff --git a/src/metadataserver/tests/test_api_twisted.py b/src/metadataserver/tests/test_api_twisted.py
index e8c7e34..65e5014 100644
--- a/src/metadataserver/tests/test_api_twisted.py
+++ b/src/metadataserver/tests/test_api_twisted.py
@@ -24,6 +24,7 @@ from crochet import wait_for
24from maasserver.enum import NODE_STATUS24from maasserver.enum import NODE_STATUS
25from maasserver.models import (25from maasserver.models import (
26 Event,26 Event,
27 NodeMetadata,
27 Tag,28 Tag,
28)29)
29from maasserver.models.signals.testing import SignalsDisabled30from maasserver.models.signals.testing import SignalsDisabled
@@ -42,13 +43,19 @@ from maasserver.utils.orm import (
42)43)
43from maasserver.utils.threads import deferToDatabase44from maasserver.utils.threads import deferToDatabase
44from maastesting.matchers import (45from maastesting.matchers import (
46 DocTestMatches,
45 MockCalledOnceWith,47 MockCalledOnceWith,
46 MockCallsMatch,48 MockCallsMatch,
47 MockNotCalled,49 MockNotCalled,
48)50)
49from maastesting.testcase import MAASTestCase51from maastesting.testcase import MAASTestCase
50from metadataserver import api52from metadataserver import (
53 api,
54 api_twisted as api_twisted_module,
55)
51from metadataserver.api_twisted import (56from metadataserver.api_twisted import (
57 _create_pod_for_deployment,
58 POD_CREATION_ERROR,
52 StatusHandlerResource,59 StatusHandlerResource,
53 StatusWorkerService,60 StatusWorkerService,
54)61)
@@ -60,6 +67,7 @@ from metadataserver.models import NodeKey
60from testtools import ExpectedException67from testtools import ExpectedException
61from testtools.matchers import (68from testtools.matchers import (
62 Equals,69 Equals,
70 Is,
63 MatchesListwise,71 MatchesListwise,
64 MatchesSetwise,72 MatchesSetwise,
65)73)
@@ -343,7 +351,7 @@ def encode_as_base64(content):
343class TestStatusWorkerService(MAASServerTestCase):351class TestStatusWorkerService(MAASServerTestCase):
344352
345 def setUp(self):353 def setUp(self):
346 super(TestStatusWorkerService, self).setUp()354 super().setUp()
347 self.useFixture(SignalsDisabled("power"))355 self.useFixture(SignalsDisabled("power"))
348356
349 def processMessage(self, node, payload):357 def processMessage(self, node, payload):
@@ -505,6 +513,23 @@ class TestStatusWorkerService(MAASServerTestCase):
505 " log for more information).",513 " log for more information).",
506 Event.objects.filter(node=node).last().description)514 Event.objects.filter(node=node).last().description)
507515
516 def test_status_ok_for_modules_final_triggers_kvm_install(self):
517 node = factory.make_Node(
518 interface=True, status=NODE_STATUS.DEPLOYING,
519 agent_name="maas-kvm-pod", install_kvm=True)
520 payload = {
521 'event_type': 'finish',
522 'result': 'OK',
523 'origin': 'cloud-init',
524 'name': 'modules-final',
525 'description': 'America for Make Benefit Glorious Nation',
526 'timestamp': datetime.utcnow(),
527 }
528 mock_create_pod = self.patch(
529 api_twisted_module, '_create_pod_for_deployment')
530 self.processMessage(node, payload)
531 self.assertThat(mock_create_pod, MockCalledOnceWith(node))
532
508 def test_status_installation_fail_leaves_node_failed(self):533 def test_status_installation_fail_leaves_node_failed(self):
509 node = factory.make_Node(interface=True, status=NODE_STATUS.DEPLOYING)534 node = factory.make_Node(interface=True, status=NODE_STATUS.DEPLOYING)
510 payload = {535 payload = {
@@ -987,3 +1012,71 @@ class TestStatusWorkerService(MAASServerTestCase):
987 node.status_expires, expected_time - timedelta(minutes=1))1012 node.status_expires, expected_time - timedelta(minutes=1))
988 self.assertLessEqual(1013 self.assertLessEqual(
989 node.status_expires, expected_time + timedelta(minutes=1))1014 node.status_expires, expected_time + timedelta(minutes=1))
1015
1016
1017class TestCreatePodForDeployment(MAASServerTestCase):
1018
1019 def setUp(self):
1020 super().setUp()
1021 self.mock_PodForm = self.patch(api_twisted_module, "PodForm")
1022
1023 def test__marks_failed_if_no_virsh_password(self):
1024 node = factory.make_Node(
1025 interface=True, status=NODE_STATUS.DEPLOYING,
1026 agent_name="maas-kvm-pod", install_kvm=True)
1027 _create_pod_for_deployment(node)
1028 self.assertThat(node.status, Equals(NODE_STATUS.FAILED_DEPLOYMENT))
1029 self.assertThat(node.error_description, DocTestMatches(
1030 "...Password not found..."))
1031
1032 def test__deletes_virsh_password_metadata_and_sets_deployed(self):
1033 node = factory.make_Node_with_Interface_on_Subnet(
1034 status=NODE_STATUS.DEPLOYING, agent_name="maas-kvm-pod",
1035 install_kvm=True)
1036 factory.make_StaticIPAddress(interface=node.boot_interface)
1037 meta = NodeMetadata.objects.create(
1038 node=node, key="virsh_password", value="xyz123")
1039 _create_pod_for_deployment(node)
1040 meta = reload_object(meta)
1041 self.assertThat(meta, Is(None))
1042 self.assertThat(node.status, Equals(NODE_STATUS.DEPLOYED))
1043
1044 def test__marks_failed_if_is_valid_returns_false(self):
1045 mock_pod_form = Mock()
1046 self.mock_PodForm.return_value = mock_pod_form
1047 mock_pod_form.errors = {}
1048 mock_pod_form.is_valid = Mock()
1049 mock_pod_form.is_valid.return_value = False
1050 node = factory.make_Node_with_Interface_on_Subnet(
1051 status=NODE_STATUS.DEPLOYING, agent_name="maas-kvm-pod",
1052 install_kvm=True)
1053 factory.make_StaticIPAddress(interface=node.boot_interface)
1054 meta = NodeMetadata.objects.create(
1055 node=node, key="virsh_password", value="xyz123")
1056 _create_pod_for_deployment(node)
1057 meta = reload_object(meta)
1058 self.assertThat(meta, Is(None))
1059 self.assertThat(node.status, Equals(NODE_STATUS.FAILED_DEPLOYMENT))
1060 self.assertThat(node.error_description, DocTestMatches(
1061 POD_CREATION_ERROR))
1062
1063 def test__marks_failed_if_save_raises(self):
1064 mock_pod_form = Mock()
1065 self.mock_PodForm.return_value = mock_pod_form
1066 mock_pod_form.errors = {}
1067 mock_pod_form.is_valid = Mock()
1068 mock_pod_form.is_valid.return_value = True
1069 mock_pod_form.save = Mock()
1070 mock_pod_form.save.side_effect = ValueError
1071 node = factory.make_Node_with_Interface_on_Subnet(
1072 status=NODE_STATUS.DEPLOYING, agent_name="maas-kvm-pod",
1073 install_kvm=True)
1074 factory.make_StaticIPAddress(interface=node.boot_interface)
1075 meta = NodeMetadata.objects.create(
1076 node=node, key="virsh_password", value="xyz123")
1077 _create_pod_for_deployment(node)
1078 meta = reload_object(meta)
1079 self.assertThat(meta, Is(None))
1080 self.assertThat(node.status, Equals(NODE_STATUS.FAILED_DEPLOYMENT))
1081 self.assertThat(node.error_description, DocTestMatches(
1082 POD_CREATION_ERROR))
diff --git a/src/metadataserver/tests/test_vendor_data.py b/src/metadataserver/tests/test_vendor_data.py
index bd0475f..3b1a6d3 100644
--- a/src/metadataserver/tests/test_vendor_data.py
+++ b/src/metadataserver/tests/test_vendor_data.py
@@ -5,7 +5,10 @@
55
6__all__ = []6__all__ = []
77
8from maasserver.models.config import Config8from maasserver.models import (
9 Config,
10 NodeMetadata,
11)
9from maasserver.server_address import get_maas_facing_server_host12from maasserver.server_address import get_maas_facing_server_host
10from maasserver.testing.factory import factory13from maasserver.testing.factory import factory
11from maasserver.testing.testcase import MAASServerTestCase14from maasserver.testing.testcase import MAASServerTestCase
@@ -21,6 +24,7 @@ from testtools.matchers import (
21 Contains,24 Contains,
22 ContainsDict,25 ContainsDict,
23 Equals,26 Equals,
27 HasLength,
24 Is,28 Is,
25 IsInstance,29 IsInstance,
26 KeysEqual,30 KeysEqual,
@@ -221,3 +225,20 @@ class TestGenerateRackControllerConfiguration(MAASServerTestCase):
221 "%s --maas-url %s --secret %s" % (cmd, maas_url, secret),225 "%s --maas-url %s --secret %s" % (cmd, maas_url, secret),
222 ]226 ]
223 }))227 }))
228
229 def test_yields_configuration_when_machine_install_kvm_true(self):
230 node = factory.make_Node(osystem='ubuntu', netboot=False)
231 node.install_kvm = True
232 configuration = get_vendor_data(node)
233 config = str(dict(configuration))
234 self.assertThat(config, Contains("virsh"))
235 self.assertThat(config, Contains("ssh_pwauth"))
236 self.assertThat(config, Contains("rbash"))
237 self.assertThat(config, Contains("libvirt-qemu"))
238 self.assertThat(config, Contains("ForceCommand"))
239 self.assertThat(config, Contains("qemu-kvm"))
240 self.assertThat(config, Contains("libvirt-bin"))
241 # Check that a password was saved for the pod-to-be.
242 virsh_password_meta = NodeMetadata.objects.filter(
243 node=node, key="virsh_password").first()
244 self.assertThat(virsh_password_meta.value, HasLength(32))
diff --git a/src/metadataserver/vendor_data.py b/src/metadataserver/vendor_data.py
index a0c689d..b6bbf72 100644
--- a/src/metadataserver/vendor_data.py
+++ b/src/metadataserver/vendor_data.py
@@ -7,10 +7,16 @@ __all__ = [
7 'get_vendor_data',7 'get_vendor_data',
8 ]8 ]
99
10from base64 import b64encode
11from crypt import crypt
10from itertools import chain12from itertools import chain
13from os import urandom
1114
12from maasserver import ntp15from maasserver import ntp
13from maasserver.models import Config16from maasserver.models import (
17 Config,
18 NodeMetadata,
19)
14from maasserver.server_address import get_maas_facing_server_host20from maasserver.server_address import get_maas_facing_server_host
15from netaddr import IPAddress21from netaddr import IPAddress
16from provisioningserver.ntp.config import normalise_address22from provisioningserver.ntp.config import normalise_address
@@ -23,6 +29,7 @@ def get_vendor_data(node):
23 generate_system_info(node),29 generate_system_info(node),
24 generate_ntp_configuration(node),30 generate_ntp_configuration(node),
25 generate_rack_controller_configuration(node),31 generate_rack_controller_configuration(node),
32 generate_kvm_pod_configuration(node),
26 ))33 ))
2734
2835
@@ -91,3 +98,56 @@ def generate_rack_controller_configuration(node):
91 "/snap/bin/maas init --mode rack --maas-url %s --secret %s" % (98 "/snap/bin/maas init --mode rack --maas-url %s --secret %s" % (
92 maas_url, secret)99 maas_url, secret)
93 ]100 ]
101
102
103def generate_kvm_pod_configuration(node):
104 """Generate cloud-init configuration to install the node as a KVM pod."""
105 if node.netboot is False and node.install_kvm is True:
106 yield "runcmd", [
107 # Restrict the $PATH so that rbash can be used to limit what the
108 # virsh user can do if they manage to get a shell.
109 ['mkdir', '-p', '/home/virsh/bin'],
110 ['ln', '-s', '/usr/bin/virsh', '/home/virsh/bin/virsh'],
111 ['sh', '-c', 'echo "PATH=/home/virsh/bin" >> /home/virsh/.bashrc'],
112 # Use a ForceCommand to make sure the only thing the virsh user
113 # can do with SSH is communicate with libvirt.
114 ['sh', '-c',
115 'printf "Match user virsh\\n'
116 ' X11Forwarding no\\n'
117 ' AllowTcpForwarding no\\n'
118 ' PermitTTY no\\n'
119 ' ForceCommand nc -q 0 -U /var/run/libvirt/libvirt-sock\\n"'
120 ' >> /etc/ssh/sshd_config'],
121 # Make sure the 'virsh' user is allowed to access libvirt.
122 ['/usr/sbin/usermod', '--append', '--groups',
123 'libvirt,libvirt-qemu', 'virsh'],
124 # SSH needs to be restarted in order for the above changes to
125 # take effect.
126 ['systemctl', 'restart', 'sshd'],
127 # Ensure services are ready before cloud-init finishes.
128 ['/bin/sleep', '10'],
129 ]
130 # Generate a 32-character password by encoding 24 bytes as base64.
131 virsh_password = b64encode(
132 urandom(24), altchars=b'.!').decode('ascii')
133 # Pass crypted (salted/hashed) version of the password to cloud-init.
134 encrypted_password = crypt(virsh_password)
135 # Store a cleartext version of the password so we can add a pod later.
136 NodeMetadata.objects.update_or_create(
137 node=node, key="virsh_password",
138 defaults=dict(value=virsh_password))
139 # Make sure SSH password authentication is enabled.
140 yield "ssh_pwauth", True
141 # Create a custom 'virsh' user (in addition to the default user)
142 # with the encrypted password, and a locked-down shell.
143 yield "users", [
144 'default',
145 {
146 'name': 'virsh',
147 'lock_passwd': False,
148 'passwd': encrypted_password,
149 'shell': '/bin/rbash',
150 }
151 ]
152 # XXX: Use correct packages based on architecture.
153 yield "packages", ["qemu-kvm", "libvirt-bin"]

Subscribers

People subscribed via source and target branches