Merge lp:~mpontillo/maas/fix-deploy-auto-allocate--bug-1672363 into lp:~maas-committers/maas/trunk

Proposed by Mike Pontillo
Status: Merged
Approved by: Mike Pontillo
Approved revision: no longer in the source branch.
Merged at revision: 6014
Proposed branch: lp:~mpontillo/maas/fix-deploy-auto-allocate--bug-1672363
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 232 lines (+150/-15)
3 files modified
src/maasserver/api/machines.py (+52/-14)
src/maasserver/api/tests/test_machine.py (+97/-0)
src/maasserver/models/node.py (+1/-1)
To merge this branch: bzr merge lp:~mpontillo/maas/fix-deploy-auto-allocate--bug-1672363
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+322904@code.launchpad.net

Commit message

Allow an individual machine to be allocated and deployed from the API in one step.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looks good.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/machines.py'
2--- src/maasserver/api/machines.py 2017-04-18 00:33:14 +0000
3+++ src/maasserver/api/machines.py 2017-04-21 20:21:20 +0000
4@@ -185,6 +185,21 @@
5 return storage_layout, params
6
7
8+def get_allocation_parameters(request):
9+ """Returns shared parameters for deploy and allocate operations."""
10+ comment = get_optional_param(request.POST, 'comment')
11+ bridge_all = get_optional_param(
12+ request.POST, 'bridge_all', default=False,
13+ validator=StringBool)
14+ bridge_stp = get_optional_param(
15+ request.POST, 'bridge_stp', default=False,
16+ validator=StringBool)
17+ bridge_fd = get_optional_param(
18+ request.POST, 'bridge_fd', default=0, validator=Int)
19+ agent_name = request.data.get('agent_name', '')
20+ return agent_name, bridge_all, bridge_fd, bridge_stp, comment
21+
22+
23 class MachineHandler(NodeHandler, OwnerDataMixin, PowerMixin):
24 """Manage an individual Machine.
25
26@@ -360,6 +375,21 @@
27 :param hwe_kernel: If present, this parameter specified the kernel to
28 be used on the machine
29 :type hwe_kernel: unicode
30+ :param agent_name: An optional agent name to attach to the
31+ acquired machine.
32+ :type agent_name: unicode
33+ :param bridge_all: Optionally create a bridge interface for every
34+ configured interface on the machine. The created bridges will be
35+ removed once the machine is released.
36+ (Default: False)
37+ :type bridge_all: boolean
38+ :param bridge_stp: Optionally turn spanning tree protocol on or off
39+ for the bridges created on every configured interface.
40+ (Default: off)
41+ :type bridge_stp: boolean
42+ :param bridge_fd: Optionally adjust the forward delay to time seconds.
43+ (Default: 15)
44+ :type bridge_fd: integer
45 :param comment: Optional comment for the event log.
46 :type comment: unicode
47
48@@ -377,14 +407,28 @@
49 series = request.POST.get('distro_series', None)
50 license_key = request.POST.get('license_key', None)
51 hwe_kernel = request.POST.get('hwe_kernel', None)
52-
53+ # Acquiring a node requires VIEW permissions.
54 machine = self.model.objects.get_node_or_404(
55 system_id=system_id, user=request.user,
56- perm=NODE_PERMISSION.EDIT)
57-
58- if machine.owner is None:
59- raise NodeStateViolation(
60- "Can't start machine: it hasn't been allocated.")
61+ perm=NODE_PERMISSION.VIEW)
62+ if machine.status == NODE_STATUS.READY:
63+ with locks.node_acquire:
64+ if machine.owner is not None and machine.owner != request.user:
65+ raise NodeStateViolation(
66+ "Can't allocate a machine belonging to another user.")
67+ agent_name, bridge_all, bridge_fd, bridge_stp, comment = (
68+ get_allocation_parameters(request))
69+ maaslog.info(
70+ "Request from user %s to acquire machine: %s (%s)",
71+ request.user.username, machine.fqdn, machine.system_id)
72+ machine.acquire(
73+ request.user, get_oauth_token(request),
74+ agent_name=agent_name, comment=comment,
75+ bridge_all=bridge_all, bridge_stp=bridge_stp,
76+ bridge_fd=bridge_fd)
77+ # Deploying a node requires re-checking for EDIT permissions.
78+ if not request.user.has_perm(NODE_PERMISSION.EDIT, machine):
79+ raise PermissionDenied()
80 if not machine.distro_series and not series:
81 series = Config.objects.get_config('default_distro_series')
82 Form = get_machine_edit_form(request.user)
83@@ -1355,18 +1399,13 @@
84 # XXX AndresRodriguez 2016-10-27: If new params are added and are not
85 # constraints, these need to be added to IGNORED_FIELDS in
86 # src/maasserver/node_constraint_filter_forms.py.
87- comment = get_optional_param(request.POST, 'comment')
88 input_constraints = str([
89 param for param in request.data.lists() if param[0] != 'op'])
90 maaslog.info(
91 "Request from user %s to acquire a machine with constraints: %s",
92 request.user.username, input_constraints)
93- bridge_all = get_optional_param(
94- request.POST, 'bridge_all', default=False, validator=StringBool)
95- bridge_stp = get_optional_param(
96- request.POST, 'bridge_stp', default=False, validator=StringBool)
97- bridge_fd = get_optional_param(
98- request.POST, 'bridge_fd', default=False, validator=Int)
99+ agent_name, bridge_all, bridge_fd, bridge_stp, comment = (
100+ get_allocation_parameters(request))
101 verbose = get_optional_param(
102 request.POST, 'verbose', default=False, validator=StringBool)
103 dry_run = get_optional_param(
104@@ -1434,7 +1473,6 @@
105 '(resolved to "%s")' % (
106 input_constraints, constraints))
107 raise NodesNotAvailable(message)
108- agent_name = request.data.get('agent_name', '')
109 if not dry_run:
110 machine.acquire(
111 request.user, get_oauth_token(request),
112
113=== modified file 'src/maasserver/api/tests/test_machine.py'
114--- src/maasserver/api/tests/test_machine.py 2017-04-17 19:44:23 +0000
115+++ src/maasserver/api/tests/test_machine.py 2017-04-21 20:21:20 +0000
116@@ -598,6 +598,103 @@
117 response_content = json_load_bytes(response.content)
118 self.assertEquals('virsh', response_content['power_type'])
119
120+ def test_POST_deploy_allocates_ready_machines(self):
121+ self.patch(node_module.Node, "_start")
122+ machine = factory.make_Node(
123+ status=NODE_STATUS.READY, interface=True,
124+ power_type='manual',
125+ architecture=make_usable_architecture(self))
126+ osystem = make_usable_osystem(self)
127+ distro_series = osystem['default_release']
128+ request = {
129+ 'op': 'deploy',
130+ 'distro_series': distro_series,
131+ }
132+ response = self.client.post(self.get_machine_uri(machine), request)
133+ self.assertEqual(http.client.OK, response.status_code)
134+
135+ def test_POST_deploy_rejects_node_owned_by_another_user(self):
136+ self.patch(node_module.Node, "_start")
137+ user2 = factory.make_User()
138+ machine = factory.make_Node(
139+ status=NODE_STATUS.READY, owner=user2, interface=True,
140+ power_type='manual',
141+ architecture=make_usable_architecture(self))
142+ osystem = make_usable_osystem(self)
143+ distro_series = osystem['default_release']
144+ request = {
145+ 'op': 'deploy',
146+ 'distro_series': distro_series,
147+ }
148+ response = self.client.post(self.get_machine_uri(machine), request)
149+ self.assertEqual(http.client.CONFLICT, response.status_code)
150+
151+ def test_POST_deploy_passes_agent_name(self):
152+ self.patch(node_module.Node, "_start")
153+ machine = factory.make_Node(
154+ status=NODE_STATUS.READY, interface=True,
155+ power_type='manual',
156+ architecture=make_usable_architecture(self))
157+ osystem = make_usable_osystem(self)
158+ distro_series = osystem['default_release']
159+ request = {
160+ 'op': 'deploy',
161+ 'distro_series': distro_series,
162+ 'agent_name': factory.make_name(),
163+ 'comment': factory.make_name(),
164+ }
165+ response = self.client.post(self.get_machine_uri(machine), request)
166+ self.assertEqual(http.client.OK, response.status_code)
167+ machine = reload_object(machine)
168+ self.assertThat(machine.agent_name, Equals(request['agent_name']))
169+
170+ def test_POST_deploy_passes_comment_on_acquire(self):
171+ self.patch(node_module.Node, "_start")
172+ machine_method = self.patch(node_module.Machine, 'acquire')
173+ machine = factory.make_Node(
174+ status=NODE_STATUS.READY, owner=self.user, interface=True,
175+ power_type='manual',
176+ architecture=make_usable_architecture(self))
177+ osystem = make_usable_osystem(self)
178+ distro_series = osystem['default_release']
179+ request = {
180+ 'op': 'deploy',
181+ 'distro_series': distro_series,
182+ 'agent_name': factory.make_name(),
183+ 'comment': factory.make_name(),
184+ }
185+ response = self.client.post(self.get_machine_uri(machine), request)
186+ self.assertEqual(http.client.OK, response.status_code)
187+ self.assertThat(
188+ machine_method, MockCalledOnceWith(
189+ ANY, ANY, agent_name=ANY,
190+ bridge_all=False, bridge_fd=False,
191+ bridge_stp=False, comment=request['comment']))
192+
193+ def test_POST_deploy_passes_bridge_settings(self):
194+ self.patch(node_module.Node, "_start")
195+ machine_method = self.patch(node_module.Machine, 'acquire')
196+ machine = factory.make_Node(
197+ status=NODE_STATUS.READY, owner=self.user, interface=True,
198+ power_type='manual',
199+ architecture=make_usable_architecture(self))
200+ osystem = make_usable_osystem(self)
201+ distro_series = osystem['default_release']
202+ request = {
203+ 'op': 'deploy',
204+ 'distro_series': distro_series,
205+ 'bridge_all': True,
206+ 'bridge_stp': True,
207+ 'bridge_fd': 7,
208+ }
209+ response = self.client.post(self.get_machine_uri(machine), request)
210+ self.assertEqual(http.client.OK, response.status_code)
211+ self.assertThat(
212+ machine_method, MockCalledOnceWith(
213+ ANY, ANY, agent_name=ANY,
214+ bridge_all=True, bridge_fd=7,
215+ bridge_stp=True, comment=None))
216+
217 def test_POST_release_releases_owned_machine(self):
218 self.patch(node_module.Machine, '_stop')
219 owned_statuses = [
220
221=== modified file 'src/maasserver/models/node.py'
222--- src/maasserver/models/node.py 2017-04-18 00:33:14 +0000
223+++ src/maasserver/models/node.py 2017-04-21 20:21:20 +0000
224@@ -2497,7 +2497,7 @@
225 self, user, token=None, agent_name='', comment=None,
226 bridge_all=False, bridge_stp=None, bridge_fd=None):
227 """Mark commissioned node as acquired by the given user and token."""
228- assert self.owner is None
229+ assert self.owner is None or self.owner == user
230 assert token is None or token.user == user
231
232 self._create_acquired_filesystems()