Merge ~mpontillo/maas:install-kvm into maas:master
- Git
- lp:~mpontillo/maas
- install-kvm
- Merge into 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) |
Related bugs: |
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.
Description of the change
To post a comment you must log in.
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
MAAS Lander (maas-lander) wrote : | # |
LANDING
-b install-kvm lp:~mpontillo/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED BUILD
LOG: http://
~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
1 | diff --git a/src/maasserver/api/machines.py b/src/maasserver/api/machines.py | |||
2 | index 9a95cc8..12cbc69 100644 | |||
3 | --- a/src/maasserver/api/machines.py | |||
4 | +++ b/src/maasserver/api/machines.py | |||
5 | @@ -8,6 +8,7 @@ __all__ = [ | |||
6 | 8 | "get_storage_layout_params", | 8 | "get_storage_layout_params", |
7 | 9 | ] | 9 | ] |
8 | 10 | 10 | ||
9 | 11 | from collections import namedtuple | ||
10 | 11 | import json | 12 | import json |
11 | 12 | import re | 13 | import re |
12 | 13 | 14 | ||
13 | @@ -193,6 +194,19 @@ DISPLAYED_ANON_MACHINE_FIELDS = ( | |||
14 | 193 | ) | 194 | ) |
15 | 194 | 195 | ||
16 | 195 | 196 | ||
17 | 197 | AllocationOptions = namedtuple( | ||
18 | 198 | 'AllocationOptions', ( | ||
19 | 199 | 'agent_name', | ||
20 | 200 | 'bridge_all', | ||
21 | 201 | 'bridge_fd', | ||
22 | 202 | 'bridge_stp', | ||
23 | 203 | 'comment', | ||
24 | 204 | 'install_rackd', | ||
25 | 205 | 'install_kvm', | ||
26 | 206 | ) | ||
27 | 207 | ) | ||
28 | 208 | |||
29 | 209 | |||
30 | 196 | def get_storage_layout_params(request, required=False, extract_params=False): | 210 | def get_storage_layout_params(request, required=False, extract_params=False): |
31 | 197 | """Return and validate the storage_layout parameter.""" | 211 | """Return and validate the storage_layout parameter.""" |
32 | 198 | form = StorageLayoutForm(required=required, data=request.data) | 212 | form = StorageLayoutForm(required=required, data=request.data) |
33 | @@ -218,11 +232,18 @@ def get_storage_layout_params(request, required=False, extract_params=False): | |||
34 | 218 | return storage_layout, params | 232 | return storage_layout, params |
35 | 219 | 233 | ||
36 | 220 | 234 | ||
39 | 221 | def get_allocation_parameters(request): | 235 | def get_allocation_options(request) -> AllocationOptions: |
40 | 222 | """Returns shared parameters for deploy and allocate operations.""" | 236 | """Parses optional parameters for allocation and deployment.""" |
41 | 223 | comment = get_optional_param(request.POST, 'comment') | 237 | comment = get_optional_param(request.POST, 'comment') |
42 | 238 | default_bridge_all = False | ||
43 | 239 | install_rackd = get_optional_param( | ||
44 | 240 | request.POST, 'install_rackd', default=False, validator=StringBool) | ||
45 | 241 | install_kvm = get_optional_param( | ||
46 | 242 | request.POST, 'install_kvm', default=False, validator=StringBool) | ||
47 | 243 | if install_kvm: | ||
48 | 244 | default_bridge_all = True | ||
49 | 224 | bridge_all = get_optional_param( | 245 | bridge_all = get_optional_param( |
51 | 225 | request.POST, 'bridge_all', default=False, | 246 | request.POST, 'bridge_all', default=default_bridge_all, |
52 | 226 | validator=StringBool) | 247 | validator=StringBool) |
53 | 227 | bridge_stp = get_optional_param( | 248 | bridge_stp = get_optional_param( |
54 | 228 | request.POST, 'bridge_stp', default=False, | 249 | request.POST, 'bridge_stp', default=False, |
55 | @@ -230,7 +251,15 @@ def get_allocation_parameters(request): | |||
56 | 230 | bridge_fd = get_optional_param( | 251 | bridge_fd = get_optional_param( |
57 | 231 | request.POST, 'bridge_fd', default=0, validator=Int) | 252 | request.POST, 'bridge_fd', default=0, validator=Int) |
58 | 232 | agent_name = request.data.get('agent_name', '') | 253 | agent_name = request.data.get('agent_name', '') |
60 | 233 | return agent_name, bridge_all, bridge_fd, bridge_stp, comment | 254 | return AllocationOptions( |
61 | 255 | agent_name, | ||
62 | 256 | bridge_all, | ||
63 | 257 | bridge_fd, | ||
64 | 258 | bridge_stp, | ||
65 | 259 | comment, | ||
66 | 260 | install_rackd, | ||
67 | 261 | install_kvm | ||
68 | 262 | ) | ||
69 | 234 | 263 | ||
70 | 235 | 264 | ||
71 | 236 | def get_allocated_composed_machine( | 265 | def get_allocated_composed_machine( |
72 | @@ -556,6 +585,9 @@ class MachineHandler(NodeHandler, OwnerDataMixin, PowerMixin): | |||
73 | 556 | :param install_rackd: If True, the Rack Controller will be installed on | 585 | :param install_rackd: If True, the Rack Controller will be installed on |
74 | 557 | this machine. | 586 | this machine. |
75 | 558 | :type install_rackd: boolean | 587 | :type install_rackd: boolean |
76 | 588 | :param install_kvm: If True, KVM will be installed on this machine and | ||
77 | 589 | added to MAAS. | ||
78 | 590 | :type install_kvm: boolean | ||
79 | 559 | 591 | ||
80 | 560 | Ideally we'd have MIME multipart and content-transfer-encoding etc. | 592 | Ideally we'd have MIME multipart and content-transfer-encoding etc. |
81 | 561 | deal with the encapsulation of binary data, but couldn't make it work | 593 | deal with the encapsulation of binary data, but couldn't make it work |
82 | @@ -571,27 +603,24 @@ class MachineHandler(NodeHandler, OwnerDataMixin, PowerMixin): | |||
83 | 571 | series = request.POST.get('distro_series', None) | 603 | series = request.POST.get('distro_series', None) |
84 | 572 | license_key = request.POST.get('license_key', None) | 604 | license_key = request.POST.get('license_key', None) |
85 | 573 | hwe_kernel = request.POST.get('hwe_kernel', None) | 605 | hwe_kernel = request.POST.get('hwe_kernel', None) |
86 | 574 | install_rackd = get_optional_param( | ||
87 | 575 | request.POST, 'install_rackd', default=False, validator=StringBool) | ||
88 | 576 | # Acquiring a node requires VIEW permissions. | 606 | # Acquiring a node requires VIEW permissions. |
89 | 577 | machine = self.model.objects.get_node_or_404( | 607 | machine = self.model.objects.get_node_or_404( |
90 | 578 | system_id=system_id, user=request.user, | 608 | system_id=system_id, user=request.user, |
91 | 579 | perm=NODE_PERMISSION.VIEW) | 609 | perm=NODE_PERMISSION.VIEW) |
92 | 610 | options = get_allocation_options(request) | ||
93 | 580 | if machine.status == NODE_STATUS.READY: | 611 | if machine.status == NODE_STATUS.READY: |
94 | 581 | with locks.node_acquire: | 612 | with locks.node_acquire: |
95 | 582 | if machine.owner is not None and machine.owner != request.user: | 613 | if machine.owner is not None and machine.owner != request.user: |
96 | 583 | raise NodeStateViolation( | 614 | raise NodeStateViolation( |
97 | 584 | "Can't allocate a machine belonging to another user.") | 615 | "Can't allocate a machine belonging to another user.") |
98 | 585 | agent_name, bridge_all, bridge_fd, bridge_stp, comment = ( | ||
99 | 586 | get_allocation_parameters(request)) | ||
100 | 587 | maaslog.info( | 616 | maaslog.info( |
101 | 588 | "Request from user %s to acquire machine: %s (%s)", | 617 | "Request from user %s to acquire machine: %s (%s)", |
102 | 589 | request.user.username, machine.fqdn, machine.system_id) | 618 | request.user.username, machine.fqdn, machine.system_id) |
103 | 590 | machine.acquire( | 619 | machine.acquire( |
104 | 591 | request.user, get_oauth_token(request), | 620 | request.user, get_oauth_token(request), |
108 | 592 | agent_name=agent_name, comment=comment, | 621 | agent_name=options.agent_name, comment=options.comment, |
109 | 593 | bridge_all=bridge_all, bridge_stp=bridge_stp, | 622 | bridge_all=options.bridge_all, |
110 | 594 | bridge_fd=bridge_fd) | 623 | bridge_stp=options.bridge_stp, bridge_fd=options.bridge_fd) |
111 | 595 | if NODE_STATUS.DEPLOYING not in NODE_TRANSITIONS[machine.status]: | 624 | if NODE_STATUS.DEPLOYING not in NODE_TRANSITIONS[machine.status]: |
112 | 596 | raise NodeStateViolation( | 625 | raise NodeStateViolation( |
113 | 597 | "Can't deploy a machine that is in the '{}' state".format( | 626 | "Can't deploy a machine that is in the '{}' state".format( |
114 | @@ -600,7 +629,11 @@ class MachineHandler(NodeHandler, OwnerDataMixin, PowerMixin): | |||
115 | 600 | if not request.user.has_perm(NODE_PERMISSION.EDIT, machine): | 629 | if not request.user.has_perm(NODE_PERMISSION.EDIT, machine): |
116 | 601 | raise PermissionDenied() | 630 | raise PermissionDenied() |
117 | 602 | # Deploying with 'install_rackd' requires ADMIN permissions. | 631 | # Deploying with 'install_rackd' requires ADMIN permissions. |
119 | 603 | if (install_rackd and not | 632 | if (options.install_rackd and not |
120 | 633 | request.user.has_perm(NODE_PERMISSION.ADMIN, machine)): | ||
121 | 634 | raise PermissionDenied() | ||
122 | 635 | # Deploying with 'install_kvm' requires ADMIN permissions. | ||
123 | 636 | if (options.install_kvm and not | ||
124 | 604 | request.user.has_perm(NODE_PERMISSION.ADMIN, machine)): | 637 | request.user.has_perm(NODE_PERMISSION.ADMIN, machine)): |
125 | 605 | raise PermissionDenied() | 638 | raise PermissionDenied() |
126 | 606 | if not machine.distro_series and not series: | 639 | if not machine.distro_series and not series: |
127 | @@ -613,8 +646,10 @@ class MachineHandler(NodeHandler, OwnerDataMixin, PowerMixin): | |||
128 | 613 | form.set_license_key(license_key=license_key) | 646 | form.set_license_key(license_key=license_key) |
129 | 614 | if hwe_kernel is not None: | 647 | if hwe_kernel is not None: |
130 | 615 | form.set_hwe_kernel(hwe_kernel=hwe_kernel) | 648 | form.set_hwe_kernel(hwe_kernel=hwe_kernel) |
133 | 616 | if install_rackd: | 649 | if options.install_rackd: |
134 | 617 | form.set_install_rackd(install_rackd=install_rackd) | 650 | form.set_install_rackd(install_rackd=options.install_rackd) |
135 | 651 | if options.install_kvm: | ||
136 | 652 | form.set_install_kvm(install_kvm=options.install_kvm) | ||
137 | 618 | if form.is_valid(): | 653 | if form.is_valid(): |
138 | 619 | form.save() | 654 | form.save() |
139 | 620 | else: | 655 | else: |
140 | @@ -1747,8 +1782,7 @@ class MachinesHandler(NodesHandler, PowersMixin): | |||
141 | 1747 | maaslog.info( | 1782 | maaslog.info( |
142 | 1748 | "Request from user %s to acquire a machine with constraints: %s", | 1783 | "Request from user %s to acquire a machine with constraints: %s", |
143 | 1749 | request.user.username, str(input_constraints)) | 1784 | request.user.username, str(input_constraints)) |
146 | 1750 | agent_name, bridge_all, bridge_fd, bridge_stp, comment = ( | 1785 | options = get_allocation_options(request) |
145 | 1751 | get_allocation_parameters(request)) | ||
147 | 1752 | verbose = get_optional_param( | 1786 | verbose = get_optional_param( |
148 | 1753 | request.POST, 'verbose', default=False, validator=StringBool) | 1787 | request.POST, 'verbose', default=False, validator=StringBool) |
149 | 1754 | dry_run = get_optional_param( | 1788 | dry_run = get_optional_param( |
150 | @@ -1814,9 +1848,9 @@ class MachinesHandler(NodesHandler, PowersMixin): | |||
151 | 1814 | if not dry_run: | 1848 | if not dry_run: |
152 | 1815 | machine.acquire( | 1849 | machine.acquire( |
153 | 1816 | request.user, get_oauth_token(request), | 1850 | request.user, get_oauth_token(request), |
157 | 1817 | agent_name=agent_name, comment=comment, | 1851 | agent_name=options.agent_name, comment=options.comment, |
158 | 1818 | bridge_all=bridge_all, bridge_stp=bridge_stp, | 1852 | bridge_all=options.bridge_all, |
159 | 1819 | bridge_fd=bridge_fd) | 1853 | bridge_stp=options.bridge_stp, bridge_fd=options.bridge_fd) |
160 | 1820 | machine.constraint_map = storage.get(machine.id, {}) | 1854 | machine.constraint_map = storage.get(machine.id, {}) |
161 | 1821 | machine.constraints_by_type = {} | 1855 | machine.constraints_by_type = {} |
162 | 1822 | # Need to get the interface constraints map into the proper format | 1856 | # Need to get the interface constraints map into the proper format |
163 | diff --git a/src/maasserver/api/tests/test_machines.py b/src/maasserver/api/tests/test_machines.py | |||
164 | index 3ef0831..ddb305e 100644 | |||
165 | --- a/src/maasserver/api/tests/test_machines.py | |||
166 | +++ b/src/maasserver/api/tests/test_machines.py | |||
167 | @@ -16,6 +16,10 @@ from maasserver import ( | |||
168 | 16 | middleware, | 16 | middleware, |
169 | 17 | ) | 17 | ) |
170 | 18 | from maasserver.api import machines as machines_module | 18 | from maasserver.api import machines as machines_module |
171 | 19 | from maasserver.api.machines import ( | ||
172 | 20 | AllocationOptions, | ||
173 | 21 | get_allocation_options, | ||
174 | 22 | ) | ||
175 | 19 | from maasserver.enum import ( | 23 | from maasserver.enum import ( |
176 | 20 | INTERFACE_TYPE, | 24 | INTERFACE_TYPE, |
177 | 21 | NODE_STATUS, | 25 | NODE_STATUS, |
178 | @@ -2774,3 +2778,35 @@ class TestPowerState(APITransactionTestCase.ForUser): | |||
179 | 2774 | self.assertEqual({"state": random_state}, response) | 2778 | self.assertEqual({"state": random_state}, response) |
180 | 2775 | # The machine's power state is now `random_state`. | 2779 | # The machine's power state is now `random_state`. |
181 | 2776 | self.assertPowerState(machine, random_state) | 2780 | self.assertPowerState(machine, random_state) |
182 | 2781 | |||
183 | 2782 | |||
184 | 2783 | class TestGetAllocationOptions(MAASTestCase): | ||
185 | 2784 | |||
186 | 2785 | def test_defaults(self): | ||
187 | 2786 | request = factory.make_fake_request(method="POST", data={}) | ||
188 | 2787 | options = get_allocation_options(request) | ||
189 | 2788 | expected_options = AllocationOptions( | ||
190 | 2789 | agent_name='', bridge_all=False, bridge_fd=0, bridge_stp=False, | ||
191 | 2790 | comment=None, install_rackd=False, install_kvm=False) | ||
192 | 2791 | self.assertThat(options, Equals(expected_options)) | ||
193 | 2792 | |||
194 | 2793 | def test_sets_bridge_all_if_install_kvm(self): | ||
195 | 2794 | request = factory.make_fake_request( | ||
196 | 2795 | method="POST", data=dict(install_kvm='true')) | ||
197 | 2796 | options = get_allocation_options(request) | ||
198 | 2797 | expected_options = AllocationOptions( | ||
199 | 2798 | agent_name='', bridge_all=True, bridge_fd=0, bridge_stp=False, | ||
200 | 2799 | comment=None, install_rackd=False, install_kvm=True) | ||
201 | 2800 | self.assertThat(options, Equals(expected_options)) | ||
202 | 2801 | |||
203 | 2802 | def test_non_defaults(self): | ||
204 | 2803 | request = factory.make_fake_request(method="POST", data=dict( | ||
205 | 2804 | install_rackd="true", install_kvm="true", bridge_all="true", | ||
206 | 2805 | bridge_stp="true", bridge_fd="42", agent_name="maas", | ||
207 | 2806 | comment="don't panic" | ||
208 | 2807 | )) | ||
209 | 2808 | options = get_allocation_options(request) | ||
210 | 2809 | expected_options = AllocationOptions( | ||
211 | 2810 | agent_name='maas', bridge_all=True, bridge_fd=42, bridge_stp=True, | ||
212 | 2811 | comment="don't panic", install_rackd=True, install_kvm=True) | ||
213 | 2812 | self.assertThat(options, Equals(expected_options)) | ||
214 | diff --git a/src/maasserver/forms/__init__.py b/src/maasserver/forms/__init__.py | |||
215 | index 69440fa..9ad9129 100644 | |||
216 | --- a/src/maasserver/forms/__init__.py | |||
217 | +++ b/src/maasserver/forms/__init__.py | |||
218 | @@ -842,6 +842,11 @@ class MachineForm(NodeForm): | |||
219 | 842 | self.is_bound = True | 842 | self.is_bound = True |
220 | 843 | self.data['install_rackd'] = install_rackd | 843 | self.data['install_rackd'] = install_rackd |
221 | 844 | 844 | ||
222 | 845 | def set_install_kvm(self, install_kvm=False): | ||
223 | 846 | """Sets whether to deploy the rack alongside this machine.""" | ||
224 | 847 | self.is_bound = True | ||
225 | 848 | self.data['install_kvm'] = install_kvm | ||
226 | 849 | |||
227 | 845 | class Meta: | 850 | class Meta: |
228 | 846 | model = Machine | 851 | model = Machine |
229 | 847 | 852 | ||
230 | @@ -853,6 +858,7 @@ class MachineForm(NodeForm): | |||
231 | 853 | 'min_hwe_kernel', | 858 | 'min_hwe_kernel', |
232 | 854 | 'hwe_kernel', | 859 | 'hwe_kernel', |
233 | 855 | 'install_rackd', | 860 | 'install_rackd', |
234 | 861 | 'install_kvm', | ||
235 | 856 | ) | 862 | ) |
236 | 857 | 863 | ||
237 | 858 | 864 | ||
238 | diff --git a/src/maasserver/forms/pods.py b/src/maasserver/forms/pods.py | |||
239 | index 9b457b3..4f5fa73 100644 | |||
240 | --- a/src/maasserver/forms/pods.py | |||
241 | +++ b/src/maasserver/forms/pods.py | |||
242 | @@ -133,9 +133,11 @@ class PodForm(MAASModelForm): | |||
243 | 133 | label="Default MACVLAN mode", required=False, | 133 | label="Default MACVLAN mode", required=False, |
244 | 134 | choices=MACVLAN_MODE_CHOICES, initial=MACVLAN_MODE_CHOICES[0]) | 134 | choices=MACVLAN_MODE_CHOICES, initial=MACVLAN_MODE_CHOICES[0]) |
245 | 135 | 135 | ||
247 | 136 | def __init__(self, data=None, instance=None, request=None, **kwargs): | 136 | def __init__( |
248 | 137 | self, data=None, instance=None, request=None, user=None, **kwargs): | ||
249 | 137 | self.is_new = instance is None | 138 | self.is_new = instance is None |
250 | 138 | self.request = request | 139 | self.request = request |
251 | 140 | self.user = user | ||
252 | 139 | super(PodForm, self).__init__( | 141 | super(PodForm, self).__init__( |
253 | 140 | data=data, instance=instance, **kwargs) | 142 | data=data, instance=instance, **kwargs) |
254 | 141 | if data is None: | 143 | if data is None: |
255 | @@ -273,7 +275,11 @@ class PodForm(MAASModelForm): | |||
256 | 273 | # also create it in the database. | 275 | # also create it in the database. |
257 | 274 | if not self.instance.name: | 276 | if not self.instance.name: |
258 | 275 | self.instance.set_random_name() | 277 | self.instance.set_random_name() |
260 | 276 | self.instance.sync(discovered_pod, self.request.user) | 278 | if self.request is not None: |
261 | 279 | user = self.request.user | ||
262 | 280 | else: | ||
263 | 281 | user = self.user | ||
264 | 282 | self.instance.sync(discovered_pod, user) | ||
265 | 277 | 283 | ||
266 | 278 | # Save which rack controllers can route and which cannot. | 284 | # Save which rack controllers can route and which cannot. |
267 | 279 | discovered_rack_ids = [ | 285 | discovered_rack_ids = [ |
268 | diff --git a/src/maasserver/forms/tests/test_machine.py b/src/maasserver/forms/tests/test_machine.py | |||
269 | index 4bca075..c8ed726 100644 | |||
270 | --- a/src/maasserver/forms/tests/test_machine.py | |||
271 | +++ b/src/maasserver/forms/tests/test_machine.py | |||
272 | @@ -50,6 +50,7 @@ class TestMachineForm(MAASServerTestCase): | |||
273 | 50 | 'min_hwe_kernel', | 50 | 'min_hwe_kernel', |
274 | 51 | 'hwe_kernel', | 51 | 'hwe_kernel', |
275 | 52 | 'install_rackd', | 52 | 'install_rackd', |
276 | 53 | 'install_kvm', | ||
277 | 53 | ], list(form.fields)) | 54 | ], list(form.fields)) |
278 | 54 | 55 | ||
279 | 55 | def test_accepts_usable_architecture(self): | 56 | def test_accepts_usable_architecture(self): |
280 | @@ -366,6 +367,7 @@ class TestAdminMachineForm(MAASServerTestCase): | |||
281 | 366 | 'min_hwe_kernel', | 367 | 'min_hwe_kernel', |
282 | 367 | 'hwe_kernel', | 368 | 'hwe_kernel', |
283 | 368 | 'install_rackd', | 369 | 'install_rackd', |
284 | 370 | 'install_kvm', | ||
285 | 369 | 'cpu_count', | 371 | 'cpu_count', |
286 | 370 | 'memory', | 372 | 'memory', |
287 | 371 | 'zone', | 373 | 'zone', |
288 | 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 | |||
289 | 372 | new file mode 100644 | 374 | new file mode 100644 |
290 | index 0000000..5cce8a1 | |||
291 | --- /dev/null | |||
292 | +++ b/src/maasserver/migrations/builtin/maasserver/0173_add_node_install_kvm.py | |||
293 | @@ -0,0 +1,23 @@ | |||
294 | 1 | # -*- coding: utf-8 -*- | ||
295 | 2 | # Generated by Django 1.11.11 on 2018-09-15 00:04 | ||
296 | 3 | from __future__ import unicode_literals | ||
297 | 4 | |||
298 | 5 | from django.db import ( | ||
299 | 6 | migrations, | ||
300 | 7 | models, | ||
301 | 8 | ) | ||
302 | 9 | |||
303 | 10 | |||
304 | 11 | class Migration(migrations.Migration): | ||
305 | 12 | |||
306 | 13 | dependencies = [ | ||
307 | 14 | ('maasserver', '0172_partition_tags'), | ||
308 | 15 | ] | ||
309 | 16 | |||
310 | 17 | operations = [ | ||
311 | 18 | migrations.AddField( | ||
312 | 19 | model_name='node', | ||
313 | 20 | name='install_kvm', | ||
314 | 21 | field=models.BooleanField(default=False), | ||
315 | 22 | ), | ||
316 | 23 | ] | ||
317 | diff --git a/src/maasserver/models/node.py b/src/maasserver/models/node.py | |||
318 | index a763b16..a46bacb 100644 | |||
319 | --- a/src/maasserver/models/node.py | |||
320 | +++ b/src/maasserver/models/node.py | |||
321 | @@ -864,6 +864,8 @@ class Node(CleanSave, TimestampedModel): | |||
322 | 864 | :ivar objects: The :class:`GeneralManager`. | 864 | :ivar objects: The :class:`GeneralManager`. |
323 | 865 | :ivar install_rackd: An optional flag to indicate if this node should be | 865 | :ivar install_rackd: An optional flag to indicate if this node should be |
324 | 866 | deployed with the rack controller. | 866 | deployed with the rack controller. |
325 | 867 | :ivar install_kvm: An optional flag to indicate if this node should be | ||
326 | 868 | deployed with KVM and added to MAAS. | ||
327 | 867 | :ivar enable_ssh: An optional flag to indicate if this node can have | 869 | :ivar enable_ssh: An optional flag to indicate if this node can have |
328 | 868 | ssh enabled during commissioning, allowing the user to ssh into the | 870 | ssh enabled during commissioning, allowing the user to ssh into the |
329 | 869 | machine's commissioning environment using the user's SSH key. | 871 | machine's commissioning environment using the user's SSH key. |
330 | @@ -1046,6 +1048,9 @@ class Node(CleanSave, TimestampedModel): | |||
331 | 1046 | # Used to deploy the rack controller on a installation machine. | 1048 | # Used to deploy the rack controller on a installation machine. |
332 | 1047 | install_rackd = BooleanField(default=False) | 1049 | install_rackd = BooleanField(default=False) |
333 | 1048 | 1050 | ||
334 | 1051 | # Used to deploy the rack controller on a installation machine. | ||
335 | 1052 | install_kvm = BooleanField(default=False) | ||
336 | 1053 | |||
337 | 1049 | # Used to determine whether to: | 1054 | # Used to determine whether to: |
338 | 1050 | # 1. Import the SSH Key during commissioning and keep power on. | 1055 | # 1. Import the SSH Key during commissioning and keep power on. |
339 | 1051 | # 2. Skip reconfiguring networking when a node is commissioned. | 1056 | # 2. Skip reconfiguring networking when a node is commissioned. |
340 | @@ -2916,6 +2921,7 @@ class Node(CleanSave, TimestampedModel): | |||
341 | 2916 | self.hwe_kernel = None | 2921 | self.hwe_kernel = None |
342 | 2917 | self.current_installation_script_set = None | 2922 | self.current_installation_script_set = None |
343 | 2918 | self.install_rackd = False | 2923 | self.install_rackd = False |
344 | 2924 | self.install_kvm = False | ||
345 | 2919 | self.save() | 2925 | self.save() |
346 | 2920 | 2926 | ||
347 | 2921 | # Clear the nodes acquired filesystems. | 2927 | # Clear the nodes acquired filesystems. |
348 | diff --git a/src/maasserver/testing/factory.py b/src/maasserver/testing/factory.py | |||
349 | index 7359e1f..1d91a62 100644 | |||
350 | --- a/src/maasserver/testing/factory.py | |||
351 | +++ b/src/maasserver/testing/factory.py | |||
352 | @@ -203,17 +203,29 @@ class Messages: | |||
353 | 203 | 203 | ||
354 | 204 | class Factory(maastesting.factory.Factory): | 204 | class Factory(maastesting.factory.Factory): |
355 | 205 | 205 | ||
357 | 206 | def make_fake_request(self, path, method="GET", cookies={}): | 206 | def make_fake_request( |
358 | 207 | self, path="/", method="GET", cookies=None, data=None): | ||
359 | 207 | """Create a fake request. | 208 | """Create a fake request. |
360 | 208 | 209 | ||
361 | 209 | :param path: The path to which to make the request. | 210 | :param path: The path to which to make the request. |
362 | 210 | :param method: The method to use for the request | 211 | :param method: The method to use for the request |
363 | 211 | ('GET' or 'POST'). | 212 | ('GET' or 'POST'). |
365 | 212 | :param cookies: A `dict` with the cookies for the request. | 213 | :param cookies: Optional `dict` with the cookies for the request. |
366 | 214 | :param data: Optional `dict` of parameters. | ||
367 | 213 | """ | 215 | """ |
368 | 214 | rf = MAASSensibleRequestFactory() | 216 | rf = MAASSensibleRequestFactory() |
371 | 215 | request = rf.get(path) | 217 | if data is None: |
372 | 216 | request.method = method | 218 | data = {} |
373 | 219 | if cookies is None: | ||
374 | 220 | cookies = {} | ||
375 | 221 | if method == "GET": | ||
376 | 222 | request = rf.get(path, data=data) | ||
377 | 223 | elif method == "POST": | ||
378 | 224 | request = rf.post(path, data=data) | ||
379 | 225 | else: | ||
380 | 226 | request = rf.get(path, data=data) | ||
381 | 227 | request.method = method | ||
382 | 228 | request.data = data | ||
383 | 217 | request._messages = Messages() | 229 | request._messages = Messages() |
384 | 218 | request.COOKIES = cookies.copy() | 230 | request.COOKIES = cookies.copy() |
385 | 219 | return request | 231 | return request |
386 | diff --git a/src/maasserver/websockets/handlers/device.py b/src/maasserver/websockets/handlers/device.py | |||
387 | index c3cf5c1..4c964a8 100644 | |||
388 | --- a/src/maasserver/websockets/handlers/device.py | |||
389 | +++ b/src/maasserver/websockets/handlers/device.py | |||
390 | @@ -131,6 +131,7 @@ class DeviceHandler(NodeHandler): | |||
391 | 131 | "last_image_sync", | 131 | "last_image_sync", |
392 | 132 | "default_user", | 132 | "default_user", |
393 | 133 | "install_rackd", | 133 | "install_rackd", |
394 | 134 | "install_kvm", | ||
395 | 134 | ] | 135 | ] |
396 | 135 | list_fields = [ | 136 | list_fields = [ |
397 | 136 | "id", | 137 | "id", |
398 | diff --git a/src/maasserver/websockets/handlers/machine.py b/src/maasserver/websockets/handlers/machine.py | |||
399 | index f53a2f3..a73da82 100644 | |||
400 | --- a/src/maasserver/websockets/handlers/machine.py | |||
401 | +++ b/src/maasserver/websockets/handlers/machine.py | |||
402 | @@ -178,6 +178,7 @@ class MachineHandler(NodeHandler): | |||
403 | 178 | "managing_process", | 178 | "managing_process", |
404 | 179 | "last_image_sync", | 179 | "last_image_sync", |
405 | 180 | "install_rackd", | 180 | "install_rackd", |
406 | 181 | "install_kvm", | ||
407 | 181 | ] | 182 | ] |
408 | 182 | list_fields = [ | 183 | list_fields = [ |
409 | 183 | "id", | 184 | "id", |
410 | diff --git a/src/metadataserver/api.py b/src/metadataserver/api.py | |||
411 | index b786954..e76a651 100644 | |||
412 | --- a/src/metadataserver/api.py | |||
413 | +++ b/src/metadataserver/api.py | |||
414 | @@ -870,7 +870,20 @@ class UserDataHandler(MetadataViewHandler): | |||
415 | 870 | # for user-data is when MAAS hands the node | 870 | # for user-data is when MAAS hands the node |
416 | 871 | # off to a user. | 871 | # off to a user. |
417 | 872 | if node.status == NODE_STATUS.DEPLOYING: | 872 | if node.status == NODE_STATUS.DEPLOYING: |
419 | 873 | node.end_deployment() | 873 | if node.install_kvm: |
420 | 874 | # Rather than ending deployment here, note that we're | ||
421 | 875 | # installing a KVM pod. | ||
422 | 876 | node.agent_name = "maas-kvm-pod" | ||
423 | 877 | node.save() | ||
424 | 878 | else: | ||
425 | 879 | # MAAS currently considers a machine "Deployed" when the | ||
426 | 880 | # cloud-init user data is requested. Note that this doesn't | ||
427 | 881 | # mean the machine is ready for use yet; cloud-init will | ||
428 | 882 | # also send a 'finish' event for the 'modules-final' | ||
429 | 883 | # activity name. However, that check is ambiguous because | ||
430 | 884 | # it occurs both when curtin is installing, and when | ||
431 | 885 | # the machine reboots to finish its deployment. | ||
432 | 886 | node.end_deployment() | ||
433 | 874 | # If this node is supposed to be powered off, serve the | 887 | # If this node is supposed to be powered off, serve the |
434 | 875 | # 'poweroff' userdata. | 888 | # 'poweroff' userdata. |
435 | 876 | if node.get_boot_purpose() == 'poweroff': | 889 | if node.get_boot_purpose() == 'poweroff': |
436 | diff --git a/src/metadataserver/api_twisted.py b/src/metadataserver/api_twisted.py | |||
437 | index 08a26d3..0659d30 100644 | |||
438 | --- a/src/metadataserver/api_twisted.py | |||
439 | +++ b/src/metadataserver/api_twisted.py | |||
440 | @@ -14,7 +14,11 @@ from maasserver.enum import ( | |||
441 | 14 | NODE_STATUS, | 14 | NODE_STATUS, |
442 | 15 | NODE_TYPE, | 15 | NODE_TYPE, |
443 | 16 | ) | 16 | ) |
445 | 17 | from maasserver.models.node import Node | 17 | from maasserver.forms.pods import PodForm |
446 | 18 | from maasserver.models import ( | ||
447 | 19 | Node, | ||
448 | 20 | NodeMetadata, | ||
449 | 21 | ) | ||
450 | 18 | from maasserver.preseed import CURTIN_INSTALL_LOG | 22 | from maasserver.preseed import CURTIN_INSTALL_LOG |
451 | 19 | from maasserver.utils.orm import ( | 23 | from maasserver.utils.orm import ( |
452 | 20 | in_transaction, | 24 | in_transaction, |
453 | @@ -175,6 +179,47 @@ class StatusHandlerResource(Resource): | |||
454 | 175 | return NOT_DONE_YET | 179 | return NOT_DONE_YET |
455 | 176 | 180 | ||
456 | 177 | 181 | ||
457 | 182 | POD_CREATION_ERROR = ( | ||
458 | 183 | "Internal error while creating KVM pod. (See regiond.log for details.)" | ||
459 | 184 | ) | ||
460 | 185 | |||
461 | 186 | |||
462 | 187 | def _create_pod_for_deployment(node): | ||
463 | 188 | virsh_password_meta = NodeMetadata.objects.filter( | ||
464 | 189 | node=node, key="virsh_password").first() | ||
465 | 190 | if virsh_password_meta is None: | ||
466 | 191 | node.mark_failed( | ||
467 | 192 | comment="Failed to deploy KVM: Password not found.", commit=False) | ||
468 | 193 | else: | ||
469 | 194 | virsh_password = virsh_password_meta.value | ||
470 | 195 | virsh_password_meta.delete() | ||
471 | 196 | # XXX: Should find the best IP to communicate with, given what the rack | ||
472 | 197 | # controller can access, or use the boot interface IP address. | ||
473 | 198 | ip = node.ip_addresses()[0] | ||
474 | 199 | if ':' in ip: | ||
475 | 200 | ip = "[%s]" % ip | ||
476 | 201 | power_address = "qemu+ssh://virsh@%s/system" % ip | ||
477 | 202 | pod_form = PodForm(data=dict( | ||
478 | 203 | type="virsh", | ||
479 | 204 | name=node.hostname, | ||
480 | 205 | power_address=power_address, | ||
481 | 206 | power_pass=virsh_password, | ||
482 | 207 | zone=node.zone.name, | ||
483 | 208 | pool=node.pool.name, | ||
484 | 209 | ), user=node.owner) | ||
485 | 210 | if pod_form.is_valid(): | ||
486 | 211 | try: | ||
487 | 212 | pod_form.save() | ||
488 | 213 | except Exception: | ||
489 | 214 | node.mark_failed(comment=POD_CREATION_ERROR, commit=False) | ||
490 | 215 | log.err(None, "Exception while saving pod form.") | ||
491 | 216 | else: | ||
492 | 217 | node.status = NODE_STATUS.DEPLOYED | ||
493 | 218 | else: | ||
494 | 219 | node.mark_failed(comment=POD_CREATION_ERROR, commit=False) | ||
495 | 220 | log.msg("Error while creating KVM pod: %s" % dict(pod_form.errors)) | ||
496 | 221 | |||
497 | 222 | |||
498 | 178 | class StatusWorkerService(TimerService, object): | 223 | class StatusWorkerService(TimerService, object): |
499 | 179 | """Service to update nodes from recieved status messages.""" | 224 | """Service to update nodes from recieved status messages.""" |
500 | 180 | 225 | ||
501 | @@ -257,7 +302,8 @@ class StatusWorkerService(TimerService, object): | |||
502 | 257 | # LP:1701352 - If no exit code is given by the client default to | 302 | # LP:1701352 - If no exit code is given by the client default to |
503 | 258 | # 0(pass) unless the signal is fail then set to 1(failure). This allows | 303 | # 0(pass) unless the signal is fail then set to 1(failure). This allows |
504 | 259 | # a Curtin failure to cause the ScriptResult to fail. | 304 | # a Curtin failure to cause the ScriptResult to fail. |
506 | 260 | default_exit_status = 1 if result in ['FAIL', 'FAILURE'] else 0 | 305 | failed = result in ['FAIL', 'FAILURE'] |
507 | 306 | default_exit_status = 1 if failed else 0 | ||
508 | 261 | 307 | ||
509 | 262 | # Add this event to the node event log. | 308 | # Add this event to the node event log. |
510 | 263 | add_event_to_node_event_log( | 309 | add_event_to_node_event_log( |
511 | @@ -310,7 +356,7 @@ class StatusWorkerService(TimerService, object): | |||
512 | 310 | # cloud-init may send a failure message if a script reboots | 356 | # cloud-init may send a failure message if a script reboots |
513 | 311 | # the system. If a script is running which may_reboot ignore | 357 | # the system. If a script is running which may_reboot ignore |
514 | 312 | # the signal. | 358 | # the signal. |
516 | 313 | if result in ['FAIL', 'FAILURE']: | 359 | if failed: |
517 | 314 | script_set = node.current_commissioning_script_set | 360 | script_set = node.current_commissioning_script_set |
518 | 315 | if (script_set is None or not | 361 | if (script_set is None or not |
519 | 316 | script_set.scriptresult_set.filter( | 362 | script_set.scriptresult_set.filter( |
520 | @@ -323,18 +369,28 @@ class StatusWorkerService(TimerService, object): | |||
521 | 323 | script_result_status=SCRIPT_STATUS.ABORTED) | 369 | script_result_status=SCRIPT_STATUS.ABORTED) |
522 | 324 | save_node = True | 370 | save_node = True |
523 | 325 | elif node.status == NODE_STATUS.DEPLOYING: | 371 | elif node.status == NODE_STATUS.DEPLOYING: |
525 | 326 | if result in ['FAIL', 'FAILURE']: | 372 | # XXX: when activity_name == moudles-config, this currently |
526 | 373 | # /always/ fails, since MAAS passes two different versions | ||
527 | 374 | # for the apt configuration. The only reason why we don't | ||
528 | 375 | # see additional issues because of this is due to the node | ||
529 | 376 | # already being marked "Deployed". Right now this is prevented | ||
530 | 377 | # only in the install_kvm case, but we should make this check | ||
531 | 378 | # more general when time allows. | ||
532 | 379 | if failed and not node.install_kvm: | ||
533 | 327 | node.mark_failed( | 380 | node.mark_failed( |
534 | 328 | comment="Installation failed (refer to the " | 381 | comment="Installation failed (refer to the " |
535 | 329 | "installation log for more information).", | 382 | "installation log for more information).", |
536 | 330 | commit=False) | 383 | commit=False) |
537 | 331 | save_node = True | 384 | save_node = True |
538 | 385 | elif (not failed and activity_name == "modules-final" and | ||
539 | 386 | node.install_kvm and node.agent_name == "maas-kvm-pod"): | ||
540 | 387 | save_node = True | ||
541 | 388 | _create_pod_for_deployment(node) | ||
542 | 332 | elif node.status == NODE_STATUS.DISK_ERASING: | 389 | elif node.status == NODE_STATUS.DISK_ERASING: |
544 | 333 | if result in ['FAIL', 'FAILURE']: | 390 | if failed: |
545 | 334 | node.mark_failed( | 391 | node.mark_failed( |
546 | 335 | comment="Failed to erase disks.", commit=False) | 392 | comment="Failed to erase disks.", commit=False) |
547 | 336 | save_node = True | 393 | save_node = True |
548 | 337 | |||
549 | 338 | # Deallocate the node if we enter any terminal state. | 394 | # Deallocate the node if we enter any terminal state. |
550 | 339 | if node.node_type == NODE_TYPE.MACHINE and node.status in [ | 395 | if node.node_type == NODE_TYPE.MACHINE and node.status in [ |
551 | 340 | NODE_STATUS.READY, | 396 | NODE_STATUS.READY, |
552 | diff --git a/src/metadataserver/tests/test_api.py b/src/metadataserver/tests/test_api.py | |||
553 | index 998df04..2e7b6cb 100644 | |||
554 | --- a/src/metadataserver/tests/test_api.py | |||
555 | +++ b/src/metadataserver/tests/test_api.py | |||
556 | @@ -948,6 +948,17 @@ class TestMetadataUserDataStateChanges(MAASServerTestCase): | |||
557 | 948 | self.assertEqual(http.client.OK, response.status_code) | 948 | self.assertEqual(http.client.OK, response.status_code) |
558 | 949 | self.assertEqual(NODE_STATUS.DEPLOYED, reload_object(node).status) | 949 | self.assertEqual(NODE_STATUS.DEPLOYED, reload_object(node).status) |
559 | 950 | 950 | ||
560 | 951 | def test_skips_status_change_if_installing_kvm_and_sets_agent_name(self): | ||
561 | 952 | node = factory.make_Node( | ||
562 | 953 | status=NODE_STATUS.DEPLOYING, install_kvm=True) | ||
563 | 954 | NodeUserData.objects.set_user_data(node, sample_binary_data) | ||
564 | 955 | client = make_node_client(node) | ||
565 | 956 | response = client.get(reverse('metadata-user-data', args=['latest'])) | ||
566 | 957 | self.assertEqual(http.client.OK, response.status_code) | ||
567 | 958 | self.assertEqual(NODE_STATUS.DEPLOYING, reload_object(node).status) | ||
568 | 959 | node = reload_object(node) | ||
569 | 960 | self.assertEqual(node.agent_name, "maas-kvm-pod") | ||
570 | 961 | |||
571 | 951 | 962 | ||
572 | 952 | class TestCurtinMetadataUserData( | 963 | class TestCurtinMetadataUserData( |
573 | 953 | PreseedRPCMixin, MAASTransactionServerTestCase): | 964 | PreseedRPCMixin, MAASTransactionServerTestCase): |
574 | diff --git a/src/metadataserver/tests/test_api_twisted.py b/src/metadataserver/tests/test_api_twisted.py | |||
575 | index e8c7e34..65e5014 100644 | |||
576 | --- a/src/metadataserver/tests/test_api_twisted.py | |||
577 | +++ b/src/metadataserver/tests/test_api_twisted.py | |||
578 | @@ -24,6 +24,7 @@ from crochet import wait_for | |||
579 | 24 | from maasserver.enum import NODE_STATUS | 24 | from maasserver.enum import NODE_STATUS |
580 | 25 | from maasserver.models import ( | 25 | from maasserver.models import ( |
581 | 26 | Event, | 26 | Event, |
582 | 27 | NodeMetadata, | ||
583 | 27 | Tag, | 28 | Tag, |
584 | 28 | ) | 29 | ) |
585 | 29 | from maasserver.models.signals.testing import SignalsDisabled | 30 | from maasserver.models.signals.testing import SignalsDisabled |
586 | @@ -42,13 +43,19 @@ from maasserver.utils.orm import ( | |||
587 | 42 | ) | 43 | ) |
588 | 43 | from maasserver.utils.threads import deferToDatabase | 44 | from maasserver.utils.threads import deferToDatabase |
589 | 44 | from maastesting.matchers import ( | 45 | from maastesting.matchers import ( |
590 | 46 | DocTestMatches, | ||
591 | 45 | MockCalledOnceWith, | 47 | MockCalledOnceWith, |
592 | 46 | MockCallsMatch, | 48 | MockCallsMatch, |
593 | 47 | MockNotCalled, | 49 | MockNotCalled, |
594 | 48 | ) | 50 | ) |
595 | 49 | from maastesting.testcase import MAASTestCase | 51 | from maastesting.testcase import MAASTestCase |
597 | 50 | from metadataserver import api | 52 | from metadataserver import ( |
598 | 53 | api, | ||
599 | 54 | api_twisted as api_twisted_module, | ||
600 | 55 | ) | ||
601 | 51 | from metadataserver.api_twisted import ( | 56 | from metadataserver.api_twisted import ( |
602 | 57 | _create_pod_for_deployment, | ||
603 | 58 | POD_CREATION_ERROR, | ||
604 | 52 | StatusHandlerResource, | 59 | StatusHandlerResource, |
605 | 53 | StatusWorkerService, | 60 | StatusWorkerService, |
606 | 54 | ) | 61 | ) |
607 | @@ -60,6 +67,7 @@ from metadataserver.models import NodeKey | |||
608 | 60 | from testtools import ExpectedException | 67 | from testtools import ExpectedException |
609 | 61 | from testtools.matchers import ( | 68 | from testtools.matchers import ( |
610 | 62 | Equals, | 69 | Equals, |
611 | 70 | Is, | ||
612 | 63 | MatchesListwise, | 71 | MatchesListwise, |
613 | 64 | MatchesSetwise, | 72 | MatchesSetwise, |
614 | 65 | ) | 73 | ) |
615 | @@ -343,7 +351,7 @@ def encode_as_base64(content): | |||
616 | 343 | class TestStatusWorkerService(MAASServerTestCase): | 351 | class TestStatusWorkerService(MAASServerTestCase): |
617 | 344 | 352 | ||
618 | 345 | def setUp(self): | 353 | def setUp(self): |
620 | 346 | super(TestStatusWorkerService, self).setUp() | 354 | super().setUp() |
621 | 347 | self.useFixture(SignalsDisabled("power")) | 355 | self.useFixture(SignalsDisabled("power")) |
622 | 348 | 356 | ||
623 | 349 | def processMessage(self, node, payload): | 357 | def processMessage(self, node, payload): |
624 | @@ -505,6 +513,23 @@ class TestStatusWorkerService(MAASServerTestCase): | |||
625 | 505 | " log for more information).", | 513 | " log for more information).", |
626 | 506 | Event.objects.filter(node=node).last().description) | 514 | Event.objects.filter(node=node).last().description) |
627 | 507 | 515 | ||
628 | 516 | def test_status_ok_for_modules_final_triggers_kvm_install(self): | ||
629 | 517 | node = factory.make_Node( | ||
630 | 518 | interface=True, status=NODE_STATUS.DEPLOYING, | ||
631 | 519 | agent_name="maas-kvm-pod", install_kvm=True) | ||
632 | 520 | payload = { | ||
633 | 521 | 'event_type': 'finish', | ||
634 | 522 | 'result': 'OK', | ||
635 | 523 | 'origin': 'cloud-init', | ||
636 | 524 | 'name': 'modules-final', | ||
637 | 525 | 'description': 'America for Make Benefit Glorious Nation', | ||
638 | 526 | 'timestamp': datetime.utcnow(), | ||
639 | 527 | } | ||
640 | 528 | mock_create_pod = self.patch( | ||
641 | 529 | api_twisted_module, '_create_pod_for_deployment') | ||
642 | 530 | self.processMessage(node, payload) | ||
643 | 531 | self.assertThat(mock_create_pod, MockCalledOnceWith(node)) | ||
644 | 532 | |||
645 | 508 | def test_status_installation_fail_leaves_node_failed(self): | 533 | def test_status_installation_fail_leaves_node_failed(self): |
646 | 509 | node = factory.make_Node(interface=True, status=NODE_STATUS.DEPLOYING) | 534 | node = factory.make_Node(interface=True, status=NODE_STATUS.DEPLOYING) |
647 | 510 | payload = { | 535 | payload = { |
648 | @@ -987,3 +1012,71 @@ class TestStatusWorkerService(MAASServerTestCase): | |||
649 | 987 | node.status_expires, expected_time - timedelta(minutes=1)) | 1012 | node.status_expires, expected_time - timedelta(minutes=1)) |
650 | 988 | self.assertLessEqual( | 1013 | self.assertLessEqual( |
651 | 989 | node.status_expires, expected_time + timedelta(minutes=1)) | 1014 | node.status_expires, expected_time + timedelta(minutes=1)) |
652 | 1015 | |||
653 | 1016 | |||
654 | 1017 | class TestCreatePodForDeployment(MAASServerTestCase): | ||
655 | 1018 | |||
656 | 1019 | def setUp(self): | ||
657 | 1020 | super().setUp() | ||
658 | 1021 | self.mock_PodForm = self.patch(api_twisted_module, "PodForm") | ||
659 | 1022 | |||
660 | 1023 | def test__marks_failed_if_no_virsh_password(self): | ||
661 | 1024 | node = factory.make_Node( | ||
662 | 1025 | interface=True, status=NODE_STATUS.DEPLOYING, | ||
663 | 1026 | agent_name="maas-kvm-pod", install_kvm=True) | ||
664 | 1027 | _create_pod_for_deployment(node) | ||
665 | 1028 | self.assertThat(node.status, Equals(NODE_STATUS.FAILED_DEPLOYMENT)) | ||
666 | 1029 | self.assertThat(node.error_description, DocTestMatches( | ||
667 | 1030 | "...Password not found...")) | ||
668 | 1031 | |||
669 | 1032 | def test__deletes_virsh_password_metadata_and_sets_deployed(self): | ||
670 | 1033 | node = factory.make_Node_with_Interface_on_Subnet( | ||
671 | 1034 | status=NODE_STATUS.DEPLOYING, agent_name="maas-kvm-pod", | ||
672 | 1035 | install_kvm=True) | ||
673 | 1036 | factory.make_StaticIPAddress(interface=node.boot_interface) | ||
674 | 1037 | meta = NodeMetadata.objects.create( | ||
675 | 1038 | node=node, key="virsh_password", value="xyz123") | ||
676 | 1039 | _create_pod_for_deployment(node) | ||
677 | 1040 | meta = reload_object(meta) | ||
678 | 1041 | self.assertThat(meta, Is(None)) | ||
679 | 1042 | self.assertThat(node.status, Equals(NODE_STATUS.DEPLOYED)) | ||
680 | 1043 | |||
681 | 1044 | def test__marks_failed_if_is_valid_returns_false(self): | ||
682 | 1045 | mock_pod_form = Mock() | ||
683 | 1046 | self.mock_PodForm.return_value = mock_pod_form | ||
684 | 1047 | mock_pod_form.errors = {} | ||
685 | 1048 | mock_pod_form.is_valid = Mock() | ||
686 | 1049 | mock_pod_form.is_valid.return_value = False | ||
687 | 1050 | node = factory.make_Node_with_Interface_on_Subnet( | ||
688 | 1051 | status=NODE_STATUS.DEPLOYING, agent_name="maas-kvm-pod", | ||
689 | 1052 | install_kvm=True) | ||
690 | 1053 | factory.make_StaticIPAddress(interface=node.boot_interface) | ||
691 | 1054 | meta = NodeMetadata.objects.create( | ||
692 | 1055 | node=node, key="virsh_password", value="xyz123") | ||
693 | 1056 | _create_pod_for_deployment(node) | ||
694 | 1057 | meta = reload_object(meta) | ||
695 | 1058 | self.assertThat(meta, Is(None)) | ||
696 | 1059 | self.assertThat(node.status, Equals(NODE_STATUS.FAILED_DEPLOYMENT)) | ||
697 | 1060 | self.assertThat(node.error_description, DocTestMatches( | ||
698 | 1061 | POD_CREATION_ERROR)) | ||
699 | 1062 | |||
700 | 1063 | def test__marks_failed_if_save_raises(self): | ||
701 | 1064 | mock_pod_form = Mock() | ||
702 | 1065 | self.mock_PodForm.return_value = mock_pod_form | ||
703 | 1066 | mock_pod_form.errors = {} | ||
704 | 1067 | mock_pod_form.is_valid = Mock() | ||
705 | 1068 | mock_pod_form.is_valid.return_value = True | ||
706 | 1069 | mock_pod_form.save = Mock() | ||
707 | 1070 | mock_pod_form.save.side_effect = ValueError | ||
708 | 1071 | node = factory.make_Node_with_Interface_on_Subnet( | ||
709 | 1072 | status=NODE_STATUS.DEPLOYING, agent_name="maas-kvm-pod", | ||
710 | 1073 | install_kvm=True) | ||
711 | 1074 | factory.make_StaticIPAddress(interface=node.boot_interface) | ||
712 | 1075 | meta = NodeMetadata.objects.create( | ||
713 | 1076 | node=node, key="virsh_password", value="xyz123") | ||
714 | 1077 | _create_pod_for_deployment(node) | ||
715 | 1078 | meta = reload_object(meta) | ||
716 | 1079 | self.assertThat(meta, Is(None)) | ||
717 | 1080 | self.assertThat(node.status, Equals(NODE_STATUS.FAILED_DEPLOYMENT)) | ||
718 | 1081 | self.assertThat(node.error_description, DocTestMatches( | ||
719 | 1082 | POD_CREATION_ERROR)) | ||
720 | diff --git a/src/metadataserver/tests/test_vendor_data.py b/src/metadataserver/tests/test_vendor_data.py | |||
721 | index bd0475f..3b1a6d3 100644 | |||
722 | --- a/src/metadataserver/tests/test_vendor_data.py | |||
723 | +++ b/src/metadataserver/tests/test_vendor_data.py | |||
724 | @@ -5,7 +5,10 @@ | |||
725 | 5 | 5 | ||
726 | 6 | __all__ = [] | 6 | __all__ = [] |
727 | 7 | 7 | ||
729 | 8 | from maasserver.models.config import Config | 8 | from maasserver.models import ( |
730 | 9 | Config, | ||
731 | 10 | NodeMetadata, | ||
732 | 11 | ) | ||
733 | 9 | from maasserver.server_address import get_maas_facing_server_host | 12 | from maasserver.server_address import get_maas_facing_server_host |
734 | 10 | from maasserver.testing.factory import factory | 13 | from maasserver.testing.factory import factory |
735 | 11 | from maasserver.testing.testcase import MAASServerTestCase | 14 | from maasserver.testing.testcase import MAASServerTestCase |
736 | @@ -21,6 +24,7 @@ from testtools.matchers import ( | |||
737 | 21 | Contains, | 24 | Contains, |
738 | 22 | ContainsDict, | 25 | ContainsDict, |
739 | 23 | Equals, | 26 | Equals, |
740 | 27 | HasLength, | ||
741 | 24 | Is, | 28 | Is, |
742 | 25 | IsInstance, | 29 | IsInstance, |
743 | 26 | KeysEqual, | 30 | KeysEqual, |
744 | @@ -221,3 +225,20 @@ class TestGenerateRackControllerConfiguration(MAASServerTestCase): | |||
745 | 221 | "%s --maas-url %s --secret %s" % (cmd, maas_url, secret), | 225 | "%s --maas-url %s --secret %s" % (cmd, maas_url, secret), |
746 | 222 | ] | 226 | ] |
747 | 223 | })) | 227 | })) |
748 | 228 | |||
749 | 229 | def test_yields_configuration_when_machine_install_kvm_true(self): | ||
750 | 230 | node = factory.make_Node(osystem='ubuntu', netboot=False) | ||
751 | 231 | node.install_kvm = True | ||
752 | 232 | configuration = get_vendor_data(node) | ||
753 | 233 | config = str(dict(configuration)) | ||
754 | 234 | self.assertThat(config, Contains("virsh")) | ||
755 | 235 | self.assertThat(config, Contains("ssh_pwauth")) | ||
756 | 236 | self.assertThat(config, Contains("rbash")) | ||
757 | 237 | self.assertThat(config, Contains("libvirt-qemu")) | ||
758 | 238 | self.assertThat(config, Contains("ForceCommand")) | ||
759 | 239 | self.assertThat(config, Contains("qemu-kvm")) | ||
760 | 240 | self.assertThat(config, Contains("libvirt-bin")) | ||
761 | 241 | # Check that a password was saved for the pod-to-be. | ||
762 | 242 | virsh_password_meta = NodeMetadata.objects.filter( | ||
763 | 243 | node=node, key="virsh_password").first() | ||
764 | 244 | self.assertThat(virsh_password_meta.value, HasLength(32)) | ||
765 | diff --git a/src/metadataserver/vendor_data.py b/src/metadataserver/vendor_data.py | |||
766 | index a0c689d..b6bbf72 100644 | |||
767 | --- a/src/metadataserver/vendor_data.py | |||
768 | +++ b/src/metadataserver/vendor_data.py | |||
769 | @@ -7,10 +7,16 @@ __all__ = [ | |||
770 | 7 | 'get_vendor_data', | 7 | 'get_vendor_data', |
771 | 8 | ] | 8 | ] |
772 | 9 | 9 | ||
773 | 10 | from base64 import b64encode | ||
774 | 11 | from crypt import crypt | ||
775 | 10 | from itertools import chain | 12 | from itertools import chain |
776 | 13 | from os import urandom | ||
777 | 11 | 14 | ||
778 | 12 | from maasserver import ntp | 15 | from maasserver import ntp |
780 | 13 | from maasserver.models import Config | 16 | from maasserver.models import ( |
781 | 17 | Config, | ||
782 | 18 | NodeMetadata, | ||
783 | 19 | ) | ||
784 | 14 | from maasserver.server_address import get_maas_facing_server_host | 20 | from maasserver.server_address import get_maas_facing_server_host |
785 | 15 | from netaddr import IPAddress | 21 | from netaddr import IPAddress |
786 | 16 | from provisioningserver.ntp.config import normalise_address | 22 | from provisioningserver.ntp.config import normalise_address |
787 | @@ -23,6 +29,7 @@ def get_vendor_data(node): | |||
788 | 23 | generate_system_info(node), | 29 | generate_system_info(node), |
789 | 24 | generate_ntp_configuration(node), | 30 | generate_ntp_configuration(node), |
790 | 25 | generate_rack_controller_configuration(node), | 31 | generate_rack_controller_configuration(node), |
791 | 32 | generate_kvm_pod_configuration(node), | ||
792 | 26 | )) | 33 | )) |
793 | 27 | 34 | ||
794 | 28 | 35 | ||
795 | @@ -91,3 +98,56 @@ def generate_rack_controller_configuration(node): | |||
796 | 91 | "/snap/bin/maas init --mode rack --maas-url %s --secret %s" % ( | 98 | "/snap/bin/maas init --mode rack --maas-url %s --secret %s" % ( |
797 | 92 | maas_url, secret) | 99 | maas_url, secret) |
798 | 93 | ] | 100 | ] |
799 | 101 | |||
800 | 102 | |||
801 | 103 | def generate_kvm_pod_configuration(node): | ||
802 | 104 | """Generate cloud-init configuration to install the node as a KVM pod.""" | ||
803 | 105 | if node.netboot is False and node.install_kvm is True: | ||
804 | 106 | yield "runcmd", [ | ||
805 | 107 | # Restrict the $PATH so that rbash can be used to limit what the | ||
806 | 108 | # virsh user can do if they manage to get a shell. | ||
807 | 109 | ['mkdir', '-p', '/home/virsh/bin'], | ||
808 | 110 | ['ln', '-s', '/usr/bin/virsh', '/home/virsh/bin/virsh'], | ||
809 | 111 | ['sh', '-c', 'echo "PATH=/home/virsh/bin" >> /home/virsh/.bashrc'], | ||
810 | 112 | # Use a ForceCommand to make sure the only thing the virsh user | ||
811 | 113 | # can do with SSH is communicate with libvirt. | ||
812 | 114 | ['sh', '-c', | ||
813 | 115 | 'printf "Match user virsh\\n' | ||
814 | 116 | ' X11Forwarding no\\n' | ||
815 | 117 | ' AllowTcpForwarding no\\n' | ||
816 | 118 | ' PermitTTY no\\n' | ||
817 | 119 | ' ForceCommand nc -q 0 -U /var/run/libvirt/libvirt-sock\\n"' | ||
818 | 120 | ' >> /etc/ssh/sshd_config'], | ||
819 | 121 | # Make sure the 'virsh' user is allowed to access libvirt. | ||
820 | 122 | ['/usr/sbin/usermod', '--append', '--groups', | ||
821 | 123 | 'libvirt,libvirt-qemu', 'virsh'], | ||
822 | 124 | # SSH needs to be restarted in order for the above changes to | ||
823 | 125 | # take effect. | ||
824 | 126 | ['systemctl', 'restart', 'sshd'], | ||
825 | 127 | # Ensure services are ready before cloud-init finishes. | ||
826 | 128 | ['/bin/sleep', '10'], | ||
827 | 129 | ] | ||
828 | 130 | # Generate a 32-character password by encoding 24 bytes as base64. | ||
829 | 131 | virsh_password = b64encode( | ||
830 | 132 | urandom(24), altchars=b'.!').decode('ascii') | ||
831 | 133 | # Pass crypted (salted/hashed) version of the password to cloud-init. | ||
832 | 134 | encrypted_password = crypt(virsh_password) | ||
833 | 135 | # Store a cleartext version of the password so we can add a pod later. | ||
834 | 136 | NodeMetadata.objects.update_or_create( | ||
835 | 137 | node=node, key="virsh_password", | ||
836 | 138 | defaults=dict(value=virsh_password)) | ||
837 | 139 | # Make sure SSH password authentication is enabled. | ||
838 | 140 | yield "ssh_pwauth", True | ||
839 | 141 | # Create a custom 'virsh' user (in addition to the default user) | ||
840 | 142 | # with the encrypted password, and a locked-down shell. | ||
841 | 143 | yield "users", [ | ||
842 | 144 | 'default', | ||
843 | 145 | { | ||
844 | 146 | 'name': 'virsh', | ||
845 | 147 | 'lock_passwd': False, | ||
846 | 148 | 'passwd': encrypted_password, | ||
847 | 149 | 'shell': '/bin/rbash', | ||
848 | 150 | } | ||
849 | 151 | ] | ||
850 | 152 | # XXX: Use correct packages based on architecture. | ||
851 | 153 | yield "packages", ["qemu-kvm", "libvirt-bin"] |
Does agent_name get cleared out on release?