Merge lp:~ltrager/maas/clean_api into lp:~maas-committers/maas/trunk

Proposed by Lee Trager
Status: Merged
Approved by: Lee Trager
Approved revision: no longer in the source branch.
Merged at revision: 4714
Proposed branch: lp:~ltrager/maas/clean_api
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 5301 lines (+1199/-1153)
85 files modified
src/maasserver/api/account.py (+1/-1)
src/maasserver/api/auth.py (+1/-1)
src/maasserver/api/bcache.py (+15/-15)
src/maasserver/api/bcache_cacheset.py (+14/-14)
src/maasserver/api/blockdevices.py (+35/-35)
src/maasserver/api/boot_resources.py (+1/-1)
src/maasserver/api/boot_source_selections.py (+1/-1)
src/maasserver/api/boot_sources.py (+1/-1)
src/maasserver/api/commissioning_scripts.py (+3/-3)
src/maasserver/api/devices.py (+34/-90)
src/maasserver/api/dnsresourcerecords.py (+1/-1)
src/maasserver/api/dnsresources.py (+1/-1)
src/maasserver/api/doc.py (+1/-1)
src/maasserver/api/doc_handler.py (+2/-2)
src/maasserver/api/domains.py (+1/-1)
src/maasserver/api/events.py (+1/-1)
src/maasserver/api/fabrics.py (+1/-1)
src/maasserver/api/fannetworks.py (+1/-1)
src/maasserver/api/files.py (+1/-1)
src/maasserver/api/interfaces.py (+2/-2)
src/maasserver/api/ip_addresses.py (+1/-1)
src/maasserver/api/license_keys.py (+1/-1)
src/maasserver/api/logger.py (+1/-1)
src/maasserver/api/maas.py (+1/-1)
src/maasserver/api/machines.py (+91/-20)
src/maasserver/api/networks.py (+1/-1)
src/maasserver/api/nodes.py (+33/-8)
src/maasserver/api/not_found.py (+1/-1)
src/maasserver/api/pxeconfig.py (+1/-1)
src/maasserver/api/rackcontrollers.py (+9/-12)
src/maasserver/api/raid.py (+15/-15)
src/maasserver/api/spaces.py (+1/-1)
src/maasserver/api/ssh_keys.py (+1/-1)
src/maasserver/api/ssl_keys.py (+1/-1)
src/maasserver/api/subnets.py (+1/-1)
src/maasserver/api/support.py (+1/-1)
src/maasserver/api/tags.py (+1/-1)
src/maasserver/api/tests/test_devices.py (+51/-2)
src/maasserver/api/tests/test_doc.py (+11/-0)
src/maasserver/api/tests/test_enlistment.py (+34/-4)
src/maasserver/api/tests/test_machine.py (+2/-205)
src/maasserver/api/tests/test_machines.py (+78/-55)
src/maasserver/api/tests/test_node.py (+1/-161)
src/maasserver/api/tests/test_nodes.py (+1/-31)
src/maasserver/api/tests/test_rackcontroller.py (+26/-4)
src/maasserver/api/users.py (+1/-1)
src/maasserver/api/utils.py (+1/-1)
src/maasserver/api/version.py (+1/-1)
src/maasserver/api/volume_groups.py (+22/-20)
src/maasserver/api/zones.py (+1/-1)
src/maasserver/forms.py (+210/-144)
src/maasserver/models/node.py (+5/-4)
src/maasserver/models/tests/test_node.py (+7/-4)
src/maasserver/preseed.py (+1/-1)
src/maasserver/rpc/nodes.py (+10/-5)
src/maasserver/rpc/regionservice.py (+2/-2)
src/maasserver/rpc/tests/test_nodes.py (+49/-0)
src/maasserver/rpc/tests/test_regionservice.py (+3/-1)
src/maasserver/tests/test_forms_device.py (+5/-16)
src/maasserver/tests/test_forms_helpers.py (+19/-8)
src/maasserver/tests/test_forms_machine.py (+40/-155)
src/maasserver/tests/test_forms_machinewithmacaddresses.py (+14/-14)
src/maasserver/tests/test_forms_node.py (+204/-0)
src/maasserver/websockets/handlers/controller.py (+2/-2)
src/maasserver/websockets/handlers/machine.py (+3/-3)
src/maasserver/websockets/handlers/tests/test_controller.py (+3/-3)
src/maasserver/websockets/handlers/tests/test_machine.py (+3/-3)
src/maasserver/websockets/tests/test_base.py (+7/-7)
src/provisioningserver/drivers/hardware/msftocs.py (+3/-2)
src/provisioningserver/drivers/hardware/seamicro.py (+5/-3)
src/provisioningserver/drivers/hardware/tests/test_msftocs.py (+3/-2)
src/provisioningserver/drivers/hardware/tests/test_seamicro.py (+3/-2)
src/provisioningserver/drivers/hardware/tests/test_ucsm.py (+3/-2)
src/provisioningserver/drivers/hardware/tests/test_virsh.py (+7/-6)
src/provisioningserver/drivers/hardware/ucsm.py (+4/-2)
src/provisioningserver/drivers/hardware/virsh.py (+5/-3)
src/provisioningserver/drivers/hardware/vmware.py (+5/-5)
src/provisioningserver/drivers/power/mscm.py (+3/-2)
src/provisioningserver/drivers/power/tests/test_mscm.py (+3/-2)
src/provisioningserver/rpc/cluster.py (+1/-0)
src/provisioningserver/rpc/clusterservice.py (+11/-9)
src/provisioningserver/rpc/region.py (+1/-0)
src/provisioningserver/rpc/tests/test_clusterservice.py (+35/-7)
src/provisioningserver/rpc/tests/test_utils.py (+6/-3)
src/provisioningserver/rpc/utils.py (+5/-3)
To merge this branch: bzr merge lp:~ltrager/maas/clean_api
Reviewer Review Type Date Requested Status
Andres Rodriguez (community) Approve
Blake Rouse (community) Approve
Review via email: mp+287759@code.launchpad.net

Commit message

Clean up the API

Description of the change

Sorry for posting such a large review, much of this is just renames and moving some code around.

* Update the API docs to use machine consistently where only a machine node type can be used.
* Clean up duplicated tests. A number of the tests in TestNodeAPI were checking the output of a given machine. As the output code is handled by the Machine API this is already tested in TestMachineAPI.
* Limit the amount of items outputted for rack controllers, see http://paste.ubuntu.com/15265877/
* Base Device and RackController API off of Node API.
* I created an update method for node which now rack controller uses as well.
* To facilitate the new base update method I split NodeForm into NodeForm and MachineForm. NodeForm is a basic form which allows setting general options(hostname, domain, disable_ipv4, swap_size). DeviceForm and MachineForm inherit from NodeForm and expand on those options. This allows us to have a consistent set of options throughout all node types while removing duplicated code.
* The API has been updated so that all node types output only the hostname in the hostname field. The domain and fqdn fields have been added to the output as well.
* Domains can now be specified when creating devices, nodes, or adding chassis. The domain field can be changed with any end points update method.

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

This branch is so large I am not going to even look through it. I did look for specifics that I know needed to be fixed. I commented inline about "status" showing on rack controller, I don't think this should be the case. It will even be removed from the WebUI as well, so lets please remove status.

Also as discussed in the hangout remove "update" on node and on rack controllers. We can add those later if really needed and once that has been fully nailed down and supported.

You need to work on making the branches smaller, because this is impossible to review. You could have split this up into different pieces to make it easier on the reviewer. I am being nice today because we need this for alpha1, but if it was not for that I would probably give this a "Disapprove."

Here is a breakdown on how this could have been a few branches:

1. Update license headers.
2. Rename node to machine.
3. Fix fields shown for rack controllers.
4. Other extra fixes (if needed).

Another thing you need to work on is the commit message. The commit message is what shows up in the bzr log. The description is just for the review to read to know what you have done (only if the commit message can not tell them). So update your commit message as well to include a better idea of what this branch did to MAAS when it was merged.

Marking "approved" since this is needed for alpha1 and since we discussed it during the standup. Please, please, please next time split the work out to make it smaller. It really doesn't make it hard on you, if anything it helps you get eyes on the code sooner as you can go work on the next piece of work.

review: Approve
Revision history for this message
Andres Rodriguez (andreserl) :
review: Needs Information
Revision history for this message
Lee Trager (ltrager) wrote :

I apologize for such a huge branch, I kept adding just one more thing to get into alpha 1 which caused this to blow up. I'll make my branches smaller in the future.

I answered this inline but the reason the Device API has a number of methods removed is because its now based on the NodesHandler class. This brings consistency to the specifiers we can pass to our read methods and removed a bunch of duplicated code.

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

go for it!

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/account.py'
2--- src/maasserver/api/account.py 2015-12-01 18:12:59 +0000
3+++ src/maasserver/api/account.py 2016-03-02 18:21:51 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
6+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """API handler: `Account`."""
10
11=== modified file 'src/maasserver/api/auth.py'
12--- src/maasserver/api/auth.py 2015-12-01 18:12:59 +0000
13+++ src/maasserver/api/auth.py 2016-03-02 18:21:51 +0000
14@@ -1,4 +1,4 @@
15-# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
16+# Copyright 2012-2016 Canonical Ltd. This software is licensed under the
17 # GNU Affero General Public License version 3 (see the file LICENSE).
18
19 """OAuth authentication for the various APIs."""
20
21=== modified file 'src/maasserver/api/bcache.py'
22--- src/maasserver/api/bcache.py 2015-12-16 00:01:56 +0000
23+++ src/maasserver/api/bcache.py 2016-03-02 18:21:51 +0000
24@@ -1,4 +1,4 @@
25-# Copyright 2015 Canonical Ltd. This software is licensed under the
26+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
27 # GNU Affero General Public License version 3 (see the file LICENSE).
28
29 """API handlers: `Bcache`."""
30@@ -38,7 +38,7 @@
31
32
33 class BcachesHandler(OperationsHandler):
34- """Manage bcache devices on a node."""
35+ """Manage bcache devices on a machine."""
36 api_doc_section_name = "Bcache Devices"
37 update = delete = None
38 fields = DISPLAYED_BCACHE_FIELDS
39@@ -49,7 +49,7 @@
40 return ('bcache_devices_handler', ["system_id"])
41
42 def read(self, request, system_id):
43- """List all bcache devices belonging to node.
44+ """List all bcache devices belonging to a machine.
45
46 Returns 404 if the machine is not found.
47 """
48@@ -77,7 +77,7 @@
49 system_id, request.user, NODE_PERMISSION.ADMIN)
50 if machine.status != NODE_STATUS.READY:
51 raise NodeStateViolation(
52- "Cannot create Bcache because node is not Ready.")
53+ "Cannot create Bcache because the machine is not Ready.")
54 form = CreateBcacheForm(machine, data=request.data)
55 if form.is_valid():
56 return form.save()
57@@ -86,7 +86,7 @@
58
59
60 class BcacheHandler(OperationsHandler):
61- """Manage bcache device on a node."""
62+ """Manage bcache device on a machine."""
63 api_doc_section_name = "Bcache Device"
64 create = None
65 model = Bcache
66@@ -123,30 +123,30 @@
67 return bcache.get_bcache_backing_filesystem().get_parent()
68
69 def read(self, request, system_id, bcache_id):
70- """Read bcache device on node.
71+ """Read bcache device on a machine.
72
73- Returns 404 if the node or bcache is not found.
74+ Returns 404 if the machine or bcache is not found.
75 """
76 return Bcache.objects.get_object_or_404(
77 system_id, bcache_id, request.user, NODE_PERMISSION.VIEW)
78
79 def delete(self, request, system_id, bcache_id):
80- """Delete bcache on node.
81+ """Delete bcache on a machine.
82
83- Returns 404 if the node or bcache is not found.
84- Returns 409 if the node is not Ready.
85+ Returns 404 if the machine or bcache is not found.
86+ Returns 409 if the machine is not Ready.
87 """
88 bcache = Bcache.objects.get_object_or_404(
89 system_id, bcache_id, request.user, NODE_PERMISSION.ADMIN)
90 node = bcache.get_node()
91 if node.status != NODE_STATUS.READY:
92 raise NodeStateViolation(
93- "Cannot delete Bcache because the node is not Ready.")
94+ "Cannot delete Bcache because the machine is not Ready.")
95 bcache.delete()
96 return rc.DELETED
97
98 def update(self, request, system_id, bcache_id):
99- """Delete bcache on node.
100+ """Delete bcache on a machine.
101
102 :param name: Name of the Bcache.
103 :param uuid: UUID of the Bcache.
104@@ -158,15 +158,15 @@
105 Specifying both a device and a partition for a given role (cache or
106 backing) is not allowed.
107
108- Returns 404 if the node or the bcache is not found.
109- Returns 409 if the node is not Ready.
110+ Returns 404 if the machine or the bcache is not found.
111+ Returns 409 if the machine is not Ready.
112 """
113 bcache = Bcache.objects.get_object_or_404(
114 system_id, bcache_id, request.user, NODE_PERMISSION.ADMIN)
115 node = bcache.get_node()
116 if node.status != NODE_STATUS.READY:
117 raise NodeStateViolation(
118- "Cannot update Bcache because the node is not Ready.")
119+ "Cannot update Bcache because the machine is not Ready.")
120 form = UpdateBcacheForm(bcache, data=request.data)
121 if form.is_valid():
122 return form.save()
123
124=== modified file 'src/maasserver/api/bcache_cacheset.py'
125--- src/maasserver/api/bcache_cacheset.py 2015-12-16 00:01:56 +0000
126+++ src/maasserver/api/bcache_cacheset.py 2016-03-02 18:21:51 +0000
127@@ -1,4 +1,4 @@
128-# Copyright 2015 Canonical Ltd. This software is licensed under the
129+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
130 # GNU Affero General Public License version 3 (see the file LICENSE).
131
132 """API handlers: `CacheSet`."""
133@@ -32,7 +32,7 @@
134
135
136 class BcacheCacheSetsHandler(OperationsHandler):
137- """Manage bcache cache sets on a node."""
138+ """Manage bcache cache sets on a machine."""
139 api_doc_section_name = "Bcache Cache Sets"
140 update = delete = None
141 fields = DISPLAYED_CACHE_SET_FIELDS
142@@ -43,7 +43,7 @@
143 return ('bcache_cache_sets_handler', ["system_id"])
144
145 def read(self, request, system_id):
146- """List all bcache cache sets belonging to node.
147+ """List all bcache cache sets belonging to a machine.
148
149 Returns 404 if the machine is not found.
150 """
151@@ -75,7 +75,7 @@
152
153
154 class BcacheCacheSetHandler(OperationsHandler):
155- """Manage bcache cache set on a node."""
156+ """Manage bcache cache set on a machine."""
157 api_doc_section_name = "Bcache Cache Set"
158 create = None
159 model = CacheSet
160@@ -99,26 +99,26 @@
161 return cache_set.get_device()
162
163 def read(self, request, system_id, cache_set_id):
164- """Read bcache cache set on node.
165+ """Read bcache cache set on a machine.
166
167- Returns 404 if the node or cache set is not found.
168+ Returns 404 if the machine or cache set is not found.
169 """
170 return CacheSet.objects.get_cache_set_or_404(
171 system_id, cache_set_id, request.user, NODE_PERMISSION.VIEW)
172
173 def delete(self, request, system_id, cache_set_id):
174- """Delete cache set on node.
175+ """Delete cache set on a machine.
176
177 Returns 400 if the cache set is in use.
178- Returns 404 if the node or cache set is not found.
179- Returns 409 if the node is not Ready.
180+ Returns 404 if the machine or cache set is not found.
181+ Returns 409 if the machine is not Ready.
182 """
183 cache_set = CacheSet.objects.get_cache_set_or_404(
184 system_id, cache_set_id, request.user, NODE_PERMISSION.ADMIN)
185 node = cache_set.get_node()
186 if node.status != NODE_STATUS.READY:
187 raise NodeStateViolation(
188- "Cannot delete cache set because the node is not Ready.")
189+ "Cannot delete cache set because the machine is not Ready.")
190 if cache_set.filesystemgroup_set.exists():
191 raise MAASAPIBadRequest(
192 "Cannot delete cache set; it's currently in use.")
193@@ -127,22 +127,22 @@
194 return rc.DELETED
195
196 def update(self, request, system_id, cache_set_id):
197- """Delete bcache on node.
198+ """Delete bcache on a machine.
199
200 :param cache_device: Cache block device to replace current one.
201 :param cache_partition: Cache partition to replace current one.
202
203 Specifying both a cache_device and a cache_partition is not allowed.
204
205- Returns 404 if the node or the cache set is not found.
206- Returns 409 if the node is not Ready.
207+ Returns 404 if the machine or the cache set is not found.
208+ Returns 409 if the machine is not Ready.
209 """
210 cache_set = CacheSet.objects.get_cache_set_or_404(
211 system_id, cache_set_id, request.user, NODE_PERMISSION.ADMIN)
212 node = cache_set.get_node()
213 if node.status != NODE_STATUS.READY:
214 raise NodeStateViolation(
215- "Cannot update cache set because the node is not Ready.")
216+ "Cannot update cache set because the machine is not Ready.")
217 form = UpdateCacheSetForm(cache_set, data=request.data)
218 if form.is_valid():
219 return form.save()
220
221=== modified file 'src/maasserver/api/blockdevices.py'
222--- src/maasserver/api/blockdevices.py 2016-03-01 16:02:44 +0000
223+++ src/maasserver/api/blockdevices.py 2016-03-02 18:21:51 +0000
224@@ -1,4 +1,4 @@
225-# Copyright 2015 Canonical Ltd. This software is licensed under the
226+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
227 # GNU Affero General Public License version 3 (see the file LICENSE).
228
229 """API handlers: `BlockDevice`."""
230@@ -61,16 +61,16 @@
231 node, user, operation):
232 if node.status not in [NODE_STATUS.READY, NODE_STATUS.ALLOCATED]:
233 raise NodeStateViolation(
234- "Cannot %s block device because the node is not Ready "
235+ "Cannot %s block device because the machine is not Ready "
236 "or Allocated." % operation)
237 if node.status == NODE_STATUS.READY and not user.is_superuser:
238 raise PermissionDenied(
239 "Cannot %s block device because you don't have the "
240- "permissions on a Ready node." % operation)
241+ "permissions on a Ready machine." % operation)
242
243
244 class BlockDevicesHandler(OperationsHandler):
245- """Manage block devices on a node."""
246+ """Manage block devices on a machine."""
247 api_doc_section_name = "Block devices"
248 replace = update = delete = None
249 fields = DISPLAYED_BLOCKDEVICE_FIELDS
250@@ -80,7 +80,7 @@
251 return ('blockdevices_handler', ["system_id"])
252
253 def read(self, request, system_id):
254- """List all block devices belonging to node.
255+ """List all block devices belonging to a machine.
256
257 Returns 404 if the machine is not found.
258 """
259@@ -113,7 +113,7 @@
260
261
262 class BlockDeviceHandler(OperationsHandler):
263- """Manage a block device on a node."""
264+ """Manage a block device on a machine."""
265 api_doc_section_name = "Block device"
266 create = replace = None
267 model = BlockDevice
268@@ -203,29 +203,29 @@
269 def read(self, request, system_id, device_id):
270 """Read block device on node.
271
272- Returns 404 if the node or block device is not found.
273+ Returns 404 if the machine or block device is not found.
274 """
275 return BlockDevice.objects.get_block_device_or_404(
276 system_id, device_id, request.user, NODE_PERMISSION.VIEW)
277
278 def delete(self, request, system_id, device_id):
279- """Delete block device on node.
280+ """Delete block device on a machine.
281
282- Returns 404 if the node or block device is not found.
283+ Returns 404 if the machine or block device is not found.
284 Returns 403 if the user is not allowed to delete the block device.
285- Returns 409 if the node is not Ready.
286+ Returns 409 if the machine is not Ready.
287 """
288 device = BlockDevice.objects.get_block_device_or_404(
289 system_id, device_id, request.user, NODE_PERMISSION.ADMIN)
290 node = device.get_node()
291 if node.status != NODE_STATUS.READY:
292 raise NodeStateViolation(
293- "Cannot delete block device because the node is not Ready.")
294+ "Cannot delete block device because the machine is not Ready.")
295 device.delete()
296 return rc.DELETED
297
298 def update(self, request, system_id, device_id):
299- """Update block device on node.
300+ """Update block device on a machine.
301
302 Fields for physical block device:
303 :param name: Name of the block device.
304@@ -243,16 +243,16 @@
305 :param size: Size of the block device. (Only allowed for logical \
306 volumes.)
307
308- Returns 404 if the node or block device is not found.
309+ Returns 404 if the machine or block device is not found.
310 Returns 403 if the user is not allowed to update the block device.
311- Returns 409 if the node is not Ready.
312+ Returns 409 if the machine is not Ready.
313 """
314 device = BlockDevice.objects.get_block_device_or_404(
315 system_id, device_id, request.user, NODE_PERMISSION.ADMIN)
316 node = device.get_node()
317 if node.status != NODE_STATUS.READY:
318 raise NodeStateViolation(
319- "Cannot update block device because the node is not Ready.")
320+ "Cannot update block device because the machine is not Ready.")
321 if device.type == 'physical':
322 form = UpdatePhysicalBlockDeviceForm(
323 instance=device, data=request.data)
324@@ -269,40 +269,40 @@
325
326 @operation(idempotent=True)
327 def add_tag(self, request, system_id, device_id):
328- """Add a tag to block device on node.
329+ """Add a tag to block device on a machine.
330
331 :param tag: The tag being added.
332
333- Returns 404 if the node or block device is not found.
334+ Returns 404 if the machine or block device is not found.
335 Returns 403 if the user is not allowed to update the block device.
336- Returns 409 if the node is not Ready.
337+ Returns 409 if the machine is not Ready.
338 """
339 device = BlockDevice.objects.get_block_device_or_404(
340 system_id, device_id, request.user, NODE_PERMISSION.ADMIN)
341 node = device.get_node()
342 if node.status != NODE_STATUS.READY:
343 raise NodeStateViolation(
344- "Cannot update block device because the node is not Ready.")
345+ "Cannot update block device because the machine is not Ready.")
346 device.add_tag(get_mandatory_param(request.GET, 'tag'))
347 device.save()
348 return device
349
350 @operation(idempotent=True)
351 def remove_tag(self, request, system_id, device_id):
352- """Remove a tag from block device on node.
353+ """Remove a tag from block device on a machine.
354
355 :param tag: The tag being removed.
356
357- Returns 404 if the node or block device is not found.
358+ Returns 404 if the machine or block device is not found.
359 Returns 403 if the user is not allowed to update the block device.
360- Returns 409 if the node is not Ready.
361+ Returns 409 if the machine is not Ready.
362 """
363 device = BlockDevice.objects.get_block_device_or_404(
364 system_id, device_id, request.user, NODE_PERMISSION.ADMIN)
365 node = device.get_node()
366 if node.status != NODE_STATUS.READY:
367 raise NodeStateViolation(
368- "Cannot update block device because the node is not Ready.")
369+ "Cannot update block device because the machine is not Ready.")
370 device.remove_tag(get_mandatory_param(request.GET, 'tag'))
371 device.save()
372 return device
373@@ -316,8 +316,8 @@
374
375 Returns 403 when the user doesn't have the ability to format the \
376 block device.
377- Returns 404 if the node or block device is not found.
378- Returns 409 if the node is not Ready or Allocated.
379+ Returns 404 if the machine or block device is not found.
380+ Returns 409 if the machine is not Ready or Allocated.
381 """
382 device = BlockDevice.objects.get_block_device_or_404(
383 system_id, device_id, request.user, NODE_PERMISSION.EDIT)
384@@ -338,8 +338,8 @@
385 or part of a filesystem group.
386 Returns 403 when the user doesn't have the ability to unformat the \
387 block device.
388- Returns 404 if the node or block device is not found.
389- Returns 409 if the node is not Ready or Allocated.
390+ Returns 404 if the machine or block device is not found.
391+ Returns 409 if the machine is not Ready or Allocated.
392 """
393 device = BlockDevice.objects.get_block_device_or_404(
394 system_id, device_id, request.user, NODE_PERMISSION.EDIT)
395@@ -371,8 +371,8 @@
396
397 Returns 403 when the user doesn't have the ability to mount the \
398 block device.
399- Returns 404 if the node or block device is not found.
400- Returns 409 if the node is not Ready or Allocated.
401+ Returns 404 if the machine or block device is not found.
402+ Returns 409 if the machine is not Ready or Allocated.
403 """
404 device = BlockDevice.objects.get_block_device_or_404(
405 system_id, device_id, request.user, NODE_PERMISSION.EDIT)
406@@ -394,8 +394,8 @@
407 mounted.
408 Returns 403 when the user doesn't have the ability to unmount the \
409 block device.
410- Returns 404 if the node or block device is not found.
411- Returns 409 if the node is not Ready or Allocated.
412+ Returns 404 if the machine or block device is not found.
413+ Returns 409 if the machine is not Ready or Allocated.
414 """
415 device = BlockDevice.objects.get_block_device_or_404(
416 system_id, device_id, request.user, NODE_PERMISSION.EDIT)
417@@ -414,19 +414,19 @@
418
419 @operation(idempotent=False)
420 def set_boot_disk(self, request, system_id, device_id):
421- """Set this block device as the boot disk for the node.
422+ """Set this block device as the boot disk for the machine.
423
424 Returns 400 if the block device is a virtual block device.
425- Returns 404 if the node or block device is not found.
426+ Returns 404 if the machine or block device is not found.
427 Returns 403 if the user is not allowed to update the block device.
428- Returns 409 if the node is not Ready or Allocated.
429+ Returns 409 if the machine is not Ready or Allocated.
430 """
431 device = BlockDevice.objects.get_block_device_or_404(
432 system_id, device_id, request.user, NODE_PERMISSION.ADMIN)
433 node = device.get_node()
434 if node.status != NODE_STATUS.READY:
435 raise NodeStateViolation(
436- "Cannot set as boot disk because the node is not Ready.")
437+ "Cannot set as boot disk because the machine is not Ready.")
438 if not isinstance(device, PhysicalBlockDevice):
439 raise MAASAPIBadRequest(
440 "Cannot set a %s block device as the boot disk." % device.type)
441
442=== modified file 'src/maasserver/api/boot_resources.py'
443--- src/maasserver/api/boot_resources.py 2016-02-19 08:38:36 +0000
444+++ src/maasserver/api/boot_resources.py 2016-03-02 18:21:51 +0000
445@@ -1,4 +1,4 @@
446-# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
447+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
448 # GNU Affero General Public License version 3 (see the file LICENSE).
449
450 """API handlers: `BootResouce`."""
451
452=== modified file 'src/maasserver/api/boot_source_selections.py'
453--- src/maasserver/api/boot_source_selections.py 2015-12-01 18:12:59 +0000
454+++ src/maasserver/api/boot_source_selections.py 2016-03-02 18:21:51 +0000
455@@ -1,4 +1,4 @@
456-# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
457+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
458 # GNU Affero General Public License version 3 (see the file LICENSE).
459
460 """API handlers: `BootSourceSelection`"""
461
462=== modified file 'src/maasserver/api/boot_sources.py'
463--- src/maasserver/api/boot_sources.py 2015-12-11 20:45:50 +0000
464+++ src/maasserver/api/boot_sources.py 2016-03-02 18:21:51 +0000
465@@ -1,4 +1,4 @@
466-# Copyright 2014 Canonical Ltd. This software is licensed under the
467+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
468 # GNU Affero General Public License version 3 (see the file LICENSE).
469
470 """API handlers: `BootSource`."""
471
472=== modified file 'src/maasserver/api/commissioning_scripts.py'
473--- src/maasserver/api/commissioning_scripts.py 2015-12-01 18:12:59 +0000
474+++ src/maasserver/api/commissioning_scripts.py 2016-03-02 18:21:51 +0000
475@@ -1,4 +1,4 @@
476-# Copyright 2014 Canonical Ltd. This software is licensed under the
477+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
478 # GNU Affero General Public License version 3 (see the file LICENSE).
479
480 """API handlers: `CommissioningScript`."""
481@@ -49,7 +49,7 @@
482 so opens you up to risks w.r.t. encoding and ordering. The name must
483 not contain any whitespace, quotes, or apostrophes.
484
485- A commissioning node will run each of the scripts in lexicographical
486+ A commissioning machine will run each of the scripts in lexicographical
487 order. There are no promises about how non-ASCII characters are
488 sorted, or even how upper-case letters are sorted relative to
489 lower-case letters. So where ordering matters, use unique numbers.
490@@ -61,7 +61,7 @@
491 script should be ASCII text to avoid any confusion over encoding. But
492 in some cases a commissioning script might consist of a binary tool
493 provided by a hardware vendor. Either way, the script gets passed to
494- the commissioning node in the exact form in which it was uploaded.
495+ the commissioning machine in the exact form in which it was uploaded.
496
497 :param name: Unique identifying name for the script. Names should
498 follow the pattern of "25-burn-in-hard-disk" (all ASCII, and with
499
500=== modified file 'src/maasserver/api/devices.py'
501--- src/maasserver/api/devices.py 2016-02-18 00:34:58 +0000
502+++ src/maasserver/api/devices.py 2016-03-02 18:21:51 +0000
503@@ -1,4 +1,4 @@
504-# Copyright 2015 Canonical Ltd. This software is licensed under the
505+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
506 # GNU Affero General Public License version 3 (see the file LICENSE).
507
508 __all__ = [
509@@ -7,17 +7,13 @@
510 ]
511
512 from maasserver.api.logger import maaslog
513-from maasserver.api.support import (
514- operation,
515- OperationsHandler,
516-)
517-from maasserver.api.utils import get_optional_list
518-from maasserver.enum import (
519- NODE_PERMISSION,
520- NODE_TYPE_CHOICES,
521-)
522+from maasserver.api.nodes import (
523+ NodeHandler,
524+ NodesHandler,
525+)
526+from maasserver.api.support import operation
527+from maasserver.enum import NODE_PERMISSION
528 from maasserver.exceptions import MAASAPIValidationError
529-from maasserver.fields import MAC_RE
530 from maasserver.forms import (
531 DeviceForm,
532 DeviceWithMACsForm,
533@@ -29,6 +25,8 @@
534 DISPLAYED_DEVICE_FIELDS = (
535 'system_id',
536 'hostname',
537+ 'domain',
538+ 'fqdn',
539 'owner',
540 'macaddress_set',
541 'parent',
542@@ -40,7 +38,7 @@
543 )
544
545
546-class DeviceHandler(OperationsHandler):
547+class DeviceHandler(NodeHandler):
548 """Manage an individual device.
549
550 The device is identified by its system_id.
551@@ -59,52 +57,28 @@
552 else:
553 return node.parent.system_id
554
555- @classmethod
556- def hostname(handler, node):
557- """Override the 'hostname' field so that it returns the FQDN."""
558- return node.fqdn
559-
560- @classmethod
561- def owner(handler, node):
562- """Override 'owner' so it emits the owner's name rather than a
563- full nested user object."""
564- if node.owner is None:
565- return None
566- return node.owner.username
567-
568- @classmethod
569- def macaddress_set(handler, device):
570- return [
571- {"mac_address": "%s" % interface.mac_address}
572- for interface in device.interface_set.all()
573- if interface.mac_address
574- ]
575-
576- @classmethod
577- def node_type_name(handler, node):
578- return NODE_TYPE_CHOICES[node.node_type][1]
579-
580- def read(self, request, system_id):
581- """Read a specific device.
582-
583- Returns 404 if the device is not found.
584- """
585- return Device.objects.get_node_or_404(
586- system_id=system_id, user=request.user, perm=NODE_PERMISSION.VIEW)
587-
588 def update(self, request, system_id):
589 """Update a specific device.
590
591 :param hostname: The new hostname for this device.
592+ :type hostname: unicode
593+
594+ :param domain: The domain for this device.
595+ :type domain: unicode
596+
597 :param parent: Optional system_id to indicate this device's parent.
598 If the parent is already set and this parameter is omitted,
599 the parent will be unchanged.
600- :type hostname: unicode
601+ :type parent: unicode
602+
603+ :param zone: Name of a valid physical zone in which to place this
604+ node.
605+ :type zone: unicode
606
607 Returns 404 if the device is not found.
608 Returns 403 if the user does not have permission to update the device.
609 """
610- device = Device.objects.get_node_or_404(
611+ device = self.model.objects.get_node_or_404(
612 system_id=system_id, user=request.user, perm=NODE_PERMISSION.EDIT)
613 form = DeviceForm(data=request.data, instance=device)
614
615@@ -120,7 +94,7 @@
616 Returns 403 if the user does not have permission to delete the device.
617 Returns 204 if the device is successfully deleted.
618 """
619- device = Device.objects.get_node_or_404(
620+ device = self.model.objects.get_node_or_404(
621 system_id=system_id, user=request.user,
622 perm=NODE_PERMISSION.EDIT)
623 device.delete()
624@@ -139,18 +113,28 @@
625 return ('device_handler', (device_system_id,))
626
627
628-class DevicesHandler(OperationsHandler):
629+class DevicesHandler(NodesHandler):
630 """Manage the collection of all the devices in the MAAS."""
631 api_doc_section_name = "Devices"
632 update = delete = None
633+ base_model = Device
634
635 @operation(idempotent=False)
636 def create(self, request):
637 """Create a new device.
638
639+ :param hostname: A hostname. If not given, one will be generated.
640+ :type hostname: unicode
641+
642+ :param domain: The domain of the device. If not given the default
643+ domain is used.
644+ :type domain: unicode
645+
646 :param mac_addresses: One or more MAC addresses for the device.
647- :param hostname: A hostname. If not given, one will be generated.
648+ :type mac_addresses: unicode
649+
650 :param parent: The system id of the parent. Optional.
651+ :type parent: unicode
652 """
653 form = DeviceWithMACsForm(data=request.data, request=request)
654 if form.is_valid():
655@@ -163,46 +147,6 @@
656 else:
657 raise MAASAPIValidationError(form.errors)
658
659- def read(self, request):
660- """List devices visible to the user, optionally filtered by criteria.
661-
662- :param hostname: An optional list of hostnames. Only devices with
663- matching hostnames will be returned.
664- :type hostname: iterable
665- :param mac_address: An optional list of MAC addresses. Only
666- devices with matching MAC addresses will be returned.
667- :type mac_address: iterable
668- :param id: An optional list of system ids. Only devices with
669- matching system ids will be returned.
670- :type id: iterable
671- """
672- # Get filters from request.
673- match_ids = get_optional_list(request.GET, 'id')
674- match_macs = get_optional_list(request.GET, 'mac_address')
675- if match_macs is not None:
676- invalid_macs = [
677- mac for mac in match_macs if MAC_RE.match(mac) is None]
678- if len(invalid_macs) != 0:
679- raise MAASAPIValidationError(
680- "Invalid MAC address(es): %s" % ", ".join(invalid_macs))
681-
682- # Fetch nodes and apply filters.
683- devices = Device.objects.get_nodes(
684- request.user, NODE_PERMISSION.VIEW, ids=match_ids)
685- if match_macs is not None:
686- devices = devices.filter(interface__mac_address__in=match_macs)
687- match_hostnames = get_optional_list(request.GET, 'hostname')
688- if match_hostnames is not None:
689- devices = devices.filter(hostname__in=match_hostnames)
690-
691- # Prefetch related objects that are needed for rendering the result.
692- devices = devices.prefetch_related('interface_set__node')
693- devices = devices.prefetch_related('interface_set__ip_addresses')
694- devices = devices.prefetch_related('tags')
695- devices = devices.prefetch_related('zone')
696- devices = devices.prefetch_related('domain')
697- return devices.order_by('id')
698-
699 @classmethod
700 def resource_uri(cls, *args, **kwargs):
701 return ('devices_handler', [])
702
703=== modified file 'src/maasserver/api/dnsresourcerecords.py'
704--- src/maasserver/api/dnsresourcerecords.py 2016-02-11 18:11:40 +0000
705+++ src/maasserver/api/dnsresourcerecords.py 2016-03-02 18:21:51 +0000
706@@ -1,4 +1,4 @@
707-# Copyright 2015,2016 Canonical Ltd. This software is licensed under the
708+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
709 # GNU Affero General Public License version 3 (see the file LICENSE).
710
711 """API handlers: `DNSData`."""
712
713=== modified file 'src/maasserver/api/dnsresources.py'
714--- src/maasserver/api/dnsresources.py 2016-01-25 15:11:21 +0000
715+++ src/maasserver/api/dnsresources.py 2016-03-02 18:21:51 +0000
716@@ -1,4 +1,4 @@
717-# Copyright 2015,2016 Canonical Ltd. This software is licensed under the
718+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
719 # GNU Affero General Public License version 3 (see the file LICENSE).
720
721 """API handlers: `DNSResource`."""
722
723=== modified file 'src/maasserver/api/doc.py'
724--- src/maasserver/api/doc.py 2016-02-10 20:24:35 +0000
725+++ src/maasserver/api/doc.py 2016-03-02 18:21:51 +0000
726@@ -1,4 +1,4 @@
727-# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
728+# Copyright 2012-2016 Canonical Ltd. This software is licensed under the
729 # GNU Affero General Public License version 3 (see the file LICENSE).
730
731 """Utilities to help document/describe the public facing API."""
732
733=== modified file 'src/maasserver/api/doc_handler.py'
734--- src/maasserver/api/doc_handler.py 2016-02-11 15:06:36 +0000
735+++ src/maasserver/api/doc_handler.py 2016-03-02 18:21:51 +0000
736@@ -1,4 +1,4 @@
737-# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
738+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
739 # GNU Affero General Public License version 3 (see the file LICENSE).
740
741 """Restful MAAS API.
742@@ -43,7 +43,7 @@
743 methods will take one special parameter, called `op`, to indicate what it is
744 you want to do.
745
746-For example, to list all nodes, you might GET "/api/2.0/nodes/?op=list".
747+For example, to list all machines, you might GET "/api/2.0/machines".
748 """
749
750 __all__ = [
751
752=== modified file 'src/maasserver/api/domains.py'
753--- src/maasserver/api/domains.py 2016-01-22 00:02:37 +0000
754+++ src/maasserver/api/domains.py 2016-03-02 18:21:51 +0000
755@@ -1,4 +1,4 @@
756-# Copyright 2015 Canonical Ltd. This software is licensed under the
757+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
758 # GNU Affero General Public License version 3 (see the file LICENSE).
759
760 """API handlers: `Domain`."""
761
762=== modified file 'src/maasserver/api/events.py'
763--- src/maasserver/api/events.py 2016-01-28 21:47:31 +0000
764+++ src/maasserver/api/events.py 2016-03-02 18:21:51 +0000
765@@ -1,4 +1,4 @@
766-# Copyright 2015 Canonical Ltd. This software is licensed under the
767+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
768 # GNU Affero General Public License version 3 (see the file LICENSE).
769
770 __all__ = [
771
772=== modified file 'src/maasserver/api/fabrics.py'
773--- src/maasserver/api/fabrics.py 2015-12-01 18:12:59 +0000
774+++ src/maasserver/api/fabrics.py 2016-03-02 18:21:51 +0000
775@@ -1,4 +1,4 @@
776-# Copyright 2015 Canonical Ltd. This software is licensed under the
777+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
778 # GNU Affero General Public License version 3 (see the file LICENSE).
779
780 """API handlers: `Fabric`."""
781
782=== modified file 'src/maasserver/api/fannetworks.py'
783--- src/maasserver/api/fannetworks.py 2015-12-01 18:12:59 +0000
784+++ src/maasserver/api/fannetworks.py 2016-03-02 18:21:51 +0000
785@@ -1,4 +1,4 @@
786-# Copyright 2015 Canonical Ltd. This software is licensed under the
787+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
788 # GNU Affero General Public License version 3 (see the file LICENSE).
789
790 """API handlers: `Fan Network`."""
791
792=== modified file 'src/maasserver/api/files.py'
793--- src/maasserver/api/files.py 2016-02-18 01:32:50 +0000
794+++ src/maasserver/api/files.py 2016-03-02 18:21:51 +0000
795@@ -1,4 +1,4 @@
796-# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
797+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
798 # GNU Affero General Public License version 3 (see the file LICENSE).
799
800 """API handlers: `File`."""
801
802=== modified file 'src/maasserver/api/interfaces.py'
803--- src/maasserver/api/interfaces.py 2015-12-16 00:01:56 +0000
804+++ src/maasserver/api/interfaces.py 2016-03-02 18:21:51 +0000
805@@ -1,4 +1,4 @@
806-# Copyright 2015 Canonical Ltd. This software is licensed under the
807+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
808 # GNU Affero General Public License version 3 (see the file LICENSE).
809
810 """API handlers: `Interface`."""
811@@ -72,7 +72,7 @@
812
813
814 class InterfacesHandler(OperationsHandler):
815- """Manage interfaces on a node or device."""
816+ """Manage interfaces on a node."""
817 api_doc_section_name = "Interfaces"
818 create = update = delete = None
819 fields = DISPLAYED_INTERFACE_FIELDS
820
821=== modified file 'src/maasserver/api/ip_addresses.py'
822--- src/maasserver/api/ip_addresses.py 2016-02-17 18:21:13 +0000
823+++ src/maasserver/api/ip_addresses.py 2016-03-02 18:21:51 +0000
824@@ -1,4 +1,4 @@
825-# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
826+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
827 # GNU Affero General Public License version 3 (see the file LICENSE).
828
829 """API handler: `StaticIPAddress`."""
830
831=== modified file 'src/maasserver/api/license_keys.py'
832--- src/maasserver/api/license_keys.py 2015-12-01 18:12:59 +0000
833+++ src/maasserver/api/license_keys.py 2016-03-02 18:21:51 +0000
834@@ -1,4 +1,4 @@
835-# Copyright 2014 Canonical Ltd. This software is licensed under the
836+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
837 # GNU Affero General Public License version 3 (see the file LICENSE).
838
839 """API handlers: `LicenseKey`."""
840
841=== modified file 'src/maasserver/api/logger.py'
842--- src/maasserver/api/logger.py 2015-12-01 18:12:59 +0000
843+++ src/maasserver/api/logger.py 2016-03-02 18:21:51 +0000
844@@ -1,4 +1,4 @@
845-# Copyright 2014 Canonical Ltd. This software is licensed under the
846+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
847 # GNU Affero General Public License version 3 (see the file LICENSE).
848
849 """API logger."""
850
851=== modified file 'src/maasserver/api/maas.py'
852--- src/maasserver/api/maas.py 2016-02-11 15:06:36 +0000
853+++ src/maasserver/api/maas.py 2016-03-02 18:21:51 +0000
854@@ -1,4 +1,4 @@
855-# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
856+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
857 # GNU Affero General Public License version 3 (see the file LICENSE).
858
859 """API handler: MAAS."""
860
861=== modified file 'src/maasserver/api/machines.py'
862--- src/maasserver/api/machines.py 2016-03-02 02:22:45 +0000
863+++ src/maasserver/api/machines.py 2016-03-02 18:21:51 +0000
864@@ -1,4 +1,4 @@
865-# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
866+# Copyright 2012-2016 Canonical Ltd. This software is licensed under the
867 # GNU Affero General Public License version 3 (see the file LICENSE).
868
869 __all__ = [
870@@ -59,12 +59,13 @@
871 Unauthorized,
872 )
873 from maasserver.forms import (
874- get_node_create_form,
875- get_node_edit_form,
876+ get_machine_create_form,
877+ get_machine_edit_form,
878 )
879 from maasserver.forms_commission import CommissionForm
880 from maasserver.models import (
881 Config,
882+ Domain,
883 Machine,
884 RackController,
885 )
886@@ -91,6 +92,8 @@
887 DISPLAYED_MACHINE_FIELDS = (
888 'system_id',
889 'hostname',
890+ 'domain',
891+ 'fqdn',
892 'owner',
893 'macaddress_set',
894 'boot_interface',
895@@ -191,17 +194,25 @@
896
897 :param hostname: The new hostname for this machine.
898 :type hostname: unicode
899+
900+ :param domain: The domain for this machine. If not given the default
901+ domain is used.
902+ :type domain: unicode
903+
904 :param architecture: The new architecture for this machine.
905 :type architecture: unicode
906+
907 :param min_hwe_kernel: A string containing the minimum kernel version
908 allowed to be ran on this machine.
909 :type min_hwe_kernel: unicode
910+
911 :param power_type: The new power type for this machine. If you use the
912 default value, power_parameters will be set to the empty string.
913 Available to admin users.
914 See the `Power types`_ section for a list of the available power
915 types.
916 :type power_type: unicode
917+
918 :param power_parameters_{param1}: The new value for the 'param1'
919 power parameter. Note that this is dynamic as the available
920 parameters depend on the selected value of the Machine's
921@@ -209,19 +220,32 @@
922 section for a list of the available power parameters for each
923 power type.
924 :type power_parameters_{param1}: unicode
925+
926 :param power_parameters_skip_check: Whether or not the new power
927 parameters for this machine should be checked against the expected
928 power parameters for the machine's power type ('true' or 'false').
929 The default is 'false'.
930 :type power_parameters_skip_check: unicode
931+
932 :param zone: Name of a valid physical zone in which to place this
933- machine
934+ machine.
935 :type zone: unicode
936+
937 :param swap_size: Specifies the size of the swap file, in bytes. Field
938 accept K, M, G and T suffixes for values expressed respectively in
939 kilobytes, megabytes, gigabytes and terabytes.
940 :type swap_size: unicode
941
942+ :param disable_ipv4: Whether or not IPv4 should be enabled on the
943+ machine.
944+ :type disable_ipv4: boolean
945+
946+ :param cpu_count: The amount of CPU cores the machine has.
947+ :type cpu_count: integer
948+
949+ :param memory: How much memory the machine has.
950+ :type memory: unicode
951+
952 Returns 404 if the machine is not found.
953 Returns 403 if the user does not have permission to update the machine.
954 """
955@@ -234,7 +258,7 @@
956 if 'power_type' not in request.data:
957 altered_query_data['power_type'] = machine.power_type
958
959- Form = get_node_edit_form(request.user)
960+ Form = get_machine_edit_form(request.user)
961 form = Form(data=altered_query_data, instance=machine)
962
963 if form.is_valid():
964@@ -325,7 +349,7 @@
965 if not machine.distro_series and not series:
966 series = Config.objects.get_config('default_distro_series')
967 if None in (series, license_key, hwe_kernel):
968- Form = get_node_edit_form(request.user)
969+ Form = get_machine_edit_form(request.user)
970 form = Form(instance=machine)
971 if series is not None:
972 form.set_distro_series(series=series)
973@@ -375,7 +399,7 @@
974 machine.start(request.user, user_data=user_data, comment=comment)
975 except StaticIPAddressExhaustion:
976 # The API response should contain error text with the
977- # system_id in it, as that is the primary API key to a node.
978+ # system_id in it, as that is the primary API key to a machine.
979 raise StaticIPAddressExhaustion(
980 "%s: Unable to allocate static IP due to address"
981 " exhaustion." % system_id)
982@@ -383,7 +407,7 @@
983
984 @operation(idempotent=False)
985 def release(self, request, system_id):
986- """Release a node. Opposite of `Machines.allocate`.
987+ """Release a machine. Opposite of `Machines.allocate`.
988
989 :param comment: Optional comment for the event log.
990 :type comment: unicode
991@@ -396,7 +420,7 @@
992 machine = self.model.objects.get_node_or_404(
993 system_id=system_id, user=request.user, perm=NODE_PERMISSION.EDIT)
994 if machine.status in (NODE_STATUS.RELEASING, NODE_STATUS.READY):
995- # Nothing to do if this node is already releasing, otherwise
996+ # Nothing to do if this machine is already releasing, otherwise
997 # this may be a redundant retry, and the
998 # postcondition is achieved, so call this success.
999 pass
1000@@ -536,7 +560,7 @@
1001
1002 If the default gateways need to be specific for this machine you can
1003 set which interface and subnet's gateway to use when this machine is
1004- deployed with the `node-interfaces set-default-gateway` API.
1005+ deployed with the `interfaces set-default-gateway` API.
1006
1007 Returns 404 if the machine could not be found.
1008 Returns 403 if the user does not have permission to clear the default
1009@@ -579,17 +603,17 @@
1010 user is not one.
1011
1012 This returns the power parameters, if any, configured for a
1013- node. For some types of power control this will include private
1014+ machine. For some types of power control this will include private
1015 information such as passwords and secret keys.
1016
1017- Returns 404 if the node is not found.
1018+ Returns 404 if the machine is not found.
1019 """
1020 machine = get_object_or_404(self.model, system_id=system_id)
1021 return machine.power_parameters
1022
1023 @operation(idempotent=True)
1024 def query_power_state(self, request, system_id):
1025- """Query the power state of a node.
1026+ """Query the power state of a machine.
1027
1028 Send a request to the machine's power controller which asks it about
1029 the machine's state. The reply to this could be delayed by up to
1030@@ -663,7 +687,7 @@
1031
1032 The machine will be in the New state.
1033
1034- :param request: The http request for this node to be created.
1035+ :param request: The http request for this machine to be created.
1036 :return: A `Machine`.
1037 :rtype: :class:`maasserver.models.Machine`.
1038 :raises: ValidationError
1039@@ -710,7 +734,7 @@
1040 raise MAASAPIValidationError(
1041 'min_hwe_kernel must be in the form of hwe-<LETTER>.')
1042
1043- Form = get_node_create_form(request.user)
1044+ Form = get_machine_create_form(request.user)
1045 form = Form(data=altered_query_data, request=request)
1046 if form.is_valid():
1047 machine = form.save()
1048@@ -745,18 +769,33 @@
1049 :param architecture: A string containing the architecture type of
1050 the machine. (For example, "i386", or "amd64".) To determine the
1051 supported architectures, use the boot-resources endpoint.
1052+ :type architecture: unicode
1053+
1054 :param min_hwe_kernel: A string containing the minimum kernel version
1055 allowed to be ran on this machine.
1056+ :type min_hwe_kernel: unicode
1057+
1058 :param subarchitecture: A string containing the subarchitecture type
1059 of the machine. (For example, "generic" or "hwe-t".) To determine
1060 the supported subarchitectures, use the boot-resources endpoint.
1061+ :type subarchitecture: unicode
1062+
1063 :param mac_addresses: One or more MAC addresses for the machine. To
1064 specify more than one MAC address, the parameter must be specified
1065 twice. (such as "machines new mac_addresses=01:02:03:04:05:06
1066 mac_addresses=02:03:04:05:06:07")
1067+ :type mac_addresses: unicode
1068+
1069 :param hostname: A hostname. If not given, one will be generated.
1070+ :type hostname: unicode
1071+
1072+ :param domain: The domain of the machine. If not given the default
1073+ domain is used.
1074+ :type domain: unicode
1075+
1076 :param power_type: A power management type, if applicable (e.g.
1077 "virsh", "ipmi").
1078+ :type power_type:unicode
1079 """
1080 return create_machine(request)
1081
1082@@ -774,7 +813,7 @@
1083
1084
1085 class MachinesHandler(NodesHandler):
1086- """Manage the collection of all the nodes in the MAAS."""
1087+ """Manage the collection of all the machines in the MAAS."""
1088 api_doc_section_name = "Machines"
1089 anonymous = AnonMachinesHandler
1090 base_model = Machine
1091@@ -798,18 +837,33 @@
1092 :param architecture: A string containing the architecture type of
1093 the machine. (For example, "i386", or "amd64".) To determine the
1094 supported architectures, use the boot-resources endpoint.
1095+ :type architecture: unicode
1096+
1097 :param min_hwe_kernel: A string containing the minimum kernel version
1098 allowed to be ran on this machine.
1099+ :type min_hwe_kernel: unicode
1100+
1101 :param subarchitecture: A string containing the subarchitecture type
1102 of the machine. (For example, "generic" or "hwe-t".) To determine
1103 the supported subarchitectures, use the boot-resources endpoint.
1104+ :type subarchitecture: unicode
1105+
1106 :param mac_addresses: One or more MAC addresses for the machine. To
1107 specify more than one MAC address, the parameter must be specified
1108 twice. (such as "machines new mac_addresses=01:02:03:04:05:06
1109 mac_addresses=02:03:04:05:06:07")
1110+ :type mac_addresses: unicode
1111+
1112 :param hostname: A hostname. If not given, one will be generated.
1113+ :type hostname: unicode
1114+
1115+ :param domain: The domain of the machine. If not given the default
1116+ domain is used.
1117+ :type domain: unicode
1118+
1119 :param power_type: A power management type, if applicable (e.g.
1120 "virsh", "ipmi").
1121+ :type power_type: unicode
1122 """
1123 machine = create_machine(request)
1124 if request.user.is_superuser:
1125@@ -857,7 +911,7 @@
1126 Returns 403 if the user is not an admin.
1127 """
1128 system_ids = set(request.POST.getlist('machines'))
1129- # Check the existence of these nodes first.
1130+ # Check the existence of these machines first.
1131 self._check_system_ids_exist(system_ids)
1132 # Make sure that the user has the required permission.
1133 machines = self.base_model.objects.get_nodes(
1134@@ -1045,7 +1099,7 @@
1135 if machine is None:
1136 constraints = form.describe_constraints()
1137 if constraints == '':
1138- # No constraints. That means no nodes at all were
1139+ # No constraints. That means no machines at all were
1140 # available.
1141 message = "No machine available."
1142 else:
1143@@ -1140,7 +1194,7 @@
1144 chassis types.
1145 :type password: unicode
1146
1147- :param accept_all: If true, all enlisted nodes will be
1148+ :param accept_all: If true, all enlisted machines will be
1149 commissioned.
1150 :type accept_all: unicode
1151
1152@@ -1149,6 +1203,9 @@
1153 automatically determine the rack controller to use.
1154 :type rack_controller: unicode
1155
1156+ :param domain: The domain that each new machine added should use.
1157+ :type domain: unicode
1158+
1159 The following are optional if you are adding a virsh, vmware, or
1160 powerkvm chassis:
1161
1162@@ -1242,6 +1299,19 @@
1163 chassis_type, content_type=(
1164 "text/plain; charset=%s" % settings.DEFAULT_CHARSET))
1165
1166+ # If given a domain make sure it exists first
1167+ domain_name = get_optional_param(request.POST, 'domain')
1168+ if domain_name is not None:
1169+ try:
1170+ domain = Domain.objects.get(id=int(domain_name))
1171+ except ValueError:
1172+ try:
1173+ domain = Domain.objects.get(name=domain_name)
1174+ except Domain.DoesNotExist:
1175+ return HttpResponseNotFound(
1176+ "Unable to find specified domain %s" % domain_name)
1177+ domain_name = domain.name
1178+
1179 rack_controller = get_optional_param(request.POST, 'rack_controller')
1180 if rack_controller is None:
1181 rack = RackController.objects.get_accessible_by_url(hostname)
1182@@ -1262,7 +1332,8 @@
1183
1184 rack.add_chassis(
1185 request.user.username, chassis_type, hostname, username, password,
1186- accept_all, prefix_filter, power_control, port, protocol)
1187+ accept_all, domain_name, prefix_filter, power_control, port,
1188+ protocol)
1189
1190 return HttpResponse(
1191 "Asking %s to add machines from chassis %s" % (
1192
1193=== modified file 'src/maasserver/api/networks.py'
1194--- src/maasserver/api/networks.py 2016-02-17 16:12:30 +0000
1195+++ src/maasserver/api/networks.py 2016-03-02 18:21:51 +0000
1196@@ -1,4 +1,4 @@
1197-# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
1198+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
1199 # GNU Affero General Public License version 3 (see the file LICENSE).
1200
1201 """API handlers: `Network`."""
1202
1203=== modified file 'src/maasserver/api/nodes.py'
1204--- src/maasserver/api/nodes.py 2016-02-22 18:49:58 +0000
1205+++ src/maasserver/api/nodes.py 2016-03-02 18:21:51 +0000
1206@@ -136,15 +136,10 @@
1207 """
1208 api_doc_section_name = "Node"
1209
1210- create = None # Disable create.
1211+ # Disable create and update
1212+ create = update = None
1213 model = Node
1214
1215- # Override the 'hostname' field so that it returns the FQDN instead as
1216- # this is used by Juju to reach that node.
1217- @classmethod
1218- def hostname(handler, node):
1219- return node.fqdn
1220-
1221 # Override 'owner' so it emits the owner's name rather than a
1222 # full nested user object.
1223 @classmethod
1224@@ -315,7 +310,37 @@
1225 base_model = Node
1226
1227 def read(self, request):
1228- """List all nodes."""
1229+ """List Nodes visible to the user, optionally filtered by criteria.
1230+
1231+ Nodes are sorted by id (i.e. most recent last) and grouped by type.
1232+
1233+ :param hostname: An optional hostname. Only nodes relating to the node
1234+ with the matching hostname will be returned. This can be specified
1235+ multiple times to see multiple nodes.
1236+ :type hostname: unicode
1237+
1238+ :param mac_address: An optional MAC address. Only nodes relating to the
1239+ node owning the specified MAC address will be returned. This can be
1240+ specified multiple times to see multiple nodes.
1241+ :type mac_address: unicode
1242+
1243+ :param id: An optional list of system ids. Only nodes relating to the
1244+ nodes with matching system ids will be returned.
1245+ :type id: unicode
1246+
1247+ :param domain: An optional name for a dns domain. Only nodes relating
1248+ to the nodes in the domain will be returned.
1249+ :type domain: unicode
1250+
1251+ :param zone: An optional name for a physical zone. Only nodes relating
1252+ to the nodes in the zone will be returned.
1253+ :type zone: unicode
1254+
1255+ :param agent_name: An optional agent name. Only nodes relating to the
1256+ nodes with matching agent names will be returned.
1257+ :type agent_name: unicode
1258+ """
1259+
1260 if self.base_model == Node:
1261 # Avoid circular dependencies
1262 from maasserver.api.devices import DevicesHandler
1263
1264=== modified file 'src/maasserver/api/not_found.py'
1265--- src/maasserver/api/not_found.py 2015-12-01 18:12:59 +0000
1266+++ src/maasserver/api/not_found.py 2016-03-02 18:21:51 +0000
1267@@ -1,4 +1,4 @@
1268-# Copyright 2015 Canonical Ltd. This software is licensed under the
1269+# Copyright 2016 Canonical Ltd. This software is licensed under the
1270 # GNU Affero General Public License version 3 (see the file LICENSE).
1271
1272 """Not found API handler."""
1273
1274=== modified file 'src/maasserver/api/pxeconfig.py'
1275--- src/maasserver/api/pxeconfig.py 2016-02-11 15:06:36 +0000
1276+++ src/maasserver/api/pxeconfig.py 2016-03-02 18:21:51 +0000
1277@@ -1,4 +1,4 @@
1278-# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
1279+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
1280 # GNU Affero General Public License version 3 (see the file LICENSE).
1281
1282 """API handler: `pxeconfig`."""
1283
1284=== modified file 'src/maasserver/api/rackcontrollers.py'
1285--- src/maasserver/api/rackcontrollers.py 2016-02-19 08:38:36 +0000
1286+++ src/maasserver/api/rackcontrollers.py 2016-03-02 18:21:51 +0000
1287@@ -1,4 +1,4 @@
1288-# Copyright 2012-2016 Canonical Ltd. This software is licensed under the
1289+# Copyright 2016 Canonical Ltd. This software is licensed under the
1290 # GNU Affero General Public License version 3 (see the file LICENSE).
1291
1292 __all__ = [
1293@@ -8,9 +8,9 @@
1294
1295 from django.conf import settings
1296 from django.http import HttpResponse
1297-from maasserver.api.machines import (
1298- MachineHandler,
1299- MachinesHandler,
1300+from maasserver.api.nodes import (
1301+ NodeHandler,
1302+ NodesHandler,
1303 )
1304 from maasserver.api.support import (
1305 admin_method,
1306@@ -27,15 +27,14 @@
1307 DISPLAYED_RACK_CONTROLLER_FIELDS = (
1308 'system_id',
1309 'hostname',
1310+ 'domain',
1311+ 'fqdn',
1312 'architecture',
1313 'cpu_count',
1314 'memory',
1315 'swap_size',
1316- 'status',
1317 'osystem',
1318 'distro_series',
1319- 'power_type',
1320- 'power_state',
1321 'ip_addresses',
1322 ('interface_set', (
1323 'id',
1324@@ -54,14 +53,12 @@
1325 )),
1326 'zone',
1327 'status_action',
1328- 'status_message',
1329- 'status_name',
1330 'node_type',
1331 'node_type_name',
1332 )
1333
1334
1335-class RackControllerHandler(MachineHandler):
1336+class RackControllerHandler(NodeHandler):
1337 """Manage an individual rack controller.
1338
1339 The rack controller is identified by its system_id.
1340@@ -75,7 +72,7 @@
1341 def refresh(self, request, system_id):
1342 """Refresh the hardware information for a specific rack controller.
1343
1344- Returns 404 if the node is not found.
1345+ Returns 404 if the rack-controller is not found.
1346 Returns 403 if the user does not have permission to refresh the rack.
1347 """
1348 rack = self.model.objects.get_node_or_404(
1349@@ -110,7 +107,7 @@
1350 return ('rackcontroller_handler', (rackcontroller_id, ))
1351
1352
1353-class RackControllersHandler(MachinesHandler):
1354+class RackControllersHandler(NodesHandler):
1355 """Manage the collection of all rack controllers in MAAS."""
1356 api_doc_section_name = "RackControllers"
1357 base_model = RackController
1358
1359=== modified file 'src/maasserver/api/raid.py'
1360--- src/maasserver/api/raid.py 2015-12-16 00:01:56 +0000
1361+++ src/maasserver/api/raid.py 2016-03-02 18:21:51 +0000
1362@@ -1,4 +1,4 @@
1363-# Copyright 2015 Canonical Ltd. This software is licensed under the
1364+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
1365 # GNU Affero General Public License version 3 (see the file LICENSE).
1366
1367 """API handlers: `RAID`."""
1368@@ -39,7 +39,7 @@
1369
1370
1371 class RaidsHandler(OperationsHandler):
1372- """Manage all RAID devices on a node."""
1373+ """Manage all RAID devices on a machine."""
1374 api_doc_section_name = "RAID Devices"
1375 update = delete = None
1376 fields = DISPLAYED_RAID_FIELDS
1377@@ -67,7 +67,7 @@
1378 system_id, request.user, NODE_PERMISSION.ADMIN)
1379 if machine.status != NODE_STATUS.READY:
1380 raise NodeStateViolation(
1381- "Cannot create RAID because the node is not Ready.")
1382+ "Cannot create RAID because the machine is not Ready.")
1383 form = CreateRaidForm(machine, data=request.data)
1384 if form.is_valid():
1385 return form.save()
1386@@ -75,7 +75,7 @@
1387 raise MAASAPIValidationError(form.errors)
1388
1389 def read(self, request, system_id):
1390- """List all RAID devices belonging to node.
1391+ """List all RAID devices belonging to a machine.
1392
1393 Returns 404 if the machine is not found.
1394 """
1395@@ -85,7 +85,7 @@
1396
1397
1398 class RaidHandler(OperationsHandler):
1399- """Manage a specific RAID device on a node."""
1400+ """Manage a specific RAID device on a machine."""
1401 api_doc_section_name = "RAID Device"
1402 create = None
1403 model = RAID
1404@@ -142,15 +142,15 @@
1405 ]
1406
1407 def read(self, request, system_id, raid_id):
1408- """Read RAID device on node.
1409+ """Read RAID device on a machine.
1410
1411- Returns 404 if the node or RAID is not found.
1412+ Returns 404 if the machine or RAID is not found.
1413 """
1414 return RAID.objects.get_object_or_404(
1415 system_id, raid_id, request.user, NODE_PERMISSION.VIEW)
1416
1417 def update(self, request, system_id, raid_id):
1418- """Update RAID on node.
1419+ """Update RAID on a machine.
1420
1421 :param name: Name of the RAID.
1422 :param uuid: UUID of the RAID.
1423@@ -165,15 +165,15 @@
1424 :param remove_spare_partitions: Spare partitions to remove from the
1425 RAID.
1426
1427- Returns 404 if the node or RAID is not found.
1428- Returns 409 if the node is not Ready.
1429+ Returns 404 if the machine or RAID is not found.
1430+ Returns 409 if the machine is not Ready.
1431 """
1432 raid = RAID.objects.get_object_or_404(
1433 system_id, raid_id, request.user, NODE_PERMISSION.ADMIN)
1434 node = raid.get_node()
1435 if node.status != NODE_STATUS.READY:
1436 raise NodeStateViolation(
1437- "Cannot update RAID because the node is not Ready.")
1438+ "Cannot update RAID because the machine is not Ready.")
1439 form = UpdateRaidForm(raid, data=request.data)
1440 if form.is_valid():
1441 return form.save()
1442@@ -181,16 +181,16 @@
1443 raise MAASAPIValidationError(form.errors)
1444
1445 def delete(self, request, system_id, raid_id):
1446- """Delete RAID on node.
1447+ """Delete RAID on a machine.
1448
1449- Returns 404 if the node or RAID is not found.
1450- Returns 409 if the node is not Ready.
1451+ Returns 404 if the machine or RAID is not found.
1452+ Returns 409 if the machine is not Ready.
1453 """
1454 raid = RAID.objects.get_object_or_404(
1455 system_id, raid_id, request.user, NODE_PERMISSION.ADMIN)
1456 node = raid.get_node()
1457 if node.status != NODE_STATUS.READY:
1458 raise NodeStateViolation(
1459- "Cannot delete RAID because the node is not Ready.")
1460+ "Cannot delete RAID because the machine is not Ready.")
1461 raid.delete()
1462 return rc.DELETED
1463
1464=== modified file 'src/maasserver/api/spaces.py'
1465--- src/maasserver/api/spaces.py 2015-12-01 18:12:59 +0000
1466+++ src/maasserver/api/spaces.py 2016-03-02 18:21:51 +0000
1467@@ -1,4 +1,4 @@
1468-# Copyright 2015 Canonical Ltd. This software is licensed under the
1469+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
1470 # GNU Affero General Public License version 3 (see the file LICENSE).
1471
1472 """API handlers: `Space`."""
1473
1474=== modified file 'src/maasserver/api/ssh_keys.py'
1475--- src/maasserver/api/ssh_keys.py 2016-02-19 08:38:36 +0000
1476+++ src/maasserver/api/ssh_keys.py 2016-03-02 18:21:51 +0000
1477@@ -1,4 +1,4 @@
1478-# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
1479+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
1480 # GNU Affero General Public License version 3 (see the file LICENSE).
1481
1482 """API handlers: `SSHKey`."""
1483
1484=== modified file 'src/maasserver/api/ssl_keys.py'
1485--- src/maasserver/api/ssl_keys.py 2016-02-19 08:38:36 +0000
1486+++ src/maasserver/api/ssl_keys.py 2016-03-02 18:21:51 +0000
1487@@ -1,4 +1,4 @@
1488-# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
1489+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
1490 # GNU Affero General Public License version 3 (see the file LICENSE).
1491
1492 """API handlers: `SSLKey`."""
1493
1494=== modified file 'src/maasserver/api/subnets.py'
1495--- src/maasserver/api/subnets.py 2016-01-29 17:32:25 +0000
1496+++ src/maasserver/api/subnets.py 2016-03-02 18:21:51 +0000
1497@@ -1,4 +1,4 @@
1498-# Copyright 2015 Canonical Ltd. This software is licensed under the
1499+# Copyright 2016 Canonical Ltd. This software is licensed under the
1500 # GNU Affero General Public License version 3 (see the file LICENSE).
1501
1502 """API handlers: `Subnet`."""
1503
1504=== modified file 'src/maasserver/api/support.py'
1505--- src/maasserver/api/support.py 2016-02-25 08:52:39 +0000
1506+++ src/maasserver/api/support.py 2016-03-02 18:21:51 +0000
1507@@ -1,4 +1,4 @@
1508-# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
1509+# Copyright 2012-2016 Canonical Ltd. This software is licensed under the
1510 # GNU Affero General Public License version 3 (see the file LICENSE).
1511
1512 """Supporting infrastructure for Piston-based APIs in MAAS."""
1513
1514=== modified file 'src/maasserver/api/tags.py'
1515--- src/maasserver/api/tags.py 2016-02-19 08:38:36 +0000
1516+++ src/maasserver/api/tags.py 2016-03-02 18:21:51 +0000
1517@@ -1,4 +1,4 @@
1518-# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
1519+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
1520 # GNU Affero General Public License version 3 (see the file LICENSE).
1521
1522 """API handlers: `Tag`."""
1523
1524=== modified file 'src/maasserver/api/tests/test_devices.py'
1525--- src/maasserver/api/tests/test_devices.py 2016-02-26 18:39:26 +0000
1526+++ src/maasserver/api/tests/test_devices.py 2016-03-02 18:21:51 +0000
1527@@ -13,7 +13,10 @@
1528 NODE_STATUS,
1529 NODE_TYPE,
1530 )
1531-from maasserver.models import Device
1532+from maasserver.models import (
1533+ Device,
1534+ Domain,
1535+)
1536 from maasserver.testing.api import APITestCase
1537 from maasserver.testing.factory import factory
1538 from maasserver.utils.converters import json_load_bytes
1539@@ -72,6 +75,48 @@
1540 self.assertEquals(parent, device.parent)
1541 self.assertEqual(device.node_type, NODE_TYPE.DEVICE)
1542
1543+ def test_POST_creates_device_with_default_domain(self):
1544+ hostname = factory.make_name('host')
1545+ macs = {
1546+ factory.make_mac_address()
1547+ for _ in range(random.randint(1, 2))
1548+ }
1549+ response = self.client.post(
1550+ reverse('devices_handler'),
1551+ {
1552+ 'hostname': hostname,
1553+ 'mac_addresses': macs,
1554+ })
1555+ self.assertEqual(
1556+ http.client.OK, response.status_code, response.content)
1557+ system_id = json_load_bytes(response.content)['system_id']
1558+ device = Device.objects.get(system_id=system_id)
1559+ self.assertEquals(hostname, device.hostname)
1560+ self.assertEquals(Domain.objects.get_default_domain(), device.domain)
1561+ self.assertEqual(device.node_type, NODE_TYPE.DEVICE)
1562+
1563+ def test_POST_creates_device_with_domain(self):
1564+ hostname = factory.make_name('host')
1565+ domain = factory.make_Domain()
1566+ macs = {
1567+ factory.make_mac_address()
1568+ for _ in range(random.randint(1, 2))
1569+ }
1570+ response = self.client.post(
1571+ reverse('devices_handler'),
1572+ {
1573+ 'hostname': hostname,
1574+ 'mac_addresses': macs,
1575+ 'domain': domain.name,
1576+ })
1577+ self.assertEqual(
1578+ http.client.OK, response.status_code, response.content)
1579+ system_id = json_load_bytes(response.content)['system_id']
1580+ device = Device.objects.get(system_id=system_id)
1581+ self.assertEquals(hostname, device.hostname)
1582+ self.assertEquals(domain, device.domain)
1583+ self.assertEqual(device.node_type, NODE_TYPE.DEVICE)
1584+
1585 def test_POST_returns_limited_fields(self):
1586 response = self.client.post(
1587 reverse('devices_handler'),
1588@@ -83,6 +128,8 @@
1589 self.assertItemsEqual(
1590 [
1591 'hostname',
1592+ 'domain',
1593+ 'fqdn',
1594 'owner',
1595 'system_id',
1596 'macaddress_set',
1597@@ -162,6 +209,8 @@
1598 self.assertItemsEqual(
1599 [
1600 'hostname',
1601+ 'domain',
1602+ 'fqdn',
1603 'owner',
1604 'system_id',
1605 'macaddress_set',
1606@@ -195,7 +244,7 @@
1607
1608 response = self.client.post(get_device_uri(device))
1609 self.assertEqual(
1610- http.client.METHOD_NOT_ALLOWED, response.status_code,
1611+ http.client.BAD_REQUEST, response.status_code,
1612 response.content)
1613
1614 def test_GET_reads_device(self):
1615
1616=== modified file 'src/maasserver/api/tests/test_doc.py'
1617--- src/maasserver/api/tests/test_doc.py 2016-02-02 14:20:45 +0000
1618+++ src/maasserver/api/tests/test_doc.py 2016-03-02 18:21:51 +0000
1619@@ -5,7 +5,10 @@
1620
1621 __all__ = []
1622
1623+import http.client
1624 from inspect import getdoc
1625+from io import StringIO
1626+import sys
1627 import types
1628
1629 from django.conf.urls import (
1630@@ -74,6 +77,14 @@
1631 name = factory.make_name("module")
1632 return types.ModuleType(name)
1633
1634+ def test_anon_api_doc(self):
1635+ # The documentation is accessible to anon users.
1636+ self.patch(sys, "stderr", StringIO())
1637+ response = self.client.get(reverse('api-doc'))
1638+ self.assertEqual(http.client.OK, response.status_code)
1639+ # No error or warning are emitted by docutils.
1640+ self.assertEqual("", sys.stderr.getvalue())
1641+
1642 def test_urlpatterns_empty(self):
1643 # No resources are found in empty modules.
1644 module = self.make_module()
1645
1646=== modified file 'src/maasserver/api/tests/test_enlistment.py'
1647--- src/maasserver/api/tests/test_enlistment.py 2016-03-01 01:15:22 +0000
1648+++ src/maasserver/api/tests/test_enlistment.py 2016-03-02 18:21:51 +0000
1649@@ -64,7 +64,7 @@
1650 self.assertIn('application/json', response['Content-Type'])
1651 domain_name = Domain.objects.get_default_domain().name
1652 self.assertEqual(
1653- 'diane.%s' % domain_name, parsed_result['hostname'])
1654+ 'diane.%s' % domain_name, parsed_result['fqdn'])
1655 self.assertNotEqual(0, len(parsed_result.get('system_id')))
1656 [diane] = Machine.objects.filter(hostname='diane')
1657 self.assertEqual(architecture, diane.architecture)
1658@@ -129,7 +129,7 @@
1659 self.assertIn('application/json', response['Content-Type'])
1660 domain_name = Domain.objects.get_default_domain().name
1661 self.assertEqual(
1662- 'diane.%s' % domain_name, parsed_result['hostname'])
1663+ 'diane.%s' % domain_name, parsed_result['fqdn'])
1664 self.assertNotEqual(0, len(parsed_result.get('system_id')))
1665 [diane] = Machine.objects.filter(hostname='diane')
1666 self.assertEqual(architecture, diane.architecture)
1667@@ -152,7 +152,7 @@
1668 self.assertIn('application/json', response['Content-Type'])
1669 domain_name = Domain.objects.get_default_domain().name
1670 self.assertEqual(
1671- 'diane.%s' % domain_name, parsed_result['hostname'])
1672+ 'diane.%s' % domain_name, parsed_result['fqdn'])
1673 self.assertNotEqual(0, len(parsed_result.get('system_id')))
1674 [diane] = Machine.objects.filter(hostname='diane')
1675 self.assertEqual(architecture, diane.architecture)
1676@@ -275,6 +275,30 @@
1677 self.assertItemsEqual(
1678 ['architecture'], parsed_result, response.content)
1679
1680+ def test_POST_create_creates_machine_with_domain(self):
1681+ domain = factory.make_Domain()
1682+ # The API allows a Machine to be created.
1683+ architecture = make_usable_architecture(self)
1684+ response = self.client.post(
1685+ reverse('machines_handler'),
1686+ {
1687+ 'hostname': 'diane',
1688+ 'architecture': architecture.split('/')[0],
1689+ 'subarchitecture': architecture.split('/')[1],
1690+ 'power_type': 'manual',
1691+ 'mac_addresses': ['aa:bb:cc:dd:ee:ff', '22:bb:cc:dd:ee:ff'],
1692+ 'domain': domain.name,
1693+ })
1694+
1695+ self.assertEqual(http.client.OK, response.status_code)
1696+ parsed_result = json_load_bytes(response.content)
1697+ self.assertIn('application/json', response['Content-Type'])
1698+ self.assertEqual(
1699+ 'diane.%s' % domain.name, parsed_result['fqdn'])
1700+ self.assertNotEqual(0, len(parsed_result.get('system_id')))
1701+ [diane] = Machine.objects.filter(hostname='diane')
1702+ self.assertEqual(architecture, diane.architecture)
1703+
1704
1705 class MachineHostnameEnlistmentTest(
1706 MultipleUsersScenarios, MAASServerTestCase):
1707@@ -307,7 +331,7 @@
1708 hostname_without_domain,
1709 Domain.objects.get_default_domain().name)
1710 self.assertEqual(
1711- expected_hostname, parsed_result.get('hostname'))
1712+ expected_hostname, parsed_result.get('fqdn'))
1713
1714
1715 class NonAdminEnlistmentAPITest(
1716@@ -376,6 +400,8 @@
1717 self.assertItemsEqual(
1718 [
1719 'hostname',
1720+ 'domain',
1721+ 'fqdn',
1722 'owner',
1723 'system_id',
1724 'architecture',
1725@@ -482,6 +508,8 @@
1726 self.assertItemsEqual(
1727 [
1728 'hostname',
1729+ 'domain',
1730+ 'fqdn',
1731 'owner',
1732 'system_id',
1733 'macaddress_set',
1734@@ -644,6 +672,8 @@
1735 self.assertItemsEqual(
1736 [
1737 'hostname',
1738+ 'domain',
1739+ 'fqdn',
1740 'owner',
1741 'system_id',
1742 'macaddress_set',
1743
1744=== modified file 'src/maasserver/api/tests/test_machine.py'
1745--- src/maasserver/api/tests/test_machine.py 2016-02-29 07:45:25 +0000
1746+++ src/maasserver/api/tests/test_machine.py 2016-03-02 18:21:51 +0000
1747@@ -7,10 +7,7 @@
1748
1749 from base64 import b64encode
1750 import http.client
1751-from io import StringIO
1752-import sys
1753
1754-import bson
1755 from django.conf import settings
1756 from django.core.urlresolvers import reverse
1757 from django.db import transaction
1758@@ -20,7 +17,6 @@
1759 INTERFACE_TYPE,
1760 IPADDRESS_TYPE,
1761 NODE_STATUS,
1762- NODE_STATUS_CHOICES,
1763 NODE_STATUS_CHOICES_DICT,
1764 NODE_TYPE,
1765 NODE_TYPE_CHOICES,
1766@@ -66,10 +62,6 @@
1767 from metadataserver.nodeinituser import get_node_init_user
1768 from mock import ANY
1769 from netaddr import IPNetwork
1770-from provisioningserver.refresh.node_info_scripts import (
1771- LLDP_OUTPUT_NAME,
1772- LSHW_OUTPUT_NAME,
1773-)
1774 from provisioningserver.rpc.exceptions import PowerActionAlreadyInProgress
1775 from provisioningserver.utils.enum import map_enum
1776 from twisted.internet import defer
1777@@ -78,19 +70,6 @@
1778
1779 class MachineAnonAPITest(MAASServerTestCase):
1780
1781- def setUp(self):
1782- super(MachineAnonAPITest, self).setUp()
1783- self.patch(node_module.Node, '_start')
1784- self.patch(node_module.Node, '_stop')
1785-
1786- def test_anon_api_doc(self):
1787- # The documentation is accessible to anon users.
1788- self.patch(sys, "stderr", StringIO())
1789- response = self.client.get(reverse('api-doc'))
1790- self.assertEqual(http.client.OK, response.status_code)
1791- # No error or warning are emitted by docutils.
1792- self.assertEqual("", sys.stderr.getvalue())
1793-
1794 def test_machine_init_user_cannot_access(self):
1795 token = NodeKey.objects.get_token_for_node(factory.make_Node())
1796 client = OAuthAuthenticatedClient(get_node_init_user(), token)
1797@@ -147,7 +126,7 @@
1798 domain_name = Domain.objects.get_default_domain().name
1799 self.assertEqual(
1800 "%s.%s" % (machine.hostname, domain_name),
1801- parsed_result['hostname'])
1802+ parsed_result['fqdn'])
1803 self.assertEqual(machine.system_id, parsed_result['system_id'])
1804
1805 def test_GET_returns_associated_tag(self):
1806@@ -924,7 +903,7 @@
1807 self.assertEqual(http.client.OK, response.status_code)
1808 domain_name = Domain.objects.get_default_domain().name
1809 self.assertEqual(
1810- 'francis.%s' % domain_name, parsed_result['hostname'])
1811+ 'francis.%s' % domain_name, parsed_result['fqdn'])
1812 self.assertEqual(0, Machine.objects.filter(hostname='diane').count())
1813 self.assertEqual(1, Machine.objects.filter(hostname='francis').count())
1814
1815@@ -1480,188 +1459,6 @@
1816 response.content)
1817
1818
1819-class TestGetDetails(APITestCase):
1820- """Tests for /api/2.0/machines/<machine>/?op=details."""
1821-
1822- def make_lshw_result(self, machine, script_result=0):
1823- return factory.make_NodeResult_for_commissioning(
1824- node=machine, name=LSHW_OUTPUT_NAME,
1825- script_result=script_result)
1826-
1827- def make_lldp_result(self, machine, script_result=0):
1828- return factory.make_NodeResult_for_commissioning(
1829- node=machine, name=LLDP_OUTPUT_NAME, script_result=script_result)
1830-
1831- def get_details(self, machine):
1832- url = reverse('machine_handler', args=[machine.system_id])
1833- response = self.client.get(url, {'op': 'details'})
1834- self.assertEqual(http.client.OK, response.status_code)
1835- self.assertEqual('application/bson', response['content-type'])
1836- return bson.BSON(response.content).decode()
1837-
1838- def test_GET_returns_empty_details_when_there_are_none(self):
1839- machine = factory.make_Node()
1840- self.assertDictEqual(
1841- {"lshw": None, "lldp": None},
1842- self.get_details(machine))
1843-
1844- def test_GET_returns_all_details(self):
1845- machine = factory.make_Node()
1846- lshw_result = self.make_lshw_result(machine)
1847- lldp_result = self.make_lldp_result(machine)
1848- self.assertDictEqual(
1849- {"lshw": lshw_result.data,
1850- "lldp": lldp_result.data},
1851- self.get_details(machine))
1852-
1853- def test_GET_returns_only_those_details_that_exist(self):
1854- machine = factory.make_Node()
1855- lshw_result = self.make_lshw_result(machine)
1856- self.assertDictEqual(
1857- {"lshw": lshw_result.data,
1858- "lldp": None},
1859- self.get_details(machine))
1860-
1861- def test_GET_returns_not_found_when_machine_does_not_exist(self):
1862- url = reverse('machine_handler', args=['does-not-exist'])
1863- response = self.client.get(url, {'op': 'details'})
1864- self.assertEqual(http.client.NOT_FOUND, response.status_code)
1865-
1866-
1867-class TestMarkBroken(APITestCase):
1868- """Tests for /api/2.0/machines/<machine>/?op=mark_broken"""
1869-
1870- def get_machine_uri(self, machine):
1871- """Get the API URI for `machine`."""
1872- return reverse('machine_handler', args=[machine.system_id])
1873-
1874- def test_mark_broken_changes_status(self):
1875- machine = factory.make_Node(
1876- status=NODE_STATUS.COMMISSIONING, owner=self.logged_in_user)
1877- response = self.client.post(
1878- self.get_machine_uri(machine), {'op': 'mark_broken'})
1879- self.assertEqual(http.client.OK, response.status_code)
1880- self.assertEqual(NODE_STATUS.BROKEN, reload_object(machine).status)
1881-
1882- def test_mark_broken_updates_error_description(self):
1883- # 'error_description' parameter was renamed 'comment' for consistency
1884- # make sure this comment updates the machine's error_description
1885- machine = factory.make_Node(
1886- status=NODE_STATUS.COMMISSIONING, owner=self.logged_in_user)
1887- comment = factory.make_name('comment')
1888- response = self.client.post(
1889- self.get_machine_uri(machine),
1890- {'op': 'mark_broken', 'comment': comment})
1891- self.assertEqual(http.client.OK, response.status_code)
1892- machine = reload_object(machine)
1893- self.assertEqual(
1894- (NODE_STATUS.BROKEN, comment),
1895- (machine.status, machine.error_description)
1896- )
1897-
1898- def test_mark_broken_updates_error_description_compatibility(self):
1899- # test old 'error_description' parameter is honored for compatibility
1900- machine = factory.make_Node(
1901- status=NODE_STATUS.COMMISSIONING, owner=self.logged_in_user)
1902- error_description = factory.make_name('error_description')
1903- response = self.client.post(
1904- self.get_machine_uri(machine),
1905- {'op': 'mark_broken', 'error_description': error_description})
1906- self.assertEqual(http.client.OK, response.status_code)
1907- machine = reload_object(machine)
1908- self.assertEqual(
1909- (NODE_STATUS.BROKEN, error_description),
1910- (machine.status, machine.error_description)
1911- )
1912-
1913- def test_mark_broken_passes_comment(self):
1914- machine = factory.make_Node(
1915- status=NODE_STATUS.COMMISSIONING, owner=self.logged_in_user)
1916- machine_mark_broken = self.patch(node_module.Machine, 'mark_broken')
1917- comment = factory.make_name('comment')
1918- self.client.post(
1919- self.get_machine_uri(machine),
1920- {'op': 'mark_broken', 'comment': comment})
1921- self.assertThat(
1922- machine_mark_broken,
1923- MockCalledOnceWith(self.logged_in_user, comment))
1924-
1925- def test_mark_broken_handles_missing_comment(self):
1926- machine = factory.make_Node(
1927- status=NODE_STATUS.COMMISSIONING, owner=self.logged_in_user)
1928- machine_mark_broken = self.patch(node_module.Machine, 'mark_broken')
1929- self.client.post(
1930- self.get_machine_uri(machine), {'op': 'mark_broken'})
1931- self.assertThat(
1932- machine_mark_broken,
1933- MockCalledOnceWith(self.logged_in_user, None))
1934-
1935- def test_mark_broken_requires_ownership(self):
1936- machine = factory.make_Node(status=NODE_STATUS.COMMISSIONING)
1937- response = self.client.post(
1938- self.get_machine_uri(machine), {'op': 'mark_broken'})
1939- self.assertEqual(http.client.FORBIDDEN, response.status_code)
1940-
1941- def test_mark_broken_allowed_from_any_other_state(self):
1942- self.patch(node_module.Node, "_stop")
1943- for status, _ in NODE_STATUS_CHOICES:
1944- if status == NODE_STATUS.BROKEN:
1945- continue
1946-
1947- machine = factory.make_Node(
1948- status=status, owner=self.logged_in_user)
1949- response = self.client.post(
1950- self.get_machine_uri(machine), {'op': 'mark_broken'})
1951- self.expectThat(
1952- response.status_code, Equals(http.client.OK), response)
1953- machine = reload_object(machine)
1954- self.expectThat(machine.status, Equals(NODE_STATUS.BROKEN))
1955-
1956-
1957-class TestMarkFixed(APITestCase):
1958- """Tests for /api/2.0/machines/<machine>/?op=mark_fixed"""
1959-
1960- def get_machine_uri(self, machine):
1961- """Get the API URI for `machine`."""
1962- return reverse('machine_handler', args=[machine.system_id])
1963-
1964- def test_mark_fixed_changes_status(self):
1965- self.become_admin()
1966- machine = factory.make_Node(status=NODE_STATUS.BROKEN)
1967- response = self.client.post(
1968- self.get_machine_uri(machine), {'op': 'mark_fixed'})
1969- self.assertEqual(http.client.OK, response.status_code)
1970- self.assertEqual(NODE_STATUS.READY, reload_object(machine).status)
1971-
1972- def test_mark_fixed_requires_admin(self):
1973- machine = factory.make_Node(status=NODE_STATUS.BROKEN)
1974- response = self.client.post(
1975- self.get_machine_uri(machine), {'op': 'mark_fixed'})
1976- self.assertEqual(http.client.FORBIDDEN, response.status_code)
1977-
1978- def test_mark_fixed_passes_comment(self):
1979- self.become_admin()
1980- machine = factory.make_Node(status=NODE_STATUS.BROKEN)
1981- machine_mark_fixed = self.patch(node_module.Machine, 'mark_fixed')
1982- comment = factory.make_name('comment')
1983- self.client.post(
1984- self.get_machine_uri(machine),
1985- {'op': 'mark_fixed', 'comment': comment})
1986- self.assertThat(
1987- machine_mark_fixed,
1988- MockCalledOnceWith(self.logged_in_user, comment))
1989-
1990- def test_mark_fixed_handles_missing_comment(self):
1991- self.become_admin()
1992- machine = factory.make_Node(status=NODE_STATUS.BROKEN)
1993- machine_mark_fixed = self.patch(node_module.Machine, 'mark_fixed')
1994- self.client.post(
1995- self.get_machine_uri(machine), {'op': 'mark_fixed'})
1996- self.assertThat(
1997- machine_mark_fixed,
1998- MockCalledOnceWith(self.logged_in_user, None))
1999-
2000-
2001 class TestPowerParameters(APITestCase):
2002 def get_machine_uri(self, machine):
2003 """Get the API URI for `machine`."""
2004
2005=== modified file 'src/maasserver/api/tests/test_machines.py'
2006--- src/maasserver/api/tests/test_machines.py 2016-02-29 07:45:25 +0000
2007+++ src/maasserver/api/tests/test_machines.py 2016-03-02 18:21:51 +0000
2008@@ -107,57 +107,15 @@
2009 name=domainname, defaults={'authoritative': True})
2010 factory.make_Node(
2011 hostname=hostname, domain=domain)
2012- expected_hostname = "%s.%s" % (hostname, domainname)
2013+ fqdn = "%s.%s" % (hostname, domainname)
2014 response = self.client.get(reverse('machines_handler'))
2015 self.assertEqual(
2016 http.client.OK.value, response.status_code, response.content)
2017 parsed_result = json.loads(
2018 response.content.decode(settings.DEFAULT_CHARSET))
2019 self.assertItemsEqual(
2020- [expected_hostname],
2021- [machine.get('hostname') for machine in parsed_result])
2022-
2023-
2024-class AnonymousIsRegisteredAPITest(MAASServerTestCase):
2025-
2026- def test_is_registered_returns_True_if_machine_registered(self):
2027- mac_address = factory.make_mac_address()
2028- factory.make_Interface(
2029- INTERFACE_TYPE.PHYSICAL, mac_address=mac_address)
2030- response = self.client.get(
2031- reverse('machines_handler'),
2032- {'op': 'is_registered', 'mac_address': mac_address})
2033- self.assertEqual(
2034- (http.client.OK.value, "true"),
2035- (response.status_code,
2036- response.content.decode(settings.DEFAULT_CHARSET)))
2037-
2038- def test_is_registered_normalizes_mac_address(self):
2039- # These two non-normalized MAC addresses are the same.
2040- non_normalized_mac_address = 'AA-bb-cc-dd-ee-ff'
2041- non_normalized_mac_address2 = 'aabbccddeeff'
2042- factory.make_Interface(
2043- INTERFACE_TYPE.PHYSICAL, mac_address=non_normalized_mac_address)
2044- response = self.client.get(
2045- reverse('machines_handler'),
2046- {
2047- 'op': 'is_registered',
2048- 'mac_address': non_normalized_mac_address2
2049- })
2050- self.assertEqual(
2051- (http.client.OK.value, "true"),
2052- (response.status_code,
2053- response.content.decode(settings.DEFAULT_CHARSET)))
2054-
2055- def test_is_registered_returns_False_if_machine_not_registered(self):
2056- mac_address = factory.make_mac_address()
2057- response = self.client.get(
2058- reverse('machines_handler'),
2059- {'op': 'is_registered', 'mac_address': mac_address})
2060- self.assertEqual(
2061- (http.client.OK.value, "false"),
2062- (response.status_code,
2063- response.content.decode(settings.DEFAULT_CHARSET)))
2064+ [fqdn],
2065+ [machine.get('fqdn') for machine in parsed_result])
2066
2067
2068 def extract_system_ids(parsed_result):
2069@@ -649,7 +607,7 @@
2070 domain_name = desired_machine.domain.name
2071 self.assertEqual(
2072 "%s.%s" % (desired_machine.hostname, domain_name),
2073- parsed_result['hostname'])
2074+ parsed_result['fqdn'])
2075
2076 def test_POST_allocate_would_rather_fail_than_disobey_constraint(self):
2077 # If "allocate" is passed a constraint, it won't return a machine
2078@@ -693,7 +651,7 @@
2079 self.assertEqual(
2080 "%s.%s" % (machine.hostname, domain_name),
2081 json.loads(
2082- response.content.decode(settings.DEFAULT_CHARSET))['hostname'])
2083+ response.content.decode(settings.DEFAULT_CHARSET))['fqdn'])
2084
2085 def test_POST_allocate_treats_unknown_name_as_resource_conflict(self):
2086 # A name constraint naming an unknown machine produces a resource
2087@@ -1693,7 +1651,7 @@
2088 self.assertThat(
2089 add_chassis, MockCalledOnceWith(
2090 self.logged_in_user.username, 'virsh', hostname, None, None,
2091- True, None, None, None, None))
2092+ True, None, None, None, None, None))
2093
2094 def test_POST_add_chassis_sends_accept_all_false_when_not_true(self):
2095 self.become_admin()
2096@@ -1716,7 +1674,7 @@
2097 self.assertThat(
2098 add_chassis, MockCalledOnceWith(
2099 self.logged_in_user.username, 'virsh', hostname, None, None,
2100- False, None, None, None, None))
2101+ False, None, None, None, None, None))
2102
2103 def test_POST_add_chassis_sends_prefix_filter(self):
2104 self.become_admin()
2105@@ -1747,8 +1705,8 @@
2106 self.assertThat(
2107 add_chassis, MockCalledWith(
2108 self.logged_in_user.username, chassis_type, hostname,
2109- username, password, False, prefix_filter, None, None, None
2110- ))
2111+ username, password, False, None, prefix_filter, None,
2112+ None, None))
2113
2114 def test_POST_add_chassis_only_allows_prefix_filter_on_virtual_chassis(
2115 self):
2116@@ -1861,7 +1819,7 @@
2117 self.assertThat(
2118 add_chassis, MockCalledWith(
2119 self.logged_in_user.username, chassis_type, hostname,
2120- username, password, False, None, None, port, None))
2121+ username, password, False, None, None, None, port, None))
2122
2123 def test_POST_add_chasis_only_allows_port_with_vmware_and_msftocs(self):
2124 self.become_admin()
2125@@ -1910,7 +1868,7 @@
2126 self.assertThat(
2127 add_chassis, MockCalledWith(
2128 self.logged_in_user.username, 'vmware', hostname, username,
2129- password, False, None, None, None, protocol))
2130+ password, False, None, None, None, None, protocol))
2131
2132 def test_POST_add_chasis_only_allows_protocol_with_vmware(self):
2133 self.become_admin()
2134@@ -1933,6 +1891,71 @@
2135 ("protocol is unavailable with the %s chassis type" %
2136 chassis_type).encode('utf-8'), response.content)
2137
2138+ def test_POST_add_chassis_accept_domain_by_name(self):
2139+ self.become_admin()
2140+ rack = factory.make_RackController()
2141+ accessible_by_url = self.patch(
2142+ machines_module.RackController.objects, 'get_accessible_by_url')
2143+ accessible_by_url.return_value = rack
2144+ add_chassis = self.patch(rack, 'add_chassis')
2145+ hostname = factory.make_url()
2146+ domain = factory.make_Domain()
2147+ response = self.client.post(
2148+ reverse('machines_handler'),
2149+ {
2150+ 'op': 'add_chassis',
2151+ 'chassis_type': 'virsh',
2152+ 'hostname': hostname,
2153+ 'domain': domain.name,
2154+ })
2155+ self.assertEqual(
2156+ http.client.OK, response.status_code, response.content)
2157+ self.assertThat(
2158+ add_chassis, MockCalledWith(
2159+ self.logged_in_user.username, 'virsh', hostname, None,
2160+ None, False, domain.name, None, None, None, None))
2161+
2162+ def test_POST_add_chassis_accept_domain_by_id(self):
2163+ self.become_admin()
2164+ rack = factory.make_RackController()
2165+ accessible_by_url = self.patch(
2166+ machines_module.RackController.objects, 'get_accessible_by_url')
2167+ accessible_by_url.return_value = rack
2168+ add_chassis = self.patch(rack, 'add_chassis')
2169+ hostname = factory.make_url()
2170+ domain = factory.make_Domain()
2171+ response = self.client.post(
2172+ reverse('machines_handler'),
2173+ {
2174+ 'op': 'add_chassis',
2175+ 'chassis_type': 'virsh',
2176+ 'hostname': hostname,
2177+ 'domain': domain.id,
2178+ })
2179+ self.assertEqual(
2180+ http.client.OK, response.status_code, response.content)
2181+ self.assertThat(
2182+ add_chassis, MockCalledWith(
2183+ self.logged_in_user.username, 'virsh', hostname, None,
2184+ None, False, domain.name, None, None, None, None))
2185+
2186+ def test_POST_add_chassis_validates_domain(self):
2187+ self.become_admin()
2188+ domain = factory.make_name('domain')
2189+ response = self.client.post(
2190+ reverse('machines_handler'),
2191+ {
2192+ 'op': 'add_chassis',
2193+ 'chassis_type': 'virsh',
2194+ 'hostname': factory.make_url(),
2195+ 'domain': domain,
2196+ })
2197+ self.assertEqual(
2198+ http.client.NOT_FOUND, response.status_code, response.content)
2199+ self.assertEqual(
2200+ ("Unable to find specified domain %s" % domain).encode('utf-8'),
2201+ response.content)
2202+
2203 def test_POST_add_chassis_accepts_system_id_for_rack_controller(self):
2204 self.become_admin()
2205 subnet = factory.make_Subnet()
2206@@ -1956,7 +1979,7 @@
2207 self.assertThat(
2208 add_chassis, MockCalledWith(
2209 self.logged_in_user.username, 'virsh', hostname, None, None,
2210- False, None, None, None, None))
2211+ False, None, None, None, None, None))
2212
2213 def test_POST_add_chassis_accepts_hostname_for_rack_controller(self):
2214 self.become_admin()
2215@@ -1981,7 +2004,7 @@
2216 self.assertThat(
2217 add_chassis, MockCalledWith(
2218 self.logged_in_user.username, 'virsh', hostname, None, None,
2219- False, None, None, None, None))
2220+ False, None, None, None, None, None))
2221
2222 def test_POST_add_chassis_rejects_invalid_rack_controller(self):
2223 self.become_admin()
2224
2225=== modified file 'src/maasserver/api/tests/test_node.py'
2226--- src/maasserver/api/tests/test_node.py 2016-02-26 18:39:26 +0000
2227+++ src/maasserver/api/tests/test_node.py 2016-03-02 18:21:51 +0000
2228@@ -6,18 +6,13 @@
2229 __all__ = []
2230
2231 import http.client
2232-from io import StringIO
2233-import sys
2234
2235 import bson
2236 from django.conf import settings
2237 from django.core.urlresolvers import reverse
2238 from maasserver.enum import (
2239- INTERFACE_TYPE,
2240- IPADDRESS_TYPE,
2241 NODE_STATUS,
2242 NODE_STATUS_CHOICES,
2243- NODE_STATUS_CHOICES_DICT,
2244 )
2245 from maasserver.models import (
2246 Node,
2247@@ -44,24 +39,10 @@
2248
2249 class NodeAnonAPITest(MAASServerTestCase):
2250
2251- def setUp(self):
2252- super(NodeAnonAPITest, self).setUp()
2253- self.patch(node_module, 'power_on_node')
2254- self.patch(node_module, 'power_off_node')
2255- self.patch(node_module, 'power_driver_check')
2256-
2257- def test_anon_api_doc(self):
2258- # The documentation is accessible to anon users.
2259- self.patch(sys, "stderr", StringIO())
2260- response = self.client.get(reverse('api-doc'))
2261- self.assertEqual(http.client.OK, response.status_code)
2262- # No error or warning are emitted by docutils.
2263- self.assertEqual("", sys.stderr.getvalue())
2264-
2265 def test_node_init_user_cannot_access(self):
2266 token = NodeKey.objects.get_token_for_node(factory.make_Node())
2267 client = OAuthAuthenticatedClient(get_node_init_user(), token)
2268- response = client.get(reverse('nodes_handler'), {'op': 'list'})
2269+ response = client.get(reverse('nodes_handler'))
2270 self.assertEqual(http.client.FORBIDDEN, response.status_code)
2271
2272
2273@@ -87,13 +68,6 @@
2274 class TestNodeAPI(APITestCase):
2275 """Tests for /api/2.0/nodes/<node>/."""
2276
2277- def setUp(self):
2278- super(TestNodeAPI, self).setUp()
2279- self.patch(node_module, 'power_on_node')
2280- self.patch(node_module, 'power_off_node')
2281- self.patch(node_module, 'power_driver_check')
2282- self.patch(node_module.Node, '_power_control_node')
2283-
2284 def test_handler_path(self):
2285 self.assertEqual(
2286 '/api/2.0/nodes/node-name/',
2287@@ -104,62 +78,6 @@
2288 """Get the API URI for `node`."""
2289 return reverse('node_handler', args=[node.system_id])
2290
2291- def test_GET_returns_node(self):
2292- # The api allows for fetching a single Node (using system_id).
2293- node = factory.make_Node()
2294- response = self.client.get(self.get_node_uri(node))
2295-
2296- self.assertEqual(http.client.OK, response.status_code)
2297- parsed_result = json_load_bytes(response.content)
2298- domain_name = node.domain.name
2299- self.assertEqual(
2300- "%s.%s" % (node.hostname, domain_name),
2301- parsed_result['hostname'])
2302- self.assertEqual(node.system_id, parsed_result['system_id'])
2303-
2304- def test_GET_returns_associated_tag(self):
2305- node = factory.make_Node()
2306- tag = factory.make_Tag()
2307- node.tags.add(tag)
2308- response = self.client.get(self.get_node_uri(node))
2309-
2310- self.assertEqual(http.client.OK, response.status_code)
2311- parsed_result = json_load_bytes(response.content)
2312- self.assertEqual([tag.name], parsed_result['tag_names'])
2313-
2314- def test_GET_returns_associated_ip_addresses(self):
2315- node = factory.make_Node(disable_ipv4=False)
2316- nic = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
2317- subnet = factory.make_Subnet()
2318- ip = factory.pick_ip_in_network(subnet.get_ipnetwork())
2319- lease = factory.make_StaticIPAddress(
2320- alloc_type=IPADDRESS_TYPE.DISCOVERED, ip=ip,
2321- interface=nic, subnet=subnet)
2322- response = self.client.get(self.get_node_uri(node))
2323-
2324- self.assertEqual(
2325- http.client.OK, response.status_code, response.content)
2326- parsed_result = json_load_bytes(response.content)
2327- self.assertEqual([lease.ip], parsed_result['ip_addresses'])
2328-
2329- def test_GET_returns_interface_set(self):
2330- node = factory.make_Node()
2331- response = self.client.get(self.get_node_uri(node))
2332- self.assertEqual(http.client.OK, response.status_code)
2333- parsed_result = json_load_bytes(response.content)
2334- self.assertIn('interface_set', parsed_result)
2335-
2336- def test_GET_returns_zone(self):
2337- node = factory.make_Node()
2338- response = self.client.get(self.get_node_uri(node))
2339- self.assertEqual(http.client.OK, response.status_code)
2340- parsed_result = json_load_bytes(response.content)
2341- self.assertEqual(
2342- [node.zone.name, node.zone.description],
2343- [
2344- parsed_result['zone']['name'],
2345- parsed_result['zone']['description']])
2346-
2347 def test_GET_refuses_to_access_nonexistent_node(self):
2348 # When fetching a Node, the api returns a 'Not Found' (404) error
2349 # if no node is found.
2350@@ -182,84 +100,6 @@
2351 self.assertEqual(
2352 "Not Found", response.content.decode(settings.DEFAULT_CHARSET))
2353
2354- def test_GET_returns_owner_name_when_allocated_to_self(self):
2355- node = factory.make_Node(
2356- status=NODE_STATUS.ALLOCATED, owner=self.logged_in_user)
2357- response = self.client.get(self.get_node_uri(node))
2358- self.assertEqual(http.client.OK, response.status_code)
2359- parsed_result = json_load_bytes(response.content)
2360- self.assertEqual(node.owner.username, parsed_result["owner"])
2361-
2362- def test_GET_returns_owner_name_when_allocated_to_other_user(self):
2363- node = factory.make_Node(
2364- status=NODE_STATUS.ALLOCATED, owner=factory.make_User())
2365- response = self.client.get(self.get_node_uri(node))
2366- self.assertEqual(http.client.OK, response.status_code)
2367- parsed_result = json_load_bytes(response.content)
2368- self.assertEqual(node.owner.username, parsed_result["owner"])
2369-
2370- def test_GET_returns_empty_owner_when_not_allocated(self):
2371- node = factory.make_Node(status=NODE_STATUS.READY)
2372- response = self.client.get(self.get_node_uri(node))
2373- self.assertEqual(http.client.OK, response.status_code)
2374- parsed_result = json_load_bytes(response.content)
2375- self.assertEqual(None, parsed_result["owner"])
2376-
2377- def test_GET_returns_physical_block_devices(self):
2378- node = factory.make_Node(with_boot_disk=False)
2379- devices = [
2380- factory.make_PhysicalBlockDevice(node=node)
2381- for _ in range(3)
2382- ]
2383- response = self.client.get(self.get_node_uri(node))
2384- self.assertEqual(http.client.OK, response.status_code)
2385- parsed_result = json_load_bytes(response.content)
2386- parsed_devices = [
2387- device['name']
2388- for device in parsed_result['physicalblockdevice_set']
2389- ]
2390- self.assertItemsEqual(
2391- [device.name for device in devices], parsed_devices)
2392-
2393- def test_GET_returns_min_hwe_kernel_and_hwe_kernel(self):
2394- node = factory.make_Node()
2395- response = self.client.get(self.get_node_uri(node))
2396-
2397- self.assertEqual(http.client.OK, response.status_code)
2398- parsed_result = json_load_bytes(response.content)
2399- self.assertEqual(None, parsed_result['min_hwe_kernel'])
2400- self.assertEqual(None, parsed_result['hwe_kernel'])
2401-
2402- def test_GET_returns_min_hwe_kernel(self):
2403- node = factory.make_Node(min_hwe_kernel="hwe-v")
2404- response = self.client.get(self.get_node_uri(node))
2405-
2406- self.assertEqual(http.client.OK, response.status_code)
2407- parsed_result = json_load_bytes(response.content)
2408- self.assertEqual("hwe-v", parsed_result['min_hwe_kernel'])
2409-
2410- def test_GET_returns_status_message_with_most_recent_event(self):
2411- """Makes sure the most recent event from this node is shown in the
2412- status_message attribute."""
2413- # The first event won't be returned.
2414- event = factory.make_Event(description="Uninteresting event")
2415- node = event.node
2416- # The second (and last) event will be returned.
2417- message = "Interesting event"
2418- factory.make_Event(description=message, node=node)
2419- response = self.client.get(self.get_node_uri(node))
2420- parsed_result = json_load_bytes(response.content)
2421- self.assertEqual(message, parsed_result['status_message'])
2422-
2423- def test_GET_returns_status_name(self):
2424- """GET should display the node status as a user-friendly string."""
2425- for status in NODE_STATUS_CHOICES_DICT:
2426- node = factory.make_Node(status=status)
2427- response = self.client.get(self.get_node_uri(node))
2428- parsed_result = json_load_bytes(response.content)
2429- self.assertEqual(NODE_STATUS_CHOICES_DICT[status],
2430- parsed_result['status_name'])
2431-
2432 def test_resource_uri_points_back_at_machine(self):
2433 self.become_admin()
2434 # When a Machine is returned by the API, the field 'resource_uri'
2435
2436=== modified file 'src/maasserver/api/tests/test_nodes.py'
2437--- src/maasserver/api/tests/test_nodes.py 2016-02-26 18:39:26 +0000
2438+++ src/maasserver/api/tests/test_nodes.py 2016-03-02 18:21:51 +0000
2439@@ -20,10 +20,7 @@
2440 NODE_TYPE,
2441 )
2442 from maasserver.exceptions import MAASAPIValidationError
2443-from maasserver.testing.api import (
2444- APITestCase,
2445- MultipleUsersScenarios,
2446-)
2447+from maasserver.testing.api import APITestCase
2448 from maasserver.testing.factory import factory
2449 from maasserver.testing.testcase import MAASServerTestCase
2450 from maasserver.utils import ignore_unused
2451@@ -31,33 +28,6 @@
2452 from maastesting.djangotestcase import count_queries
2453
2454
2455-class NodeHostnameTest(MultipleUsersScenarios,
2456- MAASServerTestCase):
2457-
2458- scenarios = [
2459- ('user', dict(userfactory=factory.make_User)),
2460- ('admin', dict(userfactory=factory.make_admin)),
2461- ]
2462-
2463- def test_GET_returns_fqdn_with_proper_domain_name(self):
2464- # The FQDN for a host is properly constructed.
2465- hostname_without_domain = factory.make_name('hostname')
2466- domain = factory.make_Domain(name=factory.make_name('domain'))
2467- hostname_with_domain = '%s.%s' % (
2468- hostname_without_domain, domain.name)
2469- factory.make_Node(
2470- hostname=hostname_with_domain)
2471- expected_hostname = hostname_with_domain
2472- response = self.client.get(reverse('nodes_handler'))
2473- self.assertEqual(
2474- http.client.OK.value, response.status_code, response.content)
2475- parsed_result = json.loads(
2476- response.content.decode(settings.DEFAULT_CHARSET))
2477- self.assertItemsEqual(
2478- [expected_hostname],
2479- [node.get('hostname') for node in parsed_result])
2480-
2481-
2482 class AnonymousIsRegisteredAPITest(MAASServerTestCase):
2483
2484 def test_is_registered_returns_True_if_node_registered(self):
2485
2486=== modified file 'src/maasserver/api/tests/test_rackcontroller.py'
2487--- src/maasserver/api/tests/test_rackcontroller.py 2016-02-18 00:34:58 +0000
2488+++ src/maasserver/api/tests/test_rackcontroller.py 2016-03-02 18:21:51 +0000
2489@@ -12,6 +12,7 @@
2490 explain_unexpected_response,
2491 )
2492 from maasserver.testing.factory import factory
2493+from maasserver.utils.converters import json_load_bytes
2494 from maastesting.matchers import MockCalledOnceWith
2495
2496
2497@@ -74,7 +75,28 @@
2498 self.assertEqual(
2499 '/api/2.0/rackcontrollers/', reverse('rackcontrollers_handler'))
2500
2501- @staticmethod
2502- def get_rack_uri(rack):
2503- """Get the API URI for `rack`."""
2504- return reverse('rackcontrollers_handler')
2505+ def test_read_returns_limited_fields(self):
2506+ factory.make_RackController(owner=self.logged_in_user)
2507+ response = self.client.get(reverse('rackcontrollers_handler'))
2508+ parsed_result = json_load_bytes(response.content)
2509+ self.assertItemsEqual(
2510+ [
2511+ 'system_id',
2512+ 'hostname',
2513+ 'domain',
2514+ 'fqdn',
2515+ 'architecture',
2516+ 'cpu_count',
2517+ 'memory',
2518+ 'swap_size',
2519+ 'osystem',
2520+ 'resource_uri',
2521+ 'distro_series',
2522+ 'interface_set',
2523+ 'ip_addresses',
2524+ 'zone',
2525+ 'status_action',
2526+ 'node_type',
2527+ 'node_type_name',
2528+ ],
2529+ list(parsed_result[0]))
2530
2531=== modified file 'src/maasserver/api/users.py'
2532--- src/maasserver/api/users.py 2016-02-03 10:27:11 +0000
2533+++ src/maasserver/api/users.py 2016-03-02 18:21:51 +0000
2534@@ -1,4 +1,4 @@
2535-# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
2536+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
2537 # GNU Affero General Public License version 3 (see the file LICENSE).
2538
2539 """API handlers: `User`."""
2540
2541=== modified file 'src/maasserver/api/utils.py'
2542--- src/maasserver/api/utils.py 2015-12-01 18:12:59 +0000
2543+++ src/maasserver/api/utils.py 2016-03-02 18:21:51 +0000
2544@@ -1,4 +1,4 @@
2545-# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
2546+# Copyright 2012-2016 Canonical Ltd. This software is licensed under the
2547 # GNU Affero General Public License version 3 (see the file LICENSE).
2548
2549 """Helpers for Piston-based MAAS APIs."""
2550
2551=== modified file 'src/maasserver/api/version.py'
2552--- src/maasserver/api/version.py 2015-12-11 20:45:50 +0000
2553+++ src/maasserver/api/version.py 2016-03-02 18:21:51 +0000
2554@@ -1,4 +1,4 @@
2555-# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
2556+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
2557 # GNU Affero General Public License version 3 (see the file LICENSE).
2558
2559 """API handler: API Version."""
2560
2561=== modified file 'src/maasserver/api/volume_groups.py'
2562--- src/maasserver/api/volume_groups.py 2015-12-16 00:01:56 +0000
2563+++ src/maasserver/api/volume_groups.py 2016-03-02 18:21:51 +0000
2564@@ -1,4 +1,4 @@
2565-# Copyright 2015 Canonical Ltd. This software is licensed under the
2566+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
2567 # GNU Affero General Public License version 3 (see the file LICENSE).
2568
2569 """API handlers: `VolumeGroups`."""
2570@@ -46,7 +46,7 @@
2571
2572
2573 class VolumeGroupsHandler(OperationsHandler):
2574- """Manage volume groups on a node."""
2575+ """Manage volume groups on a machine."""
2576 api_doc_section_name = "Volume groups"
2577 update = delete = None
2578 fields = DISPLAYED_VOLUME_GROUP_FIELDS
2579@@ -80,7 +80,7 @@
2580 system_id, request.user, NODE_PERMISSION.ADMIN)
2581 if machine.status != NODE_STATUS.READY:
2582 raise NodeStateViolation(
2583- "Cannot create volume group because the node is not Ready.")
2584+ "Cannot create volume group because the machine is not Ready.")
2585 form = CreateVolumeGroupForm(machine, data=request.data)
2586 if not form.is_valid():
2587 raise MAASAPIValidationError(form.errors)
2588@@ -89,7 +89,7 @@
2589
2590
2591 class VolumeGroupHandler(OperationsHandler):
2592- """Manage volume group on a node."""
2593+ """Manage volume group on a machine."""
2594 api_doc_section_name = "Volume group"
2595 create = None
2596 model = VolumeGroup
2597@@ -143,15 +143,15 @@
2598 ]
2599
2600 def read(self, request, system_id, volume_group_id):
2601- """Read volume group on node.
2602+ """Read volume group on a machine.
2603
2604- Returns 404 if the node or volume group is not found.
2605+ Returns 404 if the machine or volume group is not found.
2606 """
2607 return VolumeGroup.objects.get_object_or_404(
2608 system_id, volume_group_id, request.user, NODE_PERMISSION.VIEW)
2609
2610 def update(self, request, system_id, volume_group_id):
2611- """Read volume group on node.
2612+ """Read volume group on a machine.
2613
2614 :param name: Name of the volume group.
2615 :param uuid: UUID of the volume group.
2616@@ -161,15 +161,15 @@
2617 :param add_partitions: Partitions to add to the volume group.
2618 :param remove_partitions: Partitions to remove from the volume group.
2619
2620- Returns 404 if the node or volume group is not found.
2621- Returns 409 if the node is not Ready.
2622+ Returns 404 if the machine or volume group is not found.
2623+ Returns 409 if the machine is not Ready.
2624 """
2625 volume_group = VolumeGroup.objects.get_object_or_404(
2626 system_id, volume_group_id, request.user, NODE_PERMISSION.ADMIN)
2627 node = volume_group.get_node()
2628 if node.status != NODE_STATUS.READY:
2629 raise NodeStateViolation(
2630- "Cannot update volume group because the node is not Ready.")
2631+ "Cannot update volume group because the machine is not Ready.")
2632 form = UpdateVolumeGroupForm(volume_group, data=request.data)
2633 if not form.is_valid():
2634 raise MAASAPIValidationError(form.errors)
2635@@ -177,17 +177,17 @@
2636 return form.save()
2637
2638 def delete(self, request, system_id, volume_group_id):
2639- """Delete volume group on node.
2640+ """Delete volume group on a machine.
2641
2642- Returns 404 if the node or volume group is not found.
2643- Returns 409 if the node is not Ready.
2644+ Returns 404 if the machine or volume group is not found.
2645+ Returns 409 if the machine is not Ready.
2646 """
2647 volume_group = VolumeGroup.objects.get_object_or_404(
2648 system_id, volume_group_id, request.user, NODE_PERMISSION.ADMIN)
2649 node = volume_group.get_node()
2650 if node.status != NODE_STATUS.READY:
2651 raise NodeStateViolation(
2652- "Cannot delete volume group because the node is not Ready.")
2653+ "Cannot delete volume group because the machine is not Ready.")
2654 volume_group.delete()
2655 return rc.DELETED
2656
2657@@ -199,15 +199,16 @@
2658 :param uuid: (optional) UUID of the logical volume.
2659 :param size: Size of the logical volume.
2660
2661- Returns 404 if the node or volume group is not found.
2662- Returns 409 if the node is not Ready.
2663+ Returns 404 if the machine or volume group is not found.
2664+ Returns 409 if the machine is not Ready.
2665 """
2666 volume_group = VolumeGroup.objects.get_object_or_404(
2667 system_id, volume_group_id, request.user, NODE_PERMISSION.ADMIN)
2668 node = volume_group.get_node()
2669 if node.status != NODE_STATUS.READY:
2670 raise NodeStateViolation(
2671- "Cannot create logical volume because the node is not Ready.")
2672+ "Cannot create logical volume because the machine is not "
2673+ "Ready.")
2674 form = CreateLogicalVolumeForm(volume_group, data=request.data)
2675 if not form.is_valid():
2676 raise MAASAPIValidationError(form.errors)
2677@@ -221,15 +222,16 @@
2678 :param id: ID of the logical volume.
2679
2680 Returns 403 if no logical volume with id.
2681- Returns 404 if the node or volume group is not found.
2682- Returns 409 if the node is not Ready.
2683+ Returns 404 if the machine or volume group is not found.
2684+ Returns 409 if the machine is not Ready.
2685 """
2686 volume_group = VolumeGroup.objects.get_object_or_404(
2687 system_id, volume_group_id, request.user, NODE_PERMISSION.ADMIN)
2688 node = volume_group.get_node()
2689 if node.status != NODE_STATUS.READY:
2690 raise NodeStateViolation(
2691- "Cannot delete logical volume because the node is not Ready.")
2692+ "Cannot delete logical volume because the machine is not "
2693+ "Ready.")
2694 volume_id = get_mandatory_param(request.data, 'id')
2695 try:
2696 logical_volume = volume_group.virtual_devices.get(id=volume_id)
2697
2698=== modified file 'src/maasserver/api/zones.py'
2699--- src/maasserver/api/zones.py 2015-12-01 18:12:59 +0000
2700+++ src/maasserver/api/zones.py 2016-03-02 18:21:51 +0000
2701@@ -1,4 +1,4 @@
2702-# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
2703+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
2704 # GNU Affero General Public License version 3 (see the file LICENSE).
2705
2706 """API handlers: `Zone`."""
2707
2708=== modified file 'src/maasserver/forms.py'
2709--- src/maasserver/forms.py 2016-03-01 21:59:22 +0000
2710+++ src/maasserver/forms.py 2016-03-02 18:21:51 +0000
2711@@ -1,11 +1,12 @@
2712-# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
2713+# Copyright 2012-2016 Canonical Ltd. This software is licensed under the
2714 # GNU Affero General Public License version 3 (see the file LICENSE).
2715
2716 """Forms."""
2717
2718 __all__ = [
2719+ "AdminMachineForm",
2720+ "AdminMachineWithMACAddressesForm",
2721 "AdminNodeForm",
2722- "AdminNodeWithMACAddressesForm",
2723 "BootSourceForm",
2724 "BootSourceSelectionForm",
2725 "BootSourceSettingsForm",
2726@@ -13,14 +14,17 @@
2727 "ClaimIPForMACForm",
2728 "CommissioningForm",
2729 "CommissioningScriptForm",
2730+ "get_machine_edit_form",
2731+ "get_machine_create_form",
2732 "CreatePhysicalBlockDeviceForm",
2733- "get_node_create_form",
2734 "get_node_edit_form",
2735 "list_all_usable_architectures",
2736 "MAASAndNetworkForm",
2737 "MountFilesystemForm",
2738 "NetworksListingForm",
2739- "NodeWithMACAddressesForm",
2740+ "NodeChoiceField",
2741+ "MachineWithMACAddressesForm",
2742+ "CreatePhysicalBlockDeviceForm",
2743 "ReleaseIPForm",
2744 "SSHKeyForm",
2745 "SSLKeyForm",
2746@@ -102,10 +106,12 @@
2747 CacheSet,
2748 Config,
2749 Device,
2750+ Domain,
2751 Filesystem,
2752 Interface,
2753 LargeFile,
2754 LicenseKey,
2755+ Machine,
2756 Node,
2757 Partition,
2758 PartitionTable,
2759@@ -371,10 +377,10 @@
2760 class NodeForm(MAASModelForm):
2761 def __init__(self, request=None, *args, **kwargs):
2762 super(NodeForm, self).__init__(*args, **kwargs)
2763+
2764 # Even though it doesn't need it and doesn't use it, this form accepts
2765 # a parameter named 'request' because it is used interchangingly
2766- # with AdminNodeForm which actually uses this parameter.
2767-
2768+ # with AdminMachineForm which actually uses this parameter.
2769 instance = kwargs.get('instance')
2770 if instance is None or instance.owner is None:
2771 self.has_owner = False
2772@@ -384,78 +390,9 @@
2773 # Are we creating a new node object?
2774 self.new_node = (instance is None)
2775
2776- self.set_up_architecture_field()
2777- if self.has_owner:
2778- self.set_up_osystem_and_distro_series_fields(instance)
2779-
2780 self.fields['disable_ipv4'] = forms.BooleanField(
2781 label="", required=False)
2782
2783- # We only want the license key field to render in the UI if the `OS`
2784- # and `Release` fields are also present.
2785- if self.has_owner:
2786- self.fields['license_key'] = forms.CharField(
2787- label="License Key", required=False, help_text=(
2788- "License key for operating system"),
2789- max_length=30)
2790- else:
2791- self.fields['license_key'] = forms.CharField(
2792- label="", required=False, widget=forms.HiddenInput())
2793-
2794- def set_up_architecture_field(self):
2795- """Create the `architecture` field.
2796-
2797- This needs to be done on the fly so that we can pass a dynamic list of
2798- usable architectures.
2799- """
2800- architectures = list_all_usable_architectures()
2801- default_arch = pick_default_architecture(architectures)
2802- if len(architectures) == 0:
2803- choices = [BLANK_CHOICE]
2804- else:
2805- choices = list_architecture_choices(architectures)
2806- invalid_arch_message = compose_invalid_choice_text(
2807- 'architecture', choices)
2808- self.fields['architecture'] = forms.ChoiceField(
2809- choices=choices, required=False, initial=default_arch,
2810- error_messages={'invalid_choice': invalid_arch_message})
2811-
2812- def set_up_osystem_and_distro_series_fields(self, instance):
2813- """Create the `osystem` and `distro_series` fields.
2814-
2815- This needs to be done on the fly so that we can pass a dynamic list of
2816- usable operating systems and distro_series.
2817- """
2818- osystems = list_all_usable_osystems()
2819- releases = list_all_usable_releases(osystems)
2820- if self.has_owner:
2821- os_choices = list_osystem_choices(osystems)
2822- distro_choices = list_release_choices(releases)
2823- invalid_osystem_message = compose_invalid_choice_text(
2824- 'osystem', os_choices)
2825- invalid_distro_series_message = compose_invalid_choice_text(
2826- 'distro_series', distro_choices)
2827- self.fields['osystem'] = forms.ChoiceField(
2828- label="OS", choices=os_choices, required=False, initial='',
2829- error_messages={'invalid_choice': invalid_osystem_message})
2830- self.fields['distro_series'] = forms.ChoiceField(
2831- label="Release", choices=distro_choices,
2832- required=False, initial='',
2833- error_messages={
2834- 'invalid_choice': invalid_distro_series_message})
2835- else:
2836- self.fields['osystem'] = forms.ChoiceField(
2837- label="", required=False, widget=forms.HiddenInput())
2838- self.fields['distro_series'] = forms.ChoiceField(
2839- label="", required=False, widget=forms.HiddenInput())
2840- if instance is not None:
2841- initial_value = get_distro_series_initial(osystems, instance)
2842- if instance is not None:
2843- self.initial['distro_series'] = initial_value
2844-
2845- def clean_distro_series(self):
2846- return clean_distro_series_field(self, 'distro_series', 'osystem')
2847-
2848 def clean_disable_ipv4(self):
2849 # Boolean fields only show up in UI form submissions as "true" (if the
2850 # box was checked) or not at all (if the box was not checked). This
2851@@ -495,6 +432,122 @@
2852 except ValueError:
2853 raise ValidationError('Invalid size for swap: %s' % swap_size)
2854
2855+ def clean_domain(self):
2856+ domain = self.cleaned_data.get('domain')
2857+ if not domain:
2858+ return None
2859+ try:
2860+ return Domain.objects.get(id=int(domain))
2861+ except ValueError:
2862+ try:
2863+ return Domain.objects.get(name=domain)
2864+ except Domain.DoesNotExist:
2865+ raise ValidationError("Unable to find domain %s" % domain)
2866+
2867+ hostname = forms.CharField(
2868+ label="Host name", required=False, help_text=(
2869+ "The hostname of the machine"))
2870+
2871+ domain = forms.CharField(
2872+ label="Domain name", required=False, help_text=(
2873+ "The domain name of the machine."))
2874+
2875+ swap_size = forms.CharField(
2876+ label="Swap size", required=False, help_text=(
2877+ "The size of the swap file in bytes. The field also accepts K, M, "
2878+ "G and T meaning kilobytes, megabytes, gigabytes and terabytes."))
2879+
2880+ class Meta:
2881+ model = Node
2882+
2883+ # Fields that the form should generate automatically from the
2884+ # model:
2885+ # Note: fields have to be added here even if they were defined manually
2886+ # elsewhere in the form
2887+ fields = (
2888+ 'hostname',
2889+ 'domain',
2890+ 'disable_ipv4',
2891+ 'swap_size',
2892+ )
2893+
2894+
2895+class MachineForm(NodeForm):
2896+ def __init__(self, request=None, *args, **kwargs):
2897+ super(MachineForm, self).__init__(*args, **kwargs)
2898+
2899+ # Even though it doesn't need it and doesn't use it, this form accepts
2900+ # a parameter named 'request' because it is used interchangingly
2901+ # with AdminMachineForm which actually uses this parameter.
2902+ instance = kwargs.get('instance')
2903+
2904+ self.set_up_architecture_field()
2905+ # We only want the license key field to render in the UI if the `OS`
2906+ # and `Release` fields are also present.
2907+ if self.has_owner:
2908+ self.set_up_osystem_and_distro_series_fields(instance)
2909+ self.fields['license_key'] = forms.CharField(
2910+ label="License Key", required=False, help_text=(
2911+ "License key for operating system"),
2912+ max_length=30)
2913+ else:
2914+ self.fields['license_key'] = forms.CharField(
2915+ label="", required=False, widget=forms.HiddenInput())
2916+
2917+ def set_up_architecture_field(self):
2918+ """Create the `architecture` field.
2919+
2920+ This needs to be done on the fly so that we can pass a dynamic list of
2921+ usable architectures.
2922+ """
2923+ architectures = list_all_usable_architectures()
2924+ default_arch = pick_default_architecture(architectures)
2925+ if len(architectures) == 0:
2926+ choices = [BLANK_CHOICE]
2927+ else:
2928+ choices = list_architecture_choices(architectures)
2929+ invalid_arch_message = compose_invalid_choice_text(
2930+ 'architecture', choices)
2931+ self.fields['architecture'] = forms.ChoiceField(
2932+ choices=choices, required=False, initial=default_arch,
2933+ error_messages={'invalid_choice': invalid_arch_message})
2934+
2935+ def set_up_osystem_and_distro_series_fields(self, instance):
2936+ """Create the `osystem` and `distro_series` fields.
2937+
2938+ This needs to be done on the fly so that we can pass a dynamic list of
2939+ usable operating systems and distro_series.
2940+ """
2941+ osystems = list_all_usable_osystems()
2942+ releases = list_all_usable_releases(osystems)
2943+ if self.has_owner:
2944+ os_choices = list_osystem_choices(osystems)
2945+ distro_choices = list_release_choices(releases)
2946+ invalid_osystem_message = compose_invalid_choice_text(
2947+ 'osystem', os_choices)
2948+ invalid_distro_series_message = compose_invalid_choice_text(
2949+ 'distro_series', distro_choices)
2950+ self.fields['osystem'] = forms.ChoiceField(
2951+ label="OS", choices=os_choices, required=False, initial='',
2952+ error_messages={'invalid_choice': invalid_osystem_message})
2953+ self.fields['distro_series'] = forms.ChoiceField(
2954+ label="Release", choices=distro_choices,
2955+ required=False, initial='',
2956+ error_messages={
2957+ 'invalid_choice': invalid_distro_series_message})
2958+ else:
2959+ self.fields['osystem'] = forms.ChoiceField(
2960+ label="", required=False, widget=forms.HiddenInput())
2961+ self.fields['distro_series'] = forms.ChoiceField(
2962+ label="", required=False, widget=forms.HiddenInput())
2963+ if instance is not None:
2964+ initial_value = get_distro_series_initial(osystems, instance)
2965+ if instance is not None:
2966+ self.initial['distro_series'] = initial_value
2967+
2968+ def clean_distro_series(self):
2969+ return clean_distro_series_field(self, 'distro_series', 'osystem')
2970+
2971 def clean_min_hwe_kernel(self):
2972 min_hwe_kernel = self.cleaned_data.get('min_hwe_kernel')
2973 if self.new_node and not min_hwe_kernel:
2974@@ -503,7 +556,7 @@
2975 return validate_min_hwe_kernel(min_hwe_kernel)
2976
2977 def clean(self):
2978- cleaned_data = super(NodeForm, self).clean()
2979+ cleaned_data = super(MachineForm, self).clean()
2980
2981 if not self.instance.hwe_kernel:
2982 osystem = cleaned_data.get('osystem')
2983@@ -520,7 +573,7 @@
2984 return cleaned_data
2985
2986 def is_valid(self):
2987- is_valid = super(NodeForm, self).is_valid()
2988+ is_valid = super(MachineForm, self).is_valid()
2989 if not is_valid:
2990 return False
2991 if len(list_all_usable_architectures()) == 0:
2992@@ -585,41 +638,20 @@
2993 self.is_bound = True
2994 self.data['hwe_kernel'] = hwe_kernel
2995
2996- hostname = forms.CharField(
2997- label="Host name", required=False, help_text=(
2998- "The FQDN (Fully Qualified Domain Name) is derived from the "
2999- "host name: If the cluster controller for this node is managing "
3000- "DNS then the domain part in the host name (if any) is replaced "
3001- "by the domain defined on the cluster; if the cluster controller "
3002- "does not manage DNS, then the host name as entered will be the "
3003- "FQDN."))
3004-
3005- swap_size = forms.CharField(
3006- label="Swap size", required=False, help_text=(
3007- "The size of the swap file in bytes. The field also accepts K, M, "
3008- "G and T meaning kilobytes, megabytes, gigabytes and terabytes."))
3009-
3010 class Meta:
3011- model = Node
3012+ model = Machine
3013
3014- # Fields that the form should generate automatically from the
3015- # model:
3016- # Note: fields have to be added here even if they were defined manually
3017- # elsewhere in the form
3018- fields = (
3019- 'hostname',
3020+ fields = NodeForm.Meta.fields + (
3021 'architecture',
3022 'osystem',
3023 'distro_series',
3024 'license_key',
3025- 'disable_ipv4',
3026- 'swap_size',
3027 'min_hwe_kernel',
3028- 'hwe_kernel'
3029- )
3030-
3031-
3032-class DeviceForm(MAASModelForm):
3033+ 'hwe_kernel',
3034+ )
3035+
3036+
3037+class DeviceForm(NodeForm):
3038 parent = forms.ModelChoiceField(
3039 required=False, initial=None,
3040 queryset=Node.objects.all(), to_field_name='system_id')
3041@@ -627,8 +659,7 @@
3042 class Meta:
3043 model = Device
3044
3045- fields = (
3046- 'hostname',
3047+ fields = NodeForm.Meta.fields + (
3048 'parent',
3049 )
3050
3051@@ -637,8 +668,6 @@
3052 self.request = request
3053
3054 instance = kwargs.get('instance')
3055- # Are we creating a new device object?
3056- self.new_device = (instance is None)
3057 self.set_up_initial_device(instance)
3058
3059 def set_up_initial_device(self, instance):
3060@@ -652,12 +681,13 @@
3061 def save(self, commit=True):
3062 device = super(DeviceForm, self).save(commit=False)
3063 device.node_type = NODE_TYPE.DEVICE
3064- if self.new_device:
3065+ if self.new_node:
3066 # Set the owner: devices are owned by their creator.
3067 device.owner = self.request.user
3068 device.save()
3069 return device
3070
3071+
3072 CLUSTER_NOT_AVAILABLE = mark_safe(
3073 "The cluster controller for this node is not responding; power type "
3074 "validation is not available. "
3075@@ -673,7 +703,6 @@
3076
3077 class AdminNodeForm(NodeForm):
3078 """A `NodeForm` which includes fields that only an admin may change."""
3079-
3080 zone = forms.ModelChoiceField(
3081 label="Physical zone", required=False,
3082 initial=Zone.objects.get_default_zone,
3083@@ -699,7 +728,6 @@
3084 data=data, instance=instance, **kwargs)
3085 self.request = request
3086 self.set_up_initial_zone(instance)
3087- AdminNodeForm.set_up_power_type(self, data, instance)
3088 # The zone field is not required because we want to be able
3089 # to omit it when using that form in the API.
3090 # We don't want the UI to show an entry for the 'empty' zone,
3091@@ -718,17 +746,47 @@
3092 if instance is not None:
3093 self.initial['zone'] = instance.zone.name
3094
3095+ def save(self, *args, **kwargs):
3096+ """Persist the node into the database."""
3097+ node = super(AdminNodeForm, self).save(commit=False)
3098+ zone = self.cleaned_data.get('zone')
3099+ if zone:
3100+ node.zone = zone
3101+ if kwargs.get('commit', True):
3102+ node.save(*args, **kwargs)
3103+ self.save_m2m() # Save many to many relations.
3104+ return node
3105+
3106+
3107+class AdminMachineForm(MachineForm, AdminNodeForm):
3108+ """A `MachineForm` which includes fields that only an admin may change."""
3109+
3110+ class Meta:
3111+ model = Machine
3112+
3113+ # Fields that the form should generate automatically from the
3114+ # model:
3115+ fields = MachineForm.Meta.fields + (
3116+ 'cpu_count',
3117+ 'memory',
3118+ )
3119+
3120+ def __init__(self, data=None, instance=None, request=None, **kwargs):
3121+ super(AdminMachineForm, self).__init__(
3122+ data=data, instance=instance, **kwargs)
3123+ AdminMachineForm.set_up_power_type(self, data, instance)
3124+
3125 @staticmethod
3126- def _get_power_type(form, data, node):
3127+ def _get_power_type(form, data, machine):
3128 if data is None:
3129 data = {}
3130
3131 power_type = data.get('power_type', form.initial.get('power_type'))
3132
3133- # If power_type is None (this is a node creation form or this
3134+ # If power_type is None (this is a machine creation form or this
3135 # form deals with an API call which does not change the value of
3136- # 'power_type') or invalid: get the node's current 'power_type'
3137- # value or the default value if this form is not linked to a node.
3138+ # 'power_type') or invalid: get the machine's current 'power_type'
3139+ # value or the default value if this form is not linked to a machine.
3140 try:
3141 power_types = get_power_types()
3142 except ClusterUnavailable as e:
3143@@ -741,17 +799,17 @@
3144 return ''
3145
3146 if power_type not in power_types:
3147- return '' if node is None else node.power_type
3148+ return '' if machine is None else machine.power_type
3149 return power_type
3150
3151 @staticmethod
3152- def set_up_power_type(form, data, node=None):
3153+ def set_up_power_type(form, data, machine=None):
3154 """Set up the 'power_type' and 'power_parameters' fields.
3155
3156 This can't be done at the model level because the choices need to
3157 be generated on the fly by get_power_type_choices().
3158 """
3159- power_type = AdminNodeForm._get_power_type(form, data, node)
3160+ power_type = AdminMachineForm._get_power_type(form, data, machine)
3161 choices = [BLANK_CHOICE] + get_power_type_choices()
3162 form.fields['power_type'] = forms.ChoiceField(
3163 required=False, choices=choices, initial=power_type)
3164@@ -782,30 +840,37 @@
3165 return cleaned_data
3166
3167 def clean(self):
3168- cleaned_data = super(AdminNodeForm, self).clean()
3169- return AdminNodeForm.check_power_type(self, cleaned_data)
3170+ cleaned_data = super(AdminMachineForm, self).clean()
3171+ return AdminMachineForm.check_power_type(self, cleaned_data)
3172
3173 @staticmethod
3174- def set_power_type(form, node):
3175- """Persist the node into the database."""
3176+ def set_power_type(form, machine):
3177+ """Persist the machine into the database."""
3178 power_type = form.cleaned_data.get('power_type')
3179 if power_type is not None:
3180- node.power_type = power_type
3181+ machine.power_type = power_type
3182 power_parameters = form.cleaned_data.get('power_parameters')
3183 if power_parameters is not None:
3184- node.power_parameters = power_parameters
3185+ machine.power_parameters = power_parameters
3186
3187 def save(self, *args, **kwargs):
3188 """Persist the node into the database."""
3189- node = super(AdminNodeForm, self).save(commit=False)
3190+ machine = super(AdminMachineForm, self).save(commit=False)
3191 zone = self.cleaned_data.get('zone')
3192 if zone:
3193- node.zone = zone
3194- AdminNodeForm.set_power_type(self, node)
3195+ machine.zone = zone
3196+ AdminMachineForm.set_power_type(self, machine)
3197 if kwargs.get('commit', True):
3198- node.save(*args, **kwargs)
3199+ machine.save(*args, **kwargs)
3200 self.save_m2m() # Save many to many relations.
3201- return node
3202+ return machine
3203+
3204+
3205+def get_machine_edit_form(user):
3206+ if user.is_superuser:
3207+ return AdminMachineForm
3208+ else:
3209+ return MachineForm
3210
3211
3212 def get_node_edit_form(user):
3213@@ -1008,40 +1073,41 @@
3214 """A form mixin which dynamically adds power_type and power_parameters to
3215 the list of fields. This mixin also overrides the 'save' method to persist
3216 these fields and is intended to be used with a class inheriting from
3217- NodeForm.
3218+ MachineForm.
3219 """
3220
3221 def __init__(self, *args, **kwargs):
3222 super(WithPowerMixin, self).__init__(*args, **kwargs)
3223 self.data = self.data.copy()
3224- AdminNodeForm.set_up_power_type(self, self.data)
3225+ AdminMachineForm.set_up_power_type(self, self.data)
3226
3227 def clean(self):
3228 cleaned_data = super(WithPowerMixin, self).clean()
3229- return AdminNodeForm.check_power_type(self, cleaned_data)
3230+ return AdminMachineForm.check_power_type(self, cleaned_data)
3231
3232 def save(self, *args, **kwargs):
3233 """Persist the node into the database."""
3234 node = super(WithPowerMixin, self).save()
3235- AdminNodeForm.set_power_type(self, node)
3236+ AdminMachineForm.set_power_type(self, node)
3237 node.save()
3238 return node
3239
3240
3241-class AdminNodeWithMACAddressesForm(WithMACAddressesMixin, AdminNodeForm):
3242- """A version of the AdminNodeForm which includes the multi-MAC address
3243+class AdminMachineWithMACAddressesForm(
3244+ WithMACAddressesMixin, AdminMachineForm):
3245+ """A version of the AdminMachineForm which includes the multi-MAC address
3246 field.
3247 """
3248
3249
3250-class NodeWithMACAddressesForm(WithMACAddressesMixin, NodeForm):
3251- """A version of the NodeForm which includes the multi-MAC address field.
3252+class MachineWithMACAddressesForm(WithMACAddressesMixin, MachineForm):
3253+ """A version of the MachineForm which includes the multi-MAC address field.
3254 """
3255
3256
3257-class NodeWithPowerAndMACAddressesForm(
3258- WithPowerMixin, NodeWithMACAddressesForm):
3259- """A version of the NodeForm which includes the power fields.
3260+class MachineWithPowerAndMACAddressesForm(
3261+ WithPowerMixin, MachineWithMACAddressesForm):
3262+ """A version of the MachineForm which includes the power fields.
3263 """
3264
3265
3266@@ -1050,11 +1116,11 @@
3267 """
3268
3269
3270-def get_node_create_form(user):
3271+def get_machine_create_form(user):
3272 if user.is_superuser:
3273- return AdminNodeWithMACAddressesForm
3274+ return AdminMachineWithMACAddressesForm
3275 else:
3276- return NodeWithPowerAndMACAddressesForm
3277+ return MachineWithPowerAndMACAddressesForm
3278
3279
3280 class ProfileForm(MAASModelForm):
3281
3282=== modified file 'src/maasserver/models/node.py'
3283--- src/maasserver/models/node.py 2016-03-02 14:21:29 +0000
3284+++ src/maasserver/models/node.py 2016-03-02 18:21:51 +0000
3285@@ -3361,8 +3361,8 @@
3286
3287 def add_chassis(
3288 self, user, chassis_type, hostname, username=None, password=None,
3289- accept_all=False, prefix_filter=None, power_control=None,
3290- port=None, protocol=None):
3291+ accept_all=False, domain=None, prefix_filter=None,
3292+ power_control=None, port=None, protocol=None):
3293 self._register_request_event(
3294 self.owner,
3295 EVENT_TYPES.REQUEST_RACK_CONTROLLER_ADD_CHASSIS,
3296@@ -3371,8 +3371,9 @@
3297 call = client(
3298 AddChassis, user=user, chassis_type=chassis_type,
3299 hostname=hostname, username=username, password=password,
3300- accept_all=accept_all, prefix_filter=prefix_filter,
3301- power_control=power_control, port=port, protocol=protocol)
3302+ accept_all=accept_all, domain=domain,
3303+ prefix_filter=prefix_filter, power_control=power_control,
3304+ port=port, protocol=protocol)
3305 call.wait(30)
3306
3307 def get_bmc_accessible_nodes(self):
3308
3309=== modified file 'src/maasserver/models/tests/test_node.py'
3310--- src/maasserver/models/tests/test_node.py 2016-03-02 17:21:09 +0000
3311+++ src/maasserver/models/tests/test_node.py 2016-03-02 18:21:51 +0000
3312@@ -5650,6 +5650,7 @@
3313 username = factory.make_name('username')
3314 password = factory.make_name('password')
3315 accept_all = factory.pick_bool()
3316+ domain = factory.make_name('domain')
3317 prefix_filter = factory.make_name('prefix_filter')
3318 power_control = factory.make_name('power_control')
3319 port = random.randint(0, 65535)
3320@@ -5657,15 +5658,16 @@
3321
3322 rackcontroller.add_chassis(
3323 user, chassis_type, hostname, username, password, accept_all,
3324- prefix_filter, power_control, port, given_protocol)
3325+ domain, prefix_filter, power_control, port, given_protocol)
3326
3327 self.expectThat(
3328 protocol.AddChassis,
3329 MockCalledOnceWith(
3330 ANY, user=user, chassis_type=chassis_type, hostname=hostname,
3331 username=username, password=password, accept_all=accept_all,
3332- prefix_filter=prefix_filter, power_control=power_control,
3333- port=port, protocol=given_protocol))
3334+ domain=domain, prefix_filter=prefix_filter,
3335+ power_control=power_control, port=port,
3336+ protocol=given_protocol))
3337
3338 def test_add_chassis_logs_user_request(self):
3339 rackcontroller = factory.make_RackController()
3340@@ -5682,6 +5684,7 @@
3341 username = factory.make_name('username')
3342 password = factory.make_name('password')
3343 accept_all = factory.pick_bool()
3344+ domain = factory.make_name('domain')
3345 prefix_filter = factory.make_name('prefix_filter')
3346 power_control = factory.make_name('power_control')
3347 port = random.randint(0, 65535)
3348@@ -5690,7 +5693,7 @@
3349 register_event = self.patch(rackcontroller, '_register_request_event')
3350 rackcontroller.add_chassis(
3351 user, chassis_type, hostname, username, password, accept_all,
3352- prefix_filter, power_control, port, given_protocol)
3353+ domain, prefix_filter, power_control, port, given_protocol)
3354 post_commit_hooks.reset() # Ignore these for now.
3355 self.assertThat(register_event, MockCalledOnceWith(
3356 rackcontroller.owner,
3357
3358=== modified file 'src/maasserver/preseed.py'
3359--- src/maasserver/preseed.py 2016-02-26 18:34:28 +0000
3360+++ src/maasserver/preseed.py 2016-03-02 18:21:51 +0000
3361@@ -601,7 +601,7 @@
3362 'osystem': osystem,
3363 'release': release,
3364 'server_host': server_host,
3365- 'server_url': absolute_reverse('nodes_handler', base_url=base_url),
3366+ 'server_url': absolute_reverse('machines_handler', base_url=base_url),
3367 'metadata_enlist_url': absolute_reverse('enlist', base_url=base_url),
3368 'enable_http_proxy': Config.objects.get_config('enable_http_proxy'),
3369 'http_proxy': Config.objects.get_config('http_proxy'),
3370
3371=== modified file 'src/maasserver/rpc/nodes.py'
3372--- src/maasserver/rpc/nodes.py 2016-02-11 15:06:36 +0000
3373+++ src/maasserver/rpc/nodes.py 2016-03-02 18:21:51 +0000
3374@@ -19,7 +19,7 @@
3375 from maasserver import exceptions
3376 from maasserver.api.utils import get_overridden_query_dict
3377 from maasserver.enum import NODE_STATUS
3378-from maasserver.forms import AdminNodeWithMACAddressesForm
3379+from maasserver.forms import AdminMachineWithMACAddressesForm
3380 from maasserver.models import (
3381 Node,
3382 PhysicalInterface,
3383@@ -177,8 +177,9 @@
3384
3385 @synchronous
3386 @transactional
3387-def create_node(architecture, power_type,
3388- power_parameters, mac_addresses, hostname=None):
3389+def create_node(
3390+ architecture, power_type, power_parameters, mac_addresses, domain=None,
3391+ hostname=None):
3392 """Create a new `Node` and return it.
3393
3394 :param architecture: The architecture of the new node.
3395@@ -187,6 +188,7 @@
3396 for the new node.
3397 :param mac_addresses: An iterable of MAC addresses that belong to
3398 the node.
3399+ :param domain: The domain the node should join.
3400 :param hostname: the desired hostname for the new node
3401 """
3402 # Check that there isn't already a node with one of our MAC
3403@@ -209,12 +211,15 @@
3404 'mac_addresses': mac_addresses,
3405 }
3406
3407+ if domain is not None:
3408+ data['domain'] = domain
3409+
3410 if hostname is not None:
3411 data['hostname'] = hostname.strip()
3412
3413 data_query_dict = get_overridden_query_dict(
3414- {}, data, AdminNodeWithMACAddressesForm.Meta.fields)
3415- form = AdminNodeWithMACAddressesForm(data_query_dict)
3416+ {}, data, AdminMachineWithMACAddressesForm.Meta.fields)
3417+ form = AdminMachineWithMACAddressesForm(data_query_dict)
3418 if form.is_valid():
3419 node = form.save()
3420 # We have to explicitly save the power parameters; the form
3421
3422=== modified file 'src/maasserver/rpc/regionservice.py'
3423--- src/maasserver/rpc/regionservice.py 2016-02-26 16:19:41 +0000
3424+++ src/maasserver/rpc/regionservice.py 2016-03-02 18:21:51 +0000
3425@@ -349,7 +349,7 @@
3426
3427 @region.CreateNode.responder
3428 def create_node(self, architecture, power_type, power_parameters,
3429- mac_addresses, hostname=None):
3430+ mac_addresses, domain=None, hostname=None):
3431 """create_node()
3432
3433 Implementation of
3434@@ -357,7 +357,7 @@
3435 """
3436 d = deferToDatabase(
3437 create_node, architecture, power_type, power_parameters,
3438- mac_addresses, hostname=hostname)
3439+ mac_addresses, domain=domain, hostname=hostname)
3440 d.addCallback(lambda node: {'system_id': node.system_id})
3441 return d
3442
3443
3444=== modified file 'src/maasserver/rpc/tests/test_nodes.py'
3445--- src/maasserver/rpc/tests/test_nodes.py 2016-02-26 18:48:26 +0000
3446+++ src/maasserver/rpc/tests/test_nodes.py 2016-03-02 18:21:51 +0000
3447@@ -153,6 +153,55 @@
3448 architecture, power_type, power_parameters,
3449 mac_addresses, hostname=hostname)
3450
3451+ def test__creates_node_with_explicit_domain(self):
3452+ self.prepare_rack_rpc()
3453+
3454+ mac_addresses = [
3455+ factory.make_mac_address() for _ in range(3)]
3456+ architecture = make_usable_architecture(self)
3457+ hostname = factory.make_hostname()
3458+ domain = factory.make_Domain()
3459+ power_type = random.choice(self.power_types)['name']
3460+ power_parameters = dumps({})
3461+
3462+ node = create_node(
3463+ architecture, power_type, power_parameters,
3464+ mac_addresses, domain=domain.name, hostname=hostname)
3465+
3466+ self.assertEqual(
3467+ (
3468+ architecture,
3469+ power_type,
3470+ {},
3471+ domain.id,
3472+ hostname,
3473+ ),
3474+ (
3475+ node.architecture,
3476+ node.power_type,
3477+ node.power_parameters,
3478+ node.domain.id,
3479+ node.hostname,
3480+ ))
3481+ self.expectThat(node.id, Not(Is(None)))
3482+ self.assertItemsEqual(
3483+ mac_addresses,
3484+ [nic.mac_address for nic in node.interface_set.all()])
3485+
3486+ def test__create_node_fails_with_invalid_domain(self):
3487+ self.prepare_rack_rpc()
3488+
3489+ mac_addresses = [
3490+ factory.make_mac_address() for _ in range(3)]
3491+ architecture = make_usable_architecture(self)
3492+ power_type = random.choice(self.power_types)['name']
3493+ power_parameters = dumps({})
3494+
3495+ with ExpectedException(ValidationError):
3496+ create_node(
3497+ architecture, power_type, power_parameters,
3498+ mac_addresses, factory.make_name('domain'))
3499+
3500 def test__raises_validation_errors_for_invalid_data(self):
3501 self.prepare_rack_rpc()
3502
3503
3504=== modified file 'src/maasserver/rpc/tests/test_regionservice.py'
3505--- src/maasserver/rpc/tests/test_regionservice.py 2016-02-27 02:32:00 +0000
3506+++ src/maasserver/rpc/tests/test_regionservice.py 2016-03-02 18:21:51 +0000
3507@@ -2416,9 +2416,10 @@
3508
3509 params = {
3510 'architecture': make_usable_architecture(self),
3511- 'power_type': factory.make_name("power_type"),
3512+ 'power_type': factory.make_name('power_type'),
3513 'power_parameters': dumps({}),
3514 'mac_addresses': [factory.make_mac_address()],
3515+ 'domain': factory.make_name('domain'),
3516 'hostname': None,
3517 }
3518
3519@@ -2431,6 +2432,7 @@
3520 MockCalledOnceWith(
3521 params['architecture'], params['power_type'],
3522 params['power_parameters'], params['mac_addresses'],
3523+ domain=params['domain'],
3524 hostname=params['hostname']))
3525 self.assertEqual(
3526 create_node_function.return_value.system_id,
3527
3528=== modified file 'src/maasserver/tests/test_forms_device.py'
3529--- src/maasserver/tests/test_forms_device.py 2016-02-26 18:39:26 +0000
3530+++ src/maasserver/tests/test_forms_device.py 2016-03-02 18:21:51 +0000
3531@@ -1,4 +1,4 @@
3532-# Copyright 2015 Canonical Ltd. This software is licensed under the
3533+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
3534 # GNU Affero General Public License version 3 (see the file LICENSE).
3535
3536 """Tests for device forms."""
3537@@ -16,26 +16,15 @@
3538 def test_contains_limited_set_of_fields(self):
3539 form = DeviceForm()
3540
3541- self.assertEqual(
3542+ self.assertItemsEqual(
3543 [
3544 'hostname',
3545+ 'domain',
3546 'parent',
3547+ 'disable_ipv4',
3548+ 'swap_size',
3549 ], list(form.fields))
3550
3551- def test_changes_device_hostname(self):
3552- device = factory.make_Device()
3553- hostname = factory.make_string()
3554-
3555- form = DeviceForm(
3556- data={
3557- 'hostname': hostname,
3558- },
3559- instance=device)
3560- form.save()
3561- reload_object(device)
3562-
3563- self.assertEqual(hostname, device.hostname)
3564-
3565 def test_changes_device_parent(self):
3566 device = factory.make_Device()
3567 parent = factory.make_Node()
3568
3569=== modified file 'src/maasserver/tests/test_forms_helpers.py'
3570--- src/maasserver/tests/test_forms_helpers.py 2016-02-02 14:20:45 +0000
3571+++ src/maasserver/tests/test_forms_helpers.py 2016-03-02 18:21:51 +0000
3572@@ -8,14 +8,17 @@
3573 from django.forms import CharField
3574 from maasserver.enum import BOOT_RESOURCE_TYPE
3575 from maasserver.forms import (
3576+ AdminMachineForm,
3577+ AdminMachineWithMACAddressesForm,
3578 AdminNodeForm,
3579- AdminNodeWithMACAddressesForm,
3580- get_node_create_form,
3581+ get_machine_create_form,
3582+ get_machine_edit_form,
3583 get_node_edit_form,
3584 list_all_usable_architectures,
3585 MAASModelForm,
3586+ MachineForm,
3587+ MachineWithPowerAndMACAddressesForm,
3588 NodeForm,
3589- NodeWithPowerAndMACAddressesForm,
3590 pick_default_architecture,
3591 remove_None_values,
3592 )
3593@@ -98,23 +101,31 @@
3594 def test_remove_None_values_leaves_empty_dict_untouched(self):
3595 self.assertEqual({}, remove_None_values({}))
3596
3597+ def test_get_machine_edit_form_returns_MachineForm_if_non_admin(self):
3598+ user = factory.make_User()
3599+ self.assertEqual(MachineForm, get_machine_edit_form(user))
3600+
3601+ def test_get_machine_edit_form_returns_AdminMachineForm_if_admin(self):
3602+ admin = factory.make_admin()
3603+ self.assertEqual(AdminMachineForm, get_machine_edit_form(admin))
3604+
3605 def test_get_node_edit_form_returns_NodeForm_if_non_admin(self):
3606 user = factory.make_User()
3607 self.assertEqual(NodeForm, get_node_edit_form(user))
3608
3609- def test_get_node_edit_form_returns_APIAdminNodeEdit_if_admin(self):
3610+ def test_get_node_edit_form_returns_AdminNodeForm_if_admin(self):
3611 admin = factory.make_admin()
3612 self.assertEqual(AdminNodeForm, get_node_edit_form(admin))
3613
3614- def test_get_node_create_form_if_non_admin(self):
3615+ def test_get_machine_create_form_if_non_admin(self):
3616 user = factory.make_User()
3617 self.assertEqual(
3618- NodeWithPowerAndMACAddressesForm, get_node_create_form(user))
3619+ MachineWithPowerAndMACAddressesForm, get_machine_create_form(user))
3620
3621- def test_get_node_create_form_if_admin(self):
3622+ def test_get_machine_create_form_if_admin(self):
3623 admin = factory.make_admin()
3624 self.assertEqual(
3625- AdminNodeWithMACAddressesForm, get_node_create_form(admin))
3626+ AdminMachineWithMACAddressesForm, get_machine_create_form(admin))
3627
3628
3629 class TestMAASModelForm(MAASTransactionServerTestCase):
3630
3631=== renamed file 'src/maasserver/tests/test_forms_node.py' => 'src/maasserver/tests/test_forms_machine.py'
3632--- src/maasserver/tests/test_forms_node.py 2016-03-01 19:02:08 +0000
3633+++ src/maasserver/tests/test_forms_machine.py 2016-03-02 18:21:51 +0000
3634@@ -1,4 +1,4 @@
3635-# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
3636+# Copyright 2016 Canonical Ltd. This software is licensed under the
3637 # GNU Affero General Public License version 3 (see the file LICENSE).
3638
3639 """Tests for node forms."""
3640@@ -6,7 +6,6 @@
3641 __all__ = []
3642
3643 from crochet import TimeoutError
3644-from django.core.exceptions import ValidationError
3645 from maasserver import forms
3646 from maasserver.clusterrpc.power_parameters import get_power_type_choices
3647 from maasserver.clusterrpc.testing.osystems import (
3648@@ -14,13 +13,11 @@
3649 make_rpc_release,
3650 )
3651 from maasserver.forms import (
3652- AdminNodeForm,
3653+ AdminMachineForm,
3654 BLANK_CHOICE,
3655- NodeChoiceField,
3656- NodeForm,
3657+ MachineForm,
3658 pick_default_architecture,
3659 )
3660-from maasserver.models import Node
3661 from maasserver.testing.architecture import (
3662 make_usable_architecture,
3663 patch_usable_architectures,
3664@@ -32,21 +29,21 @@
3665 patch_usable_osystems,
3666 )
3667 from maasserver.testing.testcase import MAASServerTestCase
3668-from maasserver.utils.orm import reload_object
3669 from provisioningserver.rpc.exceptions import (
3670 NoConnectionsAvailable,
3671 NoSuchOperatingSystem,
3672 )
3673
3674
3675-class TestNodeForm(MAASServerTestCase):
3676+class TestMachineForm(MAASServerTestCase):
3677
3678 def test_contains_limited_set_of_fields(self):
3679- form = NodeForm()
3680+ form = MachineForm()
3681
3682- self.assertEqual(
3683+ self.assertItemsEqual(
3684 [
3685 'hostname',
3686+ 'domain',
3687 'architecture',
3688 'osystem',
3689 'distro_series',
3690@@ -57,24 +54,9 @@
3691 'hwe_kernel',
3692 ], list(form.fields))
3693
3694- def test_changes_node(self):
3695- node = factory.make_Node()
3696- hostname = factory.make_string()
3697- patch_usable_architectures(self, [node.architecture])
3698-
3699- form = NodeForm(
3700- data={
3701- 'hostname': hostname,
3702- 'architecture': make_usable_architecture(self),
3703- },
3704- instance=node)
3705- form.save()
3706-
3707- self.assertEqual(hostname, node.hostname)
3708-
3709 def test_accepts_usable_architecture(self):
3710 arch = make_usable_architecture(self)
3711- form = NodeForm(data={
3712+ form = MachineForm(data={
3713 'hostname': factory.make_name('host'),
3714 'architecture': arch,
3715 })
3716@@ -82,7 +64,7 @@
3717
3718 def test_rejects_unusable_architecture(self):
3719 patch_usable_architectures(self)
3720- form = NodeForm(data={
3721+ form = MachineForm(data={
3722 'hostname': factory.make_name('host'),
3723 'architecture': factory.make_name('arch'),
3724 })
3725@@ -92,7 +74,7 @@
3726 def test_starts_with_default_architecture(self):
3727 arches = sorted([factory.make_name('arch') for _ in range(5)])
3728 patch_usable_architectures(self, arches)
3729- form = NodeForm()
3730+ form = MachineForm()
3731 self.assertEqual(
3732 pick_default_architecture(arches),
3733 form.fields['architecture'].initial)
3734@@ -102,7 +84,7 @@
3735 node = factory.make_Node(
3736 owner=self.logged_in_user)
3737 osystem = make_usable_osystem(self)
3738- form = NodeForm(data={
3739+ form = MachineForm(data={
3740 'hostname': factory.make_name('host'),
3741 'architecture': make_usable_architecture(self),
3742 'osystem': osystem['name'],
3743@@ -114,12 +96,12 @@
3744
3745 def test_form_validates_min_hwe_kernel_by_passing_invalid_config(self):
3746 node = factory.make_Node(min_hwe_kernel='hwe-t')
3747- form = NodeForm(instance=node)
3748+ form = MachineForm(instance=node)
3749 self.assertEqual(form.is_valid(), False)
3750
3751 def test_adds_blank_default_when_no_arches_available(self):
3752 patch_usable_architectures(self, [])
3753- form = NodeForm()
3754+ form = MachineForm()
3755 self.assertEqual(
3756 [BLANK_CHOICE],
3757 form.fields['architecture'].choices)
3758@@ -128,7 +110,7 @@
3759 self.client_log_in()
3760 node = factory.make_Node(owner=self.logged_in_user)
3761 osystem = make_usable_osystem(self)
3762- form = NodeForm(data={
3763+ form = MachineForm(data={
3764 'hostname': factory.make_name('host'),
3765 'architecture': make_usable_architecture(self),
3766 'osystem': osystem['name'],
3767@@ -140,7 +122,7 @@
3768 self.client_log_in()
3769 node = factory.make_Node(owner=self.logged_in_user)
3770 patch_usable_osystems(self)
3771- form = NodeForm(data={
3772+ form = MachineForm(data={
3773 'hostname': factory.make_name('host'),
3774 'architecture': make_usable_architecture(self),
3775 'osystem': factory.make_name('os'),
3776@@ -154,7 +136,7 @@
3777 node = factory.make_Node(owner=self.logged_in_user)
3778 osystems = [make_osystem_with_releases(self) for _ in range(5)]
3779 patch_usable_osystems(self, osystems)
3780- form = NodeForm(instance=node)
3781+ form = MachineForm(instance=node)
3782 self.assertEqual(
3783 '',
3784 form.fields['osystem'].initial)
3785@@ -164,7 +146,7 @@
3786 node = factory.make_Node(owner=self.logged_in_user)
3787 osystem = make_usable_osystem(self)
3788 release = osystem['default_release']
3789- form = NodeForm(data={
3790+ form = MachineForm(data={
3791 'hostname': factory.make_name('host'),
3792 'architecture': make_usable_architecture(self),
3793 'osystem': osystem['name'],
3794@@ -178,7 +160,7 @@
3795 node = factory.make_Node(owner=self.logged_in_user)
3796 osystem = make_usable_osystem(self)
3797 release = factory.make_name('release')
3798- form = NodeForm(data={
3799+ form = MachineForm(data={
3800 'hostname': factory.make_name('host'),
3801 'architecture': make_usable_architecture(self),
3802 'osystem': osystem['name'],
3803@@ -194,7 +176,7 @@
3804 release = factory.make_name('release')
3805 make_usable_osystem(
3806 self, releases=[release + '6', release + '0', release + '3'])
3807- form = NodeForm(data={
3808+ form = MachineForm(data={
3809 'hostname': factory.make_name('host'),
3810 'architecture': make_usable_architecture(self),
3811 },
3812@@ -210,7 +192,7 @@
3813 self,
3814 osystem_name='ubuntu',
3815 releases=['trusty'])
3816- form = NodeForm(data={
3817+ form = MachineForm(data={
3818 'hostname': factory.make_name('host'),
3819 'architecture': make_usable_architecture(self),
3820 },
3821@@ -223,7 +205,7 @@
3822 node = factory.make_Node(owner=self.logged_in_user)
3823 osystems = [make_osystem_with_releases(self) for _ in range(5)]
3824 patch_usable_osystems(self, osystems)
3825- form = NodeForm(instance=node)
3826+ form = MachineForm(instance=node)
3827 self.assertEqual(
3828 '',
3829 form.fields['distro_series'].initial)
3830@@ -234,7 +216,7 @@
3831 osystem = make_usable_osystem(self)
3832 release = osystem['default_release']
3833 invalid = factory.make_name('invalid_os')
3834- form = NodeForm(data={
3835+ form = MachineForm(data={
3836 'hostname': factory.make_name('host'),
3837 'architecture': make_usable_architecture(self),
3838 'osystem': osystem['name'],
3839@@ -253,7 +235,7 @@
3840 license_key = factory.make_name('key')
3841 mock_validate = self.patch(forms, 'validate_license_key')
3842 mock_validate.return_value = False
3843- form = NodeForm(data={
3844+ form = MachineForm(data={
3845 'hostname': factory.make_name('host'),
3846 'architecture': make_usable_architecture(self),
3847 'osystem': osystem['name'],
3848@@ -273,7 +255,7 @@
3849 license_key = factory.make_name('key')
3850 mock_validate_for = self.patch(forms, 'validate_license_key_for')
3851 mock_validate_for.return_value = False
3852- form = NodeForm(data={
3853+ form = MachineForm(data={
3854 'architecture': make_usable_architecture(self),
3855 'osystem': osystem['name'],
3856 'distro_series': '%s/%s*' % (osystem['name'], release['name']),
3857@@ -292,7 +274,7 @@
3858 license_key = factory.make_name('key')
3859 mock_validate_for = self.patch(forms, 'validate_license_key_for')
3860 mock_validate_for.side_effect = NoConnectionsAvailable()
3861- form = NodeForm(data={
3862+ form = MachineForm(data={
3863 'architecture': make_usable_architecture(self),
3864 'osystem': osystem['name'],
3865 'distro_series': '%s/%s*' % (osystem['name'], release['name']),
3866@@ -311,7 +293,7 @@
3867 license_key = factory.make_name('key')
3868 mock_validate_for = self.patch(forms, 'validate_license_key_for')
3869 mock_validate_for.side_effect = TimeoutError()
3870- form = NodeForm(data={
3871+ form = MachineForm(data={
3872 'architecture': make_usable_architecture(self),
3873 'osystem': osystem['name'],
3874 'distro_series': '%s/%s*' % (osystem['name'], release['name']),
3875@@ -330,7 +312,7 @@
3876 license_key = factory.make_name('key')
3877 mock_validate_for = self.patch(forms, 'validate_license_key_for')
3878 mock_validate_for.side_effect = NoSuchOperatingSystem()
3879- form = NodeForm(data={
3880+ form = MachineForm(data={
3881 'architecture': make_usable_architecture(self),
3882 'osystem': osystem['name'],
3883 'distro_series': '%s/%s*' % (osystem['name'], release['name']),
3884@@ -340,46 +322,18 @@
3885 self.assertFalse(form.is_valid())
3886 self.assertItemsEqual(['license_key'], form._errors.keys())
3887
3888- def test_obeys_disable_ipv4_if_given(self):
3889- setting = factory.pick_bool()
3890- form = NodeForm(
3891- data={
3892- 'architecture': make_usable_architecture(self),
3893- 'disable_ipv4': setting,
3894- })
3895- node = form.save()
3896- self.assertEqual(setting, node.disable_ipv4)
3897-
3898- def test_takes_missing_disable_ipv4_as_False_in_UI(self):
3899- form = NodeForm(
3900- instance=factory.make_Node(disable_ipv4=True),
3901- data={
3902- 'architecture': make_usable_architecture(self),
3903- 'ui_submission': True,
3904- })
3905- node = form.save()
3906- self.assertFalse(node.disable_ipv4)
3907-
3908- def test_takes_missing_disable_ipv4_as_Unchanged_in_API(self):
3909- form = NodeForm(
3910- instance=factory.make_Node(disable_ipv4=True),
3911- data={
3912- 'architecture': make_usable_architecture(self),
3913- })
3914- node = form.save()
3915- self.assertTrue(node.disable_ipv4)
3916-
3917-
3918-class TestAdminNodeForm(MAASServerTestCase):
3919-
3920- def test_AdminNodeForm_contains_limited_set_of_fields(self):
3921+
3922+class TestAdminMachineForm(MAASServerTestCase):
3923+
3924+ def test_AdminMachineForm_contains_limited_set_of_fields(self):
3925 self.client_log_in()
3926 node = factory.make_Node(owner=self.logged_in_user)
3927- form = AdminNodeForm(instance=node)
3928+ form = AdminMachineForm(instance=node)
3929
3930- self.assertEqual(
3931+ self.assertItemsEqual(
3932 [
3933 'hostname',
3934+ 'domain',
3935 'architecture',
3936 'osystem',
3937 'distro_series',
3938@@ -396,59 +350,24 @@
3939 ],
3940 list(form.fields))
3941
3942- def test_AdminNodeForm_initialises_zone(self):
3943- # The zone field uses "to_field_name", so that it can refer to a zone
3944- # by name instead of by ID. A bug in Django breaks initialisation
3945- # from an instance: the field tries to initialise the field using a
3946- # zone's ID instead of its name, and ends up reverting to the default.
3947- # The code must work around this bug.
3948- zone = factory.make_Zone()
3949- node = factory.make_Node(zone=zone)
3950- # We'll create a form that makes a change, but not to the zone.
3951- data = {'hostname': factory.make_name('host')}
3952- form = AdminNodeForm(instance=node, data=data)
3953- # The Django bug would stop the initial field value from being set,
3954- # but the workaround ensures that it is initialised.
3955- self.assertEqual(zone.name, form.initial['zone'])
3956-
3957- def test_AdminNodeForm_changes_node(self):
3958- node = factory.make_Node()
3959- zone = factory.make_Zone()
3960- hostname = factory.make_string()
3961- power_type = factory.pick_power_type()
3962- form = AdminNodeForm(
3963- data={
3964- 'hostname': hostname,
3965- 'power_type': power_type,
3966- 'architecture': make_usable_architecture(self),
3967- 'zone': zone.name,
3968- },
3969- instance=node)
3970- form.save()
3971-
3972- node = reload_object(node)
3973- self.assertEqual(
3974- (node.hostname, node.power_type, node.zone),
3975- (hostname, power_type, zone))
3976-
3977- def test_AdminNodeForm_populates_power_type_choices(self):
3978- form = AdminNodeForm()
3979+ def test_AdminMachineForm_populates_power_type_choices(self):
3980+ form = AdminMachineForm()
3981 self.assertEqual(
3982 [''] + [choice[0] for choice in get_power_type_choices()],
3983 [choice[0] for choice in form.fields['power_type'].choices])
3984
3985- def test_AdminNodeForm_populates_power_type_initial(self):
3986+ def test_AdminMachineForm_populates_power_type_initial(self):
3987 node = factory.make_Node()
3988- form = AdminNodeForm(instance=node)
3989+ form = AdminMachineForm(instance=node)
3990 self.assertEqual(node.power_type, form.fields['power_type'].initial)
3991
3992- def test_AdminNodeForm_changes_node_with_skip_check(self):
3993+ def test_AdminMachineForm_changes_node_with_skip_check(self):
3994 node = factory.make_Node()
3995 hostname = factory.make_string()
3996 power_type = factory.pick_power_type()
3997 power_parameters_field = factory.make_string()
3998 arch = make_usable_architecture(self)
3999- form = AdminNodeForm(
4000+ form = AdminMachineForm(
4001 data={
4002 'hostname': hostname,
4003 'architecture': arch,
4004@@ -462,37 +381,3 @@
4005 self.assertEqual(
4006 (hostname, power_type, {'field': power_parameters_field}),
4007 (node.hostname, node.power_type, node.power_parameters))
4008-
4009-
4010-class TestNodeChoiceField(MAASServerTestCase):
4011- def test_allows_selecting_by_system_id(self):
4012- node = factory.make_Node()
4013- for _ in range(3):
4014- factory.make_Node()
4015- node_field = NodeChoiceField(Node.objects.filter())
4016- self.assertEqual(node, node_field.clean(node.system_id))
4017-
4018- def test_allows_selecting_by_hostname(self):
4019- node = factory.make_Node()
4020- for _ in range(3):
4021- factory.make_Node()
4022- node_field = NodeChoiceField(Node.objects.filter())
4023- self.assertEqual(node, node_field.clean(node.hostname))
4024-
4025- def test_raises_exception_when_not_found(self):
4026- for _ in range(3):
4027- factory.make_Node()
4028- node_field = NodeChoiceField(Node.objects.filter())
4029- self.assertRaises(
4030- ValidationError, node_field.clean, factory.make_name('query'))
4031-
4032- def test_works_with_multiple_entries_in_queryset(self):
4033- # Regression test for lp:1551399
4034- vlan = factory.make_VLAN()
4035- node = factory.make_Node_with_Interface_on_Subnet(vlan=vlan)
4036- factory.make_Interface(node=node, vlan=vlan)
4037- qs = Node.objects.filter_by_vids([vlan.vid])
4038- node_field = NodeChoiceField(qs)
4039- # Double check that we have duplicated entires
4040- self.assertEqual(2, len(qs.filter(system_id=node.system_id)))
4041- self.assertEqual(node, node_field.clean(node.system_id))
4042
4043=== renamed file 'src/maasserver/tests/test_forms_nodewithmacaddresses.py' => 'src/maasserver/tests/test_forms_machinewithmacaddresses.py'
4044--- src/maasserver/tests/test_forms_nodewithmacaddresses.py 2016-02-02 14:20:45 +0000
4045+++ src/maasserver/tests/test_forms_machinewithmacaddresses.py 2016-03-02 18:21:51 +0000
4046@@ -1,13 +1,13 @@
4047-# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
4048+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
4049 # GNU Affero General Public License version 3 (see the file LICENSE).
4050
4051-"""Tests for `NodeWithMACAddressesForm`."""
4052+"""Tests for `MachineWithMACAddressesForm`."""
4053
4054 __all__ = []
4055
4056 from django.http import QueryDict
4057 from maasserver.enum import INTERFACE_TYPE
4058-from maasserver.forms import NodeWithMACAddressesForm
4059+from maasserver.forms import MachineWithMACAddressesForm
4060 from maasserver.testing.architecture import (
4061 make_usable_architecture,
4062 patch_usable_architectures,
4063@@ -17,7 +17,7 @@
4064 from testtools.matchers import Contains
4065
4066
4067-class NodeWithMACAddressesFormTest(MAASServerTestCase):
4068+class MachineWithMACAddressesFormTest(MAASServerTestCase):
4069
4070 def get_QueryDict(self, params):
4071 query_dict = QueryDict('', mutable=True)
4072@@ -47,7 +47,7 @@
4073
4074 def test__valid(self):
4075 architecture = make_usable_architecture(self)
4076- form = NodeWithMACAddressesForm(
4077+ form = MachineWithMACAddressesForm(
4078 data=self.make_params(
4079 mac_addresses=['aa:bb:cc:dd:ee:ff', '9a:bb:c3:33:e5:7f'],
4080 architecture=architecture))
4081@@ -62,7 +62,7 @@
4082 # If the form only has one (invalid) MAC address field to validate,
4083 # the error message in form.errors['mac_addresses'] is the
4084 # message from the field's validation error.
4085- form = NodeWithMACAddressesForm(
4086+ form = MachineWithMACAddressesForm(
4087 data=self.make_params(mac_addresses=['invalid']))
4088
4089 self.assertFalse(form.is_valid())
4090@@ -75,7 +75,7 @@
4091 # If the form has multiple MAC address fields to validate,
4092 # if one or more fields are invalid, a single error message is
4093 # present in form.errors['mac_addresses'] after validation.
4094- form = NodeWithMACAddressesForm(
4095+ form = MachineWithMACAddressesForm(
4096 data=self.make_params(mac_addresses=['invalid_1', 'invalid_2']))
4097
4098 self.assertFalse(form.is_valid())
4099@@ -92,7 +92,7 @@
4100 node = factory.make_Node_with_Interface_on_Subnet(
4101 address='aa:bb:cc:dd:ee:ff')
4102 architecture = make_usable_architecture(self)
4103- form = NodeWithMACAddressesForm(
4104+ form = MachineWithMACAddressesForm(
4105 data=self.make_params(
4106 mac_addresses=['aa:bb:cc:dd:ee:ff', '9a:bb:c3:33:e5:7f'],
4107 architecture=architecture), instance=node)
4108@@ -107,7 +107,7 @@
4109 factory.make_Node_with_Interface_on_Subnet(address='aa:bb:cc:dd:ee:ff')
4110 architecture = make_usable_architecture(self)
4111 node = factory.make_Node_with_Interface_on_Subnet()
4112- form = NodeWithMACAddressesForm(
4113+ form = MachineWithMACAddressesForm(
4114 data=self.make_params(
4115 mac_addresses=['aa:bb:cc:dd:ee:ff', '9a:bb:c3:33:e5:7f'],
4116 architecture=architecture), instance=node)
4117@@ -119,7 +119,7 @@
4118 factory.make_Interface(
4119 INTERFACE_TYPE.UNKNOWN, mac_address='aa:bb:cc:dd:ee:ff')
4120 architecture = make_usable_architecture(self)
4121- form = NodeWithMACAddressesForm(
4122+ form = MachineWithMACAddressesForm(
4123 data=self.make_params(
4124 mac_addresses=['aa:bb:cc:dd:ee:ff', '9a:bb:c3:33:e5:7f'],
4125 architecture=architecture))
4126@@ -132,7 +132,7 @@
4127
4128 def test__empty(self):
4129 # Empty values in the list of MAC addresses are simply ignored.
4130- form = NodeWithMACAddressesForm(
4131+ form = MachineWithMACAddressesForm(
4132 data=self.make_params(
4133 mac_addresses=[factory.make_mac_address(), '']))
4134
4135@@ -140,7 +140,7 @@
4136
4137 def test__save(self):
4138 macs = ['aa:bb:cc:dd:ee:ff', '9a:bb:c3:33:e5:7f']
4139- form = NodeWithMACAddressesForm(
4140+ form = MachineWithMACAddressesForm(
4141 data=self.make_params(mac_addresses=macs))
4142 node = form.save()
4143
4144@@ -150,13 +150,13 @@
4145 [nic.mac_address for nic in node.interface_set.all()])
4146
4147 def test_form_without_hostname_generates_hostname(self):
4148- form = NodeWithMACAddressesForm(data=self.make_params(hostname=''))
4149+ form = MachineWithMACAddressesForm(data=self.make_params(hostname=''))
4150 node = form.save()
4151 self.assertTrue(len(node.hostname) > 0)
4152
4153 def test_form_with_ip_based_hostname_generates_hostname(self):
4154 ip_based_hostname = '192-168-12-10.maas'
4155- form = NodeWithMACAddressesForm(
4156+ form = MachineWithMACAddressesForm(
4157 data=self.make_params(hostname=ip_based_hostname))
4158 node = form.save()
4159 self.assertNotEqual(ip_based_hostname, node.hostname)
4160
4161=== added file 'src/maasserver/tests/test_forms_node.py'
4162--- src/maasserver/tests/test_forms_node.py 1970-01-01 00:00:00 +0000
4163+++ src/maasserver/tests/test_forms_node.py 2016-03-02 18:21:51 +0000
4164@@ -0,0 +1,204 @@
4165+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
4166+# GNU Affero General Public License version 3 (see the file LICENSE).
4167+
4168+"""Tests for node forms."""
4169+
4170+__all__ = []
4171+
4172+from django.core.exceptions import ValidationError
4173+from maasserver.forms import (
4174+ AdminNodeForm,
4175+ NodeChoiceField,
4176+ NodeForm,
4177+)
4178+from maasserver.models import Node
4179+from maasserver.testing.architecture import (
4180+ make_usable_architecture,
4181+ patch_usable_architectures,
4182+)
4183+from maasserver.testing.factory import factory
4184+from maasserver.testing.testcase import MAASServerTestCase
4185+from maasserver.utils.orm import reload_object
4186+
4187+
4188+class TestNodeForm(MAASServerTestCase):
4189+ def test_contains_limited_set_of_fields(self):
4190+ form = NodeForm()
4191+
4192+ self.assertItemsEqual(
4193+ [
4194+ 'hostname',
4195+ 'domain',
4196+ 'disable_ipv4',
4197+ 'swap_size',
4198+ ], list(form.fields))
4199+
4200+ def test_accepts_hostname(self):
4201+ machine = factory.make_Node()
4202+ hostname = factory.make_string()
4203+ patch_usable_architectures(self, [machine.architecture])
4204+
4205+ form = NodeForm(
4206+ data={
4207+ 'hostname': hostname,
4208+ 'architecture': make_usable_architecture(self),
4209+ },
4210+ instance=machine)
4211+ form.save()
4212+
4213+ self.assertEqual(hostname, machine.hostname)
4214+
4215+ def test_accepts_domain_by_name(self):
4216+ machine = factory.make_Node()
4217+ domain = factory.make_Domain()
4218+ patch_usable_architectures(self, [machine.architecture])
4219+
4220+ form = NodeForm(
4221+ data={
4222+ 'domain': domain.name,
4223+ },
4224+ instance=machine)
4225+ form.save()
4226+
4227+ self.assertEqual(domain.name, machine.domain.name)
4228+
4229+ def test_accepts_domain_by_id(self):
4230+ machine = factory.make_Node()
4231+ domain = factory.make_Domain()
4232+ patch_usable_architectures(self, [machine.architecture])
4233+
4234+ form = NodeForm(
4235+ data={
4236+ 'domain': domain.id,
4237+ },
4238+ instance=machine)
4239+ form.save()
4240+
4241+ self.assertEqual(domain.name, machine.domain.name)
4242+
4243+ def test_validates_domain(self):
4244+ machine = factory.make_Node()
4245+ patch_usable_architectures(self, [machine.architecture])
4246+
4247+ form = NodeForm(
4248+ data={
4249+ 'domain': factory.make_name('domain'),
4250+ },
4251+ instance=machine)
4252+
4253+ self.assertFalse(form.is_valid())
4254+
4255+ def test_obeys_disable_ipv4_if_given(self):
4256+ setting = factory.pick_bool()
4257+ form = NodeForm(
4258+ data={
4259+ 'architecture': make_usable_architecture(self),
4260+ 'disable_ipv4': setting,
4261+ })
4262+ node = form.save()
4263+ self.assertEqual(setting, node.disable_ipv4)
4264+
4265+ def test_takes_missing_disable_ipv4_as_False_in_UI(self):
4266+ form = NodeForm(
4267+ instance=factory.make_Node(disable_ipv4=True),
4268+ data={
4269+ 'architecture': make_usable_architecture(self),
4270+ 'ui_submission': True,
4271+ })
4272+ node = form.save()
4273+ self.assertFalse(node.disable_ipv4)
4274+
4275+ def test_takes_missing_disable_ipv4_as_Unchanged_in_API(self):
4276+ form = NodeForm(
4277+ instance=factory.make_Node(disable_ipv4=True),
4278+ data={
4279+ 'architecture': make_usable_architecture(self),
4280+ })
4281+ node = form.save()
4282+ self.assertTrue(node.disable_ipv4)
4283+
4284+
4285+class TestAdminNodeForm(MAASServerTestCase):
4286+
4287+ def test_contains_limited_set_of_fields(self):
4288+ self.client_log_in()
4289+ node = factory.make_Node(owner=self.logged_in_user)
4290+ form = AdminNodeForm(instance=node)
4291+
4292+ self.assertItemsEqual(
4293+ [
4294+ 'hostname',
4295+ 'domain',
4296+ 'disable_ipv4',
4297+ 'swap_size',
4298+ 'cpu_count',
4299+ 'memory',
4300+ 'zone',
4301+ ],
4302+ list(form.fields))
4303+
4304+ def test_initialises_zone(self):
4305+ # The zone field uses "to_field_name", so that it can refer to a zone
4306+ # by name instead of by ID. A bug in Django breaks initialisation
4307+ # from an instance: the field tries to initialise the field using a
4308+ # zone's ID instead of its name, and ends up reverting to the default.
4309+ # The code must work around this bug.
4310+ zone = factory.make_Zone()
4311+ node = factory.make_Node(zone=zone)
4312+ # We'll create a form that makes a change, but not to the zone.
4313+ data = {'hostname': factory.make_name('host')}
4314+ form = AdminNodeForm(instance=node, data=data)
4315+ # The Django bug would stop the initial field value from being set,
4316+ # but the workaround ensures that it is initialised.
4317+ self.assertEqual(zone.name, form.initial['zone'])
4318+
4319+ def test_changes_zone(self):
4320+ node = factory.make_Node()
4321+ zone = factory.make_Zone()
4322+ hostname = factory.make_string()
4323+ form = AdminNodeForm(
4324+ data={
4325+ 'hostname': hostname,
4326+ 'architecture': make_usable_architecture(self),
4327+ 'zone': zone.name,
4328+ },
4329+ instance=node)
4330+ form.save()
4331+
4332+ node = reload_object(node)
4333+ self.assertEqual(node.hostname, hostname)
4334+ self.assertEqual(node.zone, zone)
4335+
4336+
4337+class TestNodeChoiceField(MAASServerTestCase):
4338+ def test_allows_selecting_by_system_id(self):
4339+ node = factory.make_Node()
4340+ for _ in range(3):
4341+ factory.make_Node()
4342+ node_field = NodeChoiceField(Node.objects.filter())
4343+ self.assertEqual(node, node_field.clean(node.system_id))
4344+
4345+ def test_allows_selecting_by_hostname(self):
4346+ node = factory.make_Node()
4347+ for _ in range(3):
4348+ factory.make_Node()
4349+ node_field = NodeChoiceField(Node.objects.filter())
4350+ self.assertEqual(node, node_field.clean(node.hostname))
4351+
4352+ def test_raises_exception_when_not_found(self):
4353+ for _ in range(3):
4354+ factory.make_Node()
4355+ node_field = NodeChoiceField(Node.objects.filter())
4356+ self.assertRaises(
4357+ ValidationError, node_field.clean, factory.make_name('query'))
4358+
4359+ def test_works_with_multiple_entries_in_queryset(self):
4360+ # Regression test for lp:1551399
4361+ vlan = factory.make_VLAN()
4362+ node = factory.make_Node_with_Interface_on_Subnet(vlan=vlan)
4363+ factory.make_Interface(node=node, vlan=vlan)
4364+ qs = Node.objects.filter_by_vids([vlan.vid])
4365+ node_field = NodeChoiceField(qs)
4366+ # Double check that we have duplicated entires
4367+ self.assertEqual(2, len(qs.filter(system_id=node.system_id)))
4368+ self.assertEqual(node, node_field.clean(node.system_id))
4369
4370=== modified file 'src/maasserver/websockets/handlers/controller.py'
4371--- src/maasserver/websockets/handlers/controller.py 2016-03-01 19:02:08 +0000
4372+++ src/maasserver/websockets/handlers/controller.py 2016-03-02 18:21:51 +0000
4373@@ -8,7 +8,7 @@
4374 ]
4375
4376 from maasserver.enum import NODE_PERMISSION
4377-from maasserver.forms import AdminNodeWithMACAddressesForm
4378+from maasserver.forms import AdminMachineWithMACAddressesForm
4379 from maasserver.models.node import Node
4380 from maasserver.websockets.handlers.machine import MachineHandler
4381 from maasserver.websockets.handlers.node import node_prefetch
4382@@ -36,7 +36,7 @@
4383 'link_subnet',
4384 'unlink_subnet',
4385 ]
4386- form = AdminNodeWithMACAddressesForm
4387+ form = AdminMachineWithMACAddressesForm
4388 exclude = [
4389 "status_expires",
4390 "parent",
4391
4392=== modified file 'src/maasserver/websockets/handlers/machine.py'
4393--- src/maasserver/websockets/handlers/machine.py 2016-03-02 10:02:12 +0000
4394+++ src/maasserver/websockets/handlers/machine.py 2016-03-02 18:21:51 +0000
4395@@ -19,7 +19,7 @@
4396 from maasserver.exceptions import NodeActionError
4397 from maasserver.forms import (
4398 AddPartitionForm,
4399- AdminNodeWithMACAddressesForm,
4400+ AdminMachineWithMACAddressesForm,
4401 CreateBcacheForm,
4402 CreateCacheSetForm,
4403 CreateLogicalVolumeForm,
4404@@ -118,7 +118,7 @@
4405 'create_logical_volume',
4406 'set_boot_disk',
4407 ]
4408- form = AdminNodeWithMACAddressesForm
4409+ form = AdminMachineWithMACAddressesForm
4410 exclude = [
4411 "status_expires",
4412 "parent",
4413@@ -202,7 +202,7 @@
4414 def get_form_class(self, action):
4415 """Return the form class used for `action`."""
4416 if action in ("create", "update"):
4417- return AdminNodeWithMACAddressesForm
4418+ return AdminMachineWithMACAddressesForm
4419 else:
4420 raise HandlerError("Unknown action: %s" % action)
4421
4422
4423=== modified file 'src/maasserver/websockets/handlers/tests/test_controller.py'
4424--- src/maasserver/websockets/handlers/tests/test_controller.py 2016-03-01 19:02:08 +0000
4425+++ src/maasserver/websockets/handlers/tests/test_controller.py 2016-03-02 18:21:51 +0000
4426@@ -6,7 +6,7 @@
4427 __all__ = []
4428
4429 from maasserver.enum import NODE_TYPE
4430-from maasserver.forms import AdminNodeWithMACAddressesForm
4431+from maasserver.forms import AdminMachineWithMACAddressesForm
4432 from maasserver.testing.factory import factory
4433 from maasserver.testing.testcase import MAASServerTestCase
4434 from maasserver.websockets.handlers.controller import ControllerHandler
4435@@ -63,12 +63,12 @@
4436 user = factory.make_admin()
4437 handler = ControllerHandler(user, {})
4438 self.assertEqual(
4439- AdminNodeWithMACAddressesForm,
4440+ AdminMachineWithMACAddressesForm,
4441 handler.get_form_class("create"))
4442
4443 def test_get_form_class_for_update(self):
4444 user = factory.make_admin()
4445 handler = ControllerHandler(user, {})
4446 self.assertEqual(
4447- AdminNodeWithMACAddressesForm,
4448+ AdminMachineWithMACAddressesForm,
4449 handler.get_form_class("update"))
4450
4451=== modified file 'src/maasserver/websockets/handlers/tests/test_machine.py'
4452--- src/maasserver/websockets/handlers/tests/test_machine.py 2016-03-02 02:22:45 +0000
4453+++ src/maasserver/websockets/handlers/tests/test_machine.py 2016-03-02 18:21:51 +0000
4454@@ -26,7 +26,7 @@
4455 NODE_TYPE,
4456 )
4457 from maasserver.exceptions import NodeActionError
4458-from maasserver.forms import AdminNodeWithMACAddressesForm
4459+from maasserver.forms import AdminMachineWithMACAddressesForm
4460 from maasserver.models.blockdevice import BlockDevice
4461 from maasserver.models.cacheset import CacheSet
4462 from maasserver.models.config import Config
4463@@ -898,14 +898,14 @@
4464 user = factory.make_admin()
4465 handler = MachineHandler(user, {})
4466 self.assertEqual(
4467- AdminNodeWithMACAddressesForm,
4468+ AdminMachineWithMACAddressesForm,
4469 handler.get_form_class("create"))
4470
4471 def test_get_form_class_for_update(self):
4472 user = factory.make_admin()
4473 handler = MachineHandler(user, {})
4474 self.assertEqual(
4475- AdminNodeWithMACAddressesForm,
4476+ AdminMachineWithMACAddressesForm,
4477 handler.get_form_class("update"))
4478
4479 def test_get_form_class_raises_error_for_unknown_action(self):
4480
4481=== modified file 'src/maasserver/websockets/tests/test_base.py'
4482--- src/maasserver/websockets/tests/test_base.py 2016-02-26 18:39:26 +0000
4483+++ src/maasserver/websockets/tests/test_base.py 2016-03-02 18:21:51 +0000
4484@@ -9,8 +9,8 @@
4485
4486 from django.db.models.query import QuerySet
4487 from maasserver.forms import (
4488- AdminNodeForm,
4489- AdminNodeWithMACAddressesForm,
4490+ AdminMachineForm,
4491+ AdminMachineWithMACAddressesForm,
4492 )
4493 from maasserver.models.node import Node
4494 from maasserver.models.zone import Zone
4495@@ -446,7 +446,7 @@
4496 arch = make_usable_architecture(self)
4497 handler = self.make_nodes_handler(
4498 fields=['hostname', 'architecture'],
4499- form=AdminNodeWithMACAddressesForm)
4500+ form=AdminMachineWithMACAddressesForm)
4501 json_obj = handler.create({
4502 "hostname": hostname,
4503 "architecture": arch,
4504@@ -464,7 +464,7 @@
4505 fields=['hostname', 'architecture'])
4506 self.patch(
4507 handler,
4508- "get_form_class").return_value = AdminNodeWithMACAddressesForm
4509+ "get_form_class").return_value = AdminMachineWithMACAddressesForm
4510 json_obj = handler.create({
4511 "hostname": hostname,
4512 "architecture": arch,
4513@@ -495,7 +495,7 @@
4514 arch = make_usable_architecture(self)
4515 handler = self.make_nodes_handler(
4516 fields=['hostname', 'architecture'],
4517- form=AdminNodeWithMACAddressesForm)
4518+ form=AdminMachineWithMACAddressesForm)
4519 self.assertRaises(
4520 HandlerValidationError, handler.create, {
4521 "hostname": hostname,
4522@@ -521,7 +521,7 @@
4523 node = factory.make_Node(architecture=arch)
4524 hostname = factory.make_name("hostname")
4525 handler = self.make_nodes_handler(
4526- fields=['hostname'], form=AdminNodeForm)
4527+ fields=['hostname'], form=AdminMachineForm)
4528 json_obj = handler.update({
4529 "system_id": node.system_id,
4530 "hostname": hostname,
4531@@ -539,7 +539,7 @@
4532 handler = self.make_nodes_handler(fields=['hostname'])
4533 self.patch(
4534 handler,
4535- "get_form_class").return_value = AdminNodeForm
4536+ "get_form_class").return_value = AdminMachineForm
4537 json_obj = handler.update({
4538 "system_id": node.system_id,
4539 "hostname": hostname,
4540
4541=== modified file 'src/provisioningserver/drivers/hardware/msftocs.py'
4542--- src/provisioningserver/drivers/hardware/msftocs.py 2015-12-01 18:12:59 +0000
4543+++ src/provisioningserver/drivers/hardware/msftocs.py 2016-03-02 18:21:51 +0000
4544@@ -188,7 +188,7 @@
4545
4546 @synchronous
4547 def probe_and_enlist_msftocs(
4548- user, ip, port, username, password, accept_all=False):
4549+ user, ip, port, username, password, accept_all=False, domain=None):
4550 """ Extracts all of nodes from msftocs, sets all of them to boot via
4551 HDD by, default, sets them to bootonce via PXE, and then enlists them
4552 into MAAS.
4553@@ -223,7 +223,8 @@
4554 'power_pass': password,
4555 'blade_id': blade_id,
4556 }
4557- system_id = create_node(macs, 'amd64', 'msftocs', params).wait(30)
4558+ system_id = create_node(
4559+ macs, 'amd64', 'msftocs', params, domain).wait(30)
4560
4561 if accept_all:
4562 commission_node(system_id, user).wait(30)
4563
4564=== modified file 'src/provisioningserver/drivers/hardware/seamicro.py'
4565--- src/provisioningserver/drivers/hardware/seamicro.py 2015-12-01 18:12:59 +0000
4566+++ src/provisioningserver/drivers/hardware/seamicro.py 2016-03-02 18:21:51 +0000
4567@@ -275,8 +275,9 @@
4568
4569
4570 @synchronous
4571-def probe_seamicro15k_and_enlist(user, ip, username, password,
4572- power_control=None, accept_all=False):
4573+def probe_seamicro15k_and_enlist(
4574+ user, ip, username, password, power_control=None, accept_all=False,
4575+ domain=None):
4576 power_control = power_control or 'ipmi'
4577
4578 maaslog.info("Probing for seamicro15k servers as %s@%s", username, ip)
4579@@ -292,7 +293,8 @@
4580 'system_id': system_id
4581 }
4582 maaslog.info("Creating seamicro15k node with MACs: %s", macs)
4583- system_id = create_node(macs, 'amd64', 'sm15k', params).wait(30)
4584+ system_id = create_node(
4585+ macs, 'amd64', 'sm15k', params, domain).wait(30)
4586
4587 if accept_all:
4588 commission_node(system_id, user).wait(30)
4589
4590=== modified file 'src/provisioningserver/drivers/hardware/tests/test_msftocs.py'
4591--- src/provisioningserver/drivers/hardware/tests/test_msftocs.py 2015-12-01 18:12:59 +0000
4592+++ src/provisioningserver/drivers/hardware/tests/test_msftocs.py 2016-03-02 18:21:51 +0000
4593@@ -366,6 +366,7 @@
4594 port = randint(2000, 4000)
4595 username = factory.make_name('username')
4596 password = factory.make_name('password')
4597+ domain = factory.make_name('domain')
4598 system_id = factory.make_name('system_id')
4599 blade_id = randint(1, 24)
4600 macs = ['F4:52:14:D6:70:98', 'F4:52:14:D6:70:99']
4601@@ -385,13 +386,13 @@
4602 }
4603 yield deferToThread(
4604 probe_and_enlist_msftocs, user, ip, port, username,
4605- password, accept_all=True)
4606+ password, True, domain)
4607
4608 self.expectThat(blades_mock, MockAnyCall())
4609 self.expectThat(next_boot_device_mock.call_count, Equals(2))
4610 self.expectThat(
4611 create_node_mock,
4612- MockCalledOnceWith(macs, 'amd64', 'msftocs', params))
4613+ MockCalledOnceWith(macs, 'amd64', 'msftocs', params, domain))
4614 self.expectThat(
4615 commission_node_mock,
4616 MockCalledOnceWith(system_id, user))
4617
4618=== modified file 'src/provisioningserver/drivers/hardware/tests/test_seamicro.py'
4619--- src/provisioningserver/drivers/hardware/tests/test_seamicro.py 2015-12-01 18:12:59 +0000
4620+++ src/provisioningserver/drivers/hardware/tests/test_seamicro.py 2016-03-02 18:21:51 +0000
4621@@ -311,6 +311,7 @@
4622 username = factory.make_name('username')
4623 password = factory.make_name('password')
4624 system_id = factory.make_name('system_id')
4625+ domain = factory.make_name('domain')
4626 result = {
4627 0: {
4628 'serverId': '0/0',
4629@@ -343,7 +344,7 @@
4630 yield deferToThread(
4631 probe_seamicro15k_and_enlist,
4632 user, ip, username, password,
4633- power_control='restapi', accept_all=True)
4634+ power_control='restapi', accept_all=True, domain=domain)
4635 self.assertEqual(3, mock_create_node.call_count)
4636
4637 last = result[2]
4638@@ -357,7 +358,7 @@
4639 self.expectThat(
4640 mock_create_node,
4641 MockCalledWith(
4642- last['serverMacAddr'], 'amd64', 'sm15k', power_params))
4643+ last['serverMacAddr'], 'amd64', 'sm15k', power_params, domain))
4644 self.expectThat(
4645 mock_commission_node,
4646 MockCalledWith(system_id, user))
4647
4648=== modified file 'src/provisioningserver/drivers/hardware/tests/test_ucsm.py'
4649--- src/provisioningserver/drivers/hardware/tests/test_ucsm.py 2015-12-01 18:12:59 +0000
4650+++ src/provisioningserver/drivers/hardware/tests/test_ucsm.py 2016-03-02 18:21:51 +0000
4651@@ -618,6 +618,7 @@
4652 username = factory.make_name('username')
4653 password = factory.make_name('password')
4654 system_id = factory.make_name('system_id')
4655+ domain = factory.make_name('domain')
4656 api = Mock()
4657 self.patch(ucsm, 'UCSM_XML_API').return_value = api
4658 server_element = {'uuid': 'uuid'}
4659@@ -631,7 +632,7 @@
4660
4661 yield deferToThread(
4662 probe_and_enlist_ucsm, user, url, username,
4663- password, accept_all=True)
4664+ password, True, domain)
4665 self.expectThat(
4666 set_lan_boot_default_mock,
4667 MockCalledOnceWith(api, server_element))
4668@@ -644,7 +645,7 @@
4669 }
4670 self.expectThat(
4671 create_node_mock,
4672- MockCalledOnceWith(server[1], 'amd64', 'ucsm', params))
4673+ MockCalledOnceWith(server[1], 'amd64', 'ucsm', params, domain))
4674 self.expectThat(
4675 commission_node_mock,
4676 MockCalledOnceWith(system_id, user))
4677
4678=== modified file 'src/provisioningserver/drivers/hardware/tests/test_virsh.py'
4679--- src/provisioningserver/drivers/hardware/tests/test_virsh.py 2015-12-10 05:00:21 +0000
4680+++ src/provisioningserver/drivers/hardware/tests/test_virsh.py 2016-03-02 18:21:51 +0000
4681@@ -1,4 +1,4 @@
4682-# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
4683+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
4684 # GNU Affero General Public License version 3 (see the file LICENSE).
4685
4686 """Tests for `provisioningserver.drivers.hardware.virsh`.
4687@@ -271,6 +271,7 @@
4688 fake_arch = factory.make_name('arch')
4689 mock_arch = self.patch(virsh.VirshSSH, 'get_arch')
4690 mock_arch.return_value = fake_arch
4691+ domain = factory.make_name('domain')
4692
4693 # Patch get_state so that one of the machines is on, so we
4694 # can check that it will be forced off.
4695@@ -330,7 +331,7 @@
4696 # Perform the probe and enlist
4697 yield deferToThread(
4698 virsh.probe_virsh_and_enlist, user, poweraddr,
4699- password=fake_password, accept_all=True)
4700+ fake_password, True, domain)
4701
4702 # Check that login was called with the provided poweraddr and
4703 # password.
4704@@ -351,16 +352,16 @@
4705 mock_create_node, MockCallsMatch(
4706 call(
4707 fake_macs[0], fake_arch, 'virsh', called_params[0],
4708- machines[0]),
4709+ domain, machines[0]),
4710 call(
4711 fake_macs[1], fake_arch, 'virsh', called_params[1],
4712- machines[1]),
4713+ domain, machines[1]),
4714 call(
4715 fake_macs[2], fake_arch, 'virsh', called_params[2],
4716- machines[2]),
4717+ domain, machines[2]),
4718 call(
4719 fake_macs[3], fake_arch, 'virsh', called_params[3],
4720- machines[3]),
4721+ domain, machines[3]),
4722 ))
4723 self.assertThat(mock_logout, MockCalledOnceWith())
4724 self.expectThat(
4725
4726=== modified file 'src/provisioningserver/drivers/hardware/ucsm.py'
4727--- src/provisioningserver/drivers/hardware/ucsm.py 2015-12-01 18:12:59 +0000
4728+++ src/provisioningserver/drivers/hardware/ucsm.py 2016-03-02 18:21:51 +0000
4729@@ -432,7 +432,8 @@
4730
4731
4732 @synchronous
4733-def probe_and_enlist_ucsm(user, url, username, password, accept_all=False):
4734+def probe_and_enlist_ucsm(
4735+ user, url, username, password, accept_all=False, domain=None):
4736 """Probe a UCS Manager and enlist all its servers.
4737
4738 Here's what happens here: 1. Get a list of servers from the UCS
4739@@ -474,7 +475,8 @@
4740 'power_pass': password,
4741 'uuid': server.get('uuid'),
4742 }
4743- system_id = create_node(macs, 'amd64', 'ucsm', params).wait(30)
4744+ system_id = create_node(
4745+ macs, 'amd64', 'ucsm', params, domain).wait(30)
4746
4747 if accept_all:
4748 commission_node(system_id, user).wait(30)
4749
4750=== modified file 'src/provisioningserver/drivers/hardware/virsh.py'
4751--- src/provisioningserver/drivers/hardware/virsh.py 2015-12-11 18:18:23 +0000
4752+++ src/provisioningserver/drivers/hardware/virsh.py 2016-03-02 18:21:51 +0000
4753@@ -257,8 +257,9 @@
4754
4755
4756 @synchronous
4757-def probe_virsh_and_enlist(user, poweraddr, password=None,
4758- prefix_filter=None, accept_all=False):
4759+def probe_virsh_and_enlist(
4760+ user, poweraddr, password=None, prefix_filter=None, accept_all=False,
4761+ domain=None):
4762 """Extracts all of the VMs from virsh and enlists them
4763 into MAAS.
4764
4765@@ -267,6 +268,7 @@
4766 :param password: password connection string.
4767 :param prefix_filter: only enlist nodes that have the prefix.
4768 :param accept_all: if True, commission enlisted nodes.
4769+ :param domain: The domain for the node to join.
4770 """
4771 conn = VirshSSH(dom_prefix=prefix_filter)
4772 if not conn.login(poweraddr, password):
4773@@ -289,7 +291,7 @@
4774 if password is not None:
4775 params['power_pass'] = password
4776 system_id = create_node(
4777- macs, arch, 'virsh', params, hostname=machine).wait(30)
4778+ macs, arch, 'virsh', params, domain, machine).wait(30)
4779
4780 if system_id is not None:
4781 conn.configure_pxe_boot(machine)
4782
4783=== modified file 'src/provisioningserver/drivers/hardware/vmware.py'
4784--- src/provisioningserver/drivers/hardware/vmware.py 2016-01-19 22:36:17 +0000
4785+++ src/provisioningserver/drivers/hardware/vmware.py 2016-03-02 18:21:51 +0000
4786@@ -1,4 +1,4 @@
4787-# Copyright 2015 Canonical Ltd. This software is licensed under the
4788+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
4789 # GNU Affero General Public License version 3 (see the file LICENSE).
4790
4791 __all__ = [
4792@@ -353,7 +353,7 @@
4793 @synchronous
4794 def probe_vmware_and_enlist(
4795 user, host, username, password, port=None,
4796- protocol=None, prefix_filter=None, accept_all=False):
4797+ protocol=None, prefix_filter=None, accept_all=False, domain=None):
4798
4799 # Both '' and None mean the same thing, so normalize it.
4800 if prefix_filter is None:
4801@@ -367,14 +367,14 @@
4802 servers = api.get_all_vm_properties()
4803 _probe_and_enlist_vmware_servers(
4804 api, accept_all, host, password, port, prefix_filter, protocol,
4805- servers, user, username)
4806+ servers, user, username, domain)
4807 finally:
4808 api.disconnect()
4809
4810
4811 def _probe_and_enlist_vmware_servers(
4812 api, accept_all, host, password, port, prefix_filter, protocol,
4813- servers, user, username):
4814+ servers, user, username, domain):
4815 maaslog.info("Found %d VMware servers", len(servers))
4816 for system_name in servers:
4817 if not system_name.startswith(prefix_filter):
4818@@ -403,7 +403,7 @@
4819
4820 system_id = create_node(
4821 properties['macs'], properties['architecture'],
4822- 'vmware', params, hostname=system_name).wait(30)
4823+ 'vmware', params, domain, system_name).wait(30)
4824
4825 if system_id is not None:
4826 api.set_pxe_boot(properties)
4827
4828=== modified file 'src/provisioningserver/drivers/power/mscm.py'
4829--- src/provisioningserver/drivers/power/mscm.py 2016-02-25 16:13:34 +0000
4830+++ src/provisioningserver/drivers/power/mscm.py 2016-03-02 18:21:51 +0000
4831@@ -141,7 +141,8 @@
4832
4833
4834 @synchronous
4835-def probe_and_enlist_mscm(user, host, username, password, accept_all=False):
4836+def probe_and_enlist_mscm(
4837+ user, host, username, password, accept_all=False, domain=None):
4838 """ Extracts all of nodes from the MSCM, sets all of them to boot via M.2
4839 by, default, sets them to bootonce via PXE, and then enlists them into
4840 MAAS. If accept_all is True, it will also commission them.
4841@@ -201,7 +202,7 @@
4842 "show node macaddr %s" % node_id, **params)
4843 macs = re.findall(r':'.join(['[0-9a-f]{2}'] * 6), node_macaddr)
4844 # Create node
4845- system_id = create_node(macs, arch, 'mscm', params).wait(30)
4846+ system_id = create_node(macs, arch, 'mscm', params, domain).wait(30)
4847
4848 if accept_all:
4849 commission_node(system_id, user).wait(30)
4850
4851=== modified file 'src/provisioningserver/drivers/power/tests/test_mscm.py'
4852--- src/provisioningserver/drivers/power/tests/test_mscm.py 2016-02-25 16:13:34 +0000
4853+++ src/provisioningserver/drivers/power/tests/test_mscm.py 2016-03-02 18:21:51 +0000
4854@@ -248,6 +248,7 @@
4855 host = factory.make_hostname('mscm')
4856 username = factory.make_name('user')
4857 password = factory.make_name('password')
4858+ domain = factory.make_name('domain')
4859 system_id = factory.make_name('system_id')
4860 Driver = self.patch(mscm_module, "MSCMPowerDriver")
4861 mscm_driver = Driver.return_value
4862@@ -265,11 +266,11 @@
4863
4864 yield deferToThread(
4865 probe_and_enlist_mscm,
4866- user, host, username, password, accept_all=True)
4867+ user, host, username, password, True, domain)
4868
4869 self.expectThat(
4870 create_node,
4871- MockCalledOnceWith(macs, self.arch, 'mscm', params))
4872+ MockCalledOnceWith(macs, self.arch, 'mscm', params, domain))
4873 self.expectThat(
4874 commission_node,
4875 MockCalledOnceWith(system_id, user))
4876
4877=== modified file 'src/provisioningserver/rpc/cluster.py'
4878--- src/provisioningserver/rpc/cluster.py 2016-02-29 07:45:25 +0000
4879+++ src/provisioningserver/rpc/cluster.py 2016-03-02 18:21:51 +0000
4880@@ -419,6 +419,7 @@
4881 (b"username", amp.Unicode(optional=True)),
4882 (b"password", amp.Unicode(optional=True)),
4883 (b"accept_all", amp.Boolean(optional=True)),
4884+ (b"domain", amp.Unicode(optional=True)),
4885 (b"prefix_filter", amp.Unicode(optional=True)),
4886 (b"power_control", amp.Unicode(optional=True)),
4887 (b"port", amp.Integer(optional=True)),
4888
4889=== modified file 'src/provisioningserver/rpc/clusterservice.py'
4890--- src/provisioningserver/rpc/clusterservice.py 2016-03-02 14:21:29 +0000
4891+++ src/provisioningserver/rpc/clusterservice.py 2016-03-02 18:21:51 +0000
4892@@ -1,4 +1,4 @@
4893-# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
4894+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
4895 # GNU Affero General Public License version 3 (see the file LICENSE).
4896
4897 """RPC implementation for clusters."""
4898@@ -380,8 +380,8 @@
4899 @cluster.AddChassis.responder
4900 def add_chassis(
4901 self, user, chassis_type, hostname, username=None, password=None,
4902- accept_all=False, prefix_filter=None, power_control=None,
4903- port=None, protocol=None):
4904+ accept_all=False, domain=None, prefix_filter=None,
4905+ power_control=None, port=None, protocol=None):
4906 """AddChassis()
4907
4908 Implementation of
4909@@ -391,34 +391,36 @@
4910 if chassis_type in ('virsh', 'powerkvm'):
4911 d = deferToThread(
4912 probe_virsh_and_enlist,
4913- user, hostname, password, prefix_filter, accept_all)
4914+ user, hostname, password, prefix_filter, accept_all,
4915+ domain)
4916 d.addErrback(partial(catch_probe_and_enlist_error, "virsh"))
4917 elif chassis_type == 'vmware':
4918 d = deferToThread(
4919 probe_vmware_and_enlist,
4920 user, hostname, username, password, port, protocol,
4921- prefix_filter, accept_all)
4922+ prefix_filter, accept_all, domain)
4923 d.addErrback(partial(catch_probe_and_enlist_error, "VMware"))
4924 elif chassis_type == 'seamicro15k':
4925 d = deferToThread(
4926 probe_seamicro15k_and_enlist,
4927- user, hostname, username, password, power_control, accept_all)
4928+ user, hostname, username, password, power_control, accept_all,
4929+ domain)
4930 d.addErrback(
4931 partial(catch_probe_and_enlist_error, "SeaMicro 15000"))
4932 elif chassis_type == 'mscm':
4933 d = deferToThread(
4934 probe_and_enlist_mscm, user, hostname, username, password,
4935- accept_all)
4936+ accept_all, domain)
4937 d.addErrback(partial(catch_probe_and_enlist_error, "Moonshot"))
4938 elif chassis_type == 'msftocs':
4939 d = deferToThread(
4940 probe_and_enlist_msftocs, user, hostname, port, username,
4941- password, accept_all)
4942+ password, accept_all, domain)
4943 d.addErrback(partial(catch_probe_and_enlist_error, "MicrosoftOCS"))
4944 elif chassis_type == 'ucsm':
4945 d = deferToThread(
4946 probe_and_enlist_ucsm, user, hostname, username, password,
4947- accept_all)
4948+ accept_all, domain)
4949 d.addErrback(partial(catch_probe_and_enlist_error, "UCS"))
4950 else:
4951 message = "Unknown chassis type %s" % chassis_type
4952
4953=== modified file 'src/provisioningserver/rpc/region.py'
4954--- src/provisioningserver/rpc/region.py 2016-02-26 16:19:41 +0000
4955+++ src/provisioningserver/rpc/region.py 2016-03-02 18:21:51 +0000
4956@@ -344,6 +344,7 @@
4957 (b'power_parameters', amp.Unicode()),
4958 (b'mac_addresses', amp.ListOf(amp.Unicode())),
4959 (b'hostname', amp.Unicode(optional=True)),
4960+ (b'domain', amp.Unicode(optional=True)),
4961 ]
4962 response = [
4963 (b'system_id', amp.Unicode()),
4964
4965=== modified file 'src/provisioningserver/rpc/tests/test_clusterservice.py'
4966--- src/provisioningserver/rpc/tests/test_clusterservice.py 2016-03-02 14:21:29 +0000
4967+++ src/provisioningserver/rpc/tests/test_clusterservice.py 2016-03-02 18:21:51 +0000
4968@@ -1881,6 +1881,7 @@
4969 hostname = factory.make_hostname()
4970 password = factory.make_name('password')
4971 accept_all = factory.pick_bool()
4972+ domain = factory.make_name('domain')
4973 prefix_filter = factory.make_name('prefix_filter')
4974 call_responder(Cluster(), cluster.AddChassis, {
4975 'user': user,
4976@@ -1888,12 +1889,14 @@
4977 'hostname': hostname,
4978 'password': password,
4979 'accept_all': accept_all,
4980+ 'domain': domain,
4981 'prefix_filter': prefix_filter,
4982 })
4983 self.assertThat(
4984 mock_deferToThread, MockCalledOnceWith(
4985 clusterservice.probe_virsh_and_enlist,
4986- user, hostname, password, prefix_filter, accept_all))
4987+ user, hostname, password, prefix_filter, accept_all,
4988+ domain))
4989
4990 def test_chassis_type_powerkvm_calls_probe_virsh_and_enlist(self):
4991 mock_deferToThread = self.patch_autospec(
4992@@ -1902,6 +1905,7 @@
4993 hostname = factory.make_hostname()
4994 password = factory.make_name('password')
4995 accept_all = factory.pick_bool()
4996+ domain = factory.make_name('domain')
4997 prefix_filter = factory.make_name('prefix_filter')
4998 call_responder(Cluster(), cluster.AddChassis, {
4999 'user': user,
5000@@ -1909,12 +1913,14 @@
The diff has been truncated for viewing.