Merge lp:~mpontillo/maas/node-statistics-bug-1584926--part2 into lp:maas/trunk

Proposed by Mike Pontillo
Status: Rejected
Rejected by: MAAS Lander
Proposed branch: lp:~mpontillo/maas/node-statistics-bug-1584926--part2
Merge into: lp:maas/trunk
Prerequisite: lp:~mpontillo/maas/node-statistics-bug-1584926--part1
Diff against target: 560 lines (+466/-3)
4 files modified
src/maasserver/api/devices.py (+34/-1)
src/maasserver/api/machines.py (+86/-0)
src/maasserver/api/tests/test_devices.py (+69/-2)
src/maasserver/api/tests/test_machines.py (+277/-0)
To merge this branch: bzr merge lp:~mpontillo/maas/node-statistics-bug-1584926--part2
Reviewer Review Type Date Requested Status
MAAS Maintainers Pending
Review via email: mp+297684@code.launchpad.net

Commit message

* Add new APIs to fetch machines and devices using a "group by":
   - machines by-status [numeric=<bool>] [count=<bool>] [summary=<bool>]
   - machines by-user [count=<bool>] [summary=<bool>]
   - machines by-zone [count=<bool>] [summary=<bool>]
   - devices by-parent [by_system_id=<bool>] [count=<bool>] [summary=<bool>]
 * Add new API to map node status IDs to labels:
   - machines get-status-descriptions

To post a comment you must log in.
MAAS Lander (maas-lander) wrote :

Transitioned to Git.

lp:maas has now moved from Bzr to Git.
Please propose your branches with Launchpad using Git.

git clone https://git.launchpad.net/maas

Unmerged revisions

5146. By Mike Pontillo on 2016-06-16

Add tests for devices by-parent.

5145. By Mike Pontillo on 2016-06-16

Add tests for machines by-zone.

5144. By Mike Pontillo on 2016-06-16

Add tests for machines by-status.

5143. By Mike Pontillo on 2016-06-16

Add tests for machines by-user.

5142. By Mike Pontillo on 2016-06-16

Reinstate API changes for devices and machines.

5141. By Mike Pontillo on 2016-06-16

Revert changes to devices.py and machines.py. (they'll be moved to another branch)

5140. By Mike Pontillo on 2016-06-16

Fix lint, review comments.

5139. By Mike Pontillo on 2016-06-16

More tests.

5138. By Mike Pontillo on 2016-06-16

Start adding test cases.

5137. By Mike Pontillo on 2016-06-15

Format imports.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/devices.py'
2--- src/maasserver/api/devices.py 2016-05-12 19:07:37 +0000
3+++ src/maasserver/api/devices.py 2016-06-16 21:29:13 +0000
4@@ -6,6 +6,7 @@
5 "DevicesHandler",
6 ]
7
8+from formencode.validators import StringBool
9 from maasserver.api.logger import maaslog
10 from maasserver.api.nodes import (
11 NodeHandler,
12@@ -13,13 +14,17 @@
13 OwnerDataMixin,
14 )
15 from maasserver.api.support import operation
16+from maasserver.api.utils import get_optional_param
17 from maasserver.enum import NODE_PERMISSION
18 from maasserver.exceptions import MAASAPIValidationError
19 from maasserver.forms import (
20 DeviceForm,
21 DeviceWithMACsForm,
22 )
23-from maasserver.models.node import Device
24+from maasserver.models.node import (
25+ Device,
26+ Node,
27+)
28 from maasserver.utils.orm import reload_object
29 from piston3.utils import rc
30
31@@ -190,6 +195,34 @@
32 else:
33 raise MAASAPIValidationError(form.errors)
34
35+ @operation(idempotent=True)
36+ def by_parent(self, request):
37+ """Fetches the list of devices by parent.
38+
39+ Optional arguments:
40+ summary: If True, prints a summary of each node instead of the complete
41+ object.
42+ count: If True, prints a count of the number of nodes allocated to each
43+ user instead of any node details.
44+ by_system_id: If True, prints the parent node system_id values instead
45+ of hostnames.
46+
47+ Also accepts any filter parameter the 'read' operation accepts.
48+ """
49+ summary = get_optional_param(
50+ request.GET, 'summary', default=False, validator=StringBool)
51+ count = get_optional_param(
52+ request.GET, 'count', default=False, validator=StringBool)
53+ by_system_id = get_optional_param(
54+ request.GET, 'by_system_id', default=False, validator=StringBool)
55+ return {
56+ node.system_id if by_system_id else node.hostname:
57+ self.prepare_results(
58+ self._query(request).filter(parent=node),
59+ count=count, summary=summary)
60+ for node in Node.objects.filter(children__isnull=False)
61+ }
62+
63 @classmethod
64 def resource_uri(cls, *args, **kwargs):
65 return ('devices_handler', [])
66
67=== modified file 'src/maasserver/api/machines.py'
68--- src/maasserver/api/machines.py 2016-06-10 16:43:12 +0000
69+++ src/maasserver/api/machines.py 2016-06-16 21:29:13 +0000
70@@ -45,6 +45,7 @@
71 from maasserver.enum import (
72 NODE_PERMISSION,
73 NODE_STATUS,
74+ NODE_STATUS_CHOICES,
75 )
76 from maasserver.exceptions import (
77 MAASAPIBadRequest,
78@@ -68,6 +69,8 @@
79 Filesystem,
80 Machine,
81 RackController,
82+ User,
83+ Zone,
84 )
85 from maasserver.models.node import RELEASABLE_STATUSES
86 from maasserver.node_constraint_filter_forms import AcquireNodeForm
87@@ -1358,6 +1361,89 @@
88 rack.hostname, hostname),
89 content_type=("text/plain; charset=%s" % settings.DEFAULT_CHARSET))
90
91+ @operation(idempotent=True)
92+ def get_status_descriptions(self, request):
93+ """Returns the mapping of stauts IDs to status labels."""
94+ return {choice[0]: choice[1] for choice in NODE_STATUS_CHOICES}
95+
96+ @operation(idempotent=True)
97+ def by_user(self, request):
98+ """Fetches the list of nodes allocated to each user.
99+
100+ Optional arguments:
101+ summary: If True, prints a summary of each node instead of the complete
102+ object.
103+ count: If True, prints a count of the number of nodes allocated to each
104+ user instead of any node details.
105+
106+ Also accepts any filter parameter the 'read' operation accepts.
107+ """
108+ summary = get_optional_param(
109+ request.GET, 'summary', default=False, validator=StringBool)
110+ count = get_optional_param(
111+ request.GET, 'count', default=False, validator=StringBool)
112+ return {
113+ user.username:
114+ self.prepare_results(
115+ self._query(request).filter(owner=user),
116+ count=count, summary=summary)
117+ for user in User.objects.filter(is_active=True)
118+ }
119+
120+ @operation(idempotent=True)
121+ def by_status(self, request):
122+ """Fetches the list of nodes grouped by node status.
123+
124+ Optional arguments:
125+ summary: If True, prints a summary of each node instead of the complete
126+ object.
127+ count: If True, prints a count of the number of nodes with each status
128+ instead of any node details.
129+ numeric: If True, prints numeric status values as the key instead of
130+ friendly descriptions.
131+
132+ Also accepts any filter parameter the 'read' operation accepts.
133+ """
134+ summary = get_optional_param(
135+ request.GET, 'summary', default=False, validator=StringBool)
136+ count = get_optional_param(
137+ request.GET, 'count', default=False, validator=StringBool)
138+ numeric = get_optional_param(
139+ request.GET, 'numeric', default=False, validator=StringBool)
140+ return {
141+ # NODE_STATUS_CHOICES[0] will be the numeric value; [1] is the
142+ # description.
143+ status_choice[0 if numeric else 1]:
144+ self.prepare_results(
145+ self._query(request).filter(status=status_choice[0]),
146+ count=count, summary=summary)
147+ for status_choice in NODE_STATUS_CHOICES
148+ }
149+
150+ @operation(idempotent=True)
151+ def by_zone(self, request):
152+ """Fetches the list of nodes grouped by zone.
153+
154+ Optional arguments:
155+ summary: If True, prints a summary of each node instead of the complete
156+ object.
157+ count: If True, prints a count of the number of nodes in each zone
158+ instead of any node details.
159+
160+ Also accepts any filter parameter the 'read' operation accepts.
161+ """
162+ summary = get_optional_param(
163+ request.GET, 'summary', default=False, validator=StringBool)
164+ count = get_optional_param(
165+ request.GET, 'count', default=False, validator=StringBool)
166+ return {
167+ zone.name:
168+ self.prepare_results(
169+ self._query(request).filter(zone=zone),
170+ count=count, summary=summary)
171+ for zone in Zone.objects.all()
172+ }
173+
174 @classmethod
175 def resource_uri(cls, *args, **kwargs):
176 return ('machines_handler', [])
177
178=== modified file 'src/maasserver/api/tests/test_devices.py'
179--- src/maasserver/api/tests/test_devices.py 2016-05-24 22:05:45 +0000
180+++ src/maasserver/api/tests/test_devices.py 2016-06-16 21:29:13 +0000
181@@ -23,6 +23,7 @@
182 from maasserver.utils.converters import json_load_bytes
183 from maasserver.utils.orm import reload_object
184 from maastesting.matchers import MockCalledOnce
185+from testtools.matchers import (Equals, HasLength)
186
187
188 class DeviceOwnerDataTest(APITestCase.ForUser):
189@@ -201,6 +202,73 @@
190 [device.system_id for device in devices],
191 [device.get('system_id') for device in parsed_result])
192
193+ def test_GET_by_parent(self):
194+ factory.make_Node()
195+ parent = factory.make_Node()
196+ child1 = factory.make_Device(parent=parent)
197+ child2 = factory.make_Device(parent=parent)
198+ devices = [child1, child2]
199+ response = self.client.get(reverse('devices_handler'), {
200+ 'op': 'by_parent'
201+ })
202+ parsed_result = json_load_bytes(response.content)
203+ self.assertEqual(http.client.OK, response.status_code)
204+ self.assertItemsEqual(
205+ [device.system_id for device in devices],
206+ [device.get('system_id')
207+ for device in parsed_result[parent.hostname]])
208+
209+ def test_GET_summary_by_parent(self):
210+ factory.make_Node()
211+ parent = factory.make_Node()
212+ child1 = factory.make_Device(parent=parent)
213+ child2 = factory.make_Device(parent=parent)
214+ devices = [child1, child2]
215+ response = self.client.get(reverse('devices_handler'), {
216+ 'op': 'by_parent',
217+ 'summary': True
218+ })
219+ parsed_result = json_load_bytes(response.content)
220+ self.assertEqual(http.client.OK, response.status_code)
221+ self.assertItemsEqual(
222+ [device.system_id for device in devices],
223+ [device.get('system_id')
224+ for device in parsed_result[parent.hostname]])
225+ self.assertThat(parsed_result[parent.hostname], HasLength(2))
226+
227+ def test_GET_by_parent_system_id(self):
228+ factory.make_Node()
229+ parent = factory.make_Node()
230+ child1 = factory.make_Device(parent=parent)
231+ child2 = factory.make_Device(parent=parent)
232+ devices = [child1, child2]
233+ response = self.client.get(reverse('devices_handler'), {
234+ 'op': 'by_parent',
235+ 'by_system_id': True
236+ })
237+ parsed_result = json_load_bytes(response.content)
238+ self.assertEqual(http.client.OK, response.status_code)
239+ self.assertItemsEqual(
240+ [device.system_id for device in devices],
241+ [device.get('system_id')
242+ for device in parsed_result[parent.system_id]])
243+
244+ def test_GET_by_parent_count(self):
245+ factory.make_Node()
246+ parent = factory.make_Node()
247+ num_devices = random.choice([3, 5, 7])
248+ devices = [
249+ factory.make_Device(parent=parent) for _ in range(num_devices)
250+ ]
251+ response = self.client.get(reverse('devices_handler'), {
252+ 'op': 'by_parent',
253+ 'by_system_id': True,
254+ 'count': True
255+ })
256+ parsed_result = json_load_bytes(response.content)
257+ self.assertEqual(http.client.OK, response.status_code)
258+ self.assertThat(parsed_result[parent.system_id], Equals(len(devices)))
259+
260 def test_read_ignores_nodes(self):
261 factory.make_Node(
262 status=NODE_STATUS.ALLOCATED, owner=self.user)
263@@ -209,8 +277,7 @@
264
265 self.assertEqual(http.client.OK, response.status_code)
266 self.assertEqual(
267- [],
268- [device.get('system_id') for device in parsed_result])
269+ [], [device.get('system_id') for device in parsed_result])
270
271 def test_read_with_id_returns_matching_devices(self):
272 # The "list" operation takes optional "id" parameters. Only
273
274=== modified file 'src/maasserver/api/tests/test_machines.py'
275--- src/maasserver/api/tests/test_machines.py 2016-06-16 21:29:13 +0000
276+++ src/maasserver/api/tests/test_machines.py 2016-06-16 21:29:13 +0000
277@@ -252,6 +252,283 @@
278 self.assertEqual(http.client.OK, response.status_code)
279 self.expectThat(parsed_result, Equals(num_hosts))
280
281+ def test_GET_by_user_lists_machines(self):
282+ # The api allows for fetching the list of Machines.
283+ machine1 = factory.make_Node()
284+ machine2 = factory.make_Node(
285+ status=NODE_STATUS.ALLOCATED, owner=self.user)
286+ # This device should be ignored.
287+ factory.make_Device()
288+ response = self.client.get(reverse('machines_handler'), {
289+ 'op': 'by_user',
290+ })
291+ parsed_result = json.loads(
292+ response.content.decode(settings.DEFAULT_CHARSET))
293+ self.assertEqual(http.client.OK, response.status_code)
294+ self.assertItemsEqual(
295+ [machine2.system_id], extract_system_ids(
296+ parsed_result[self.user.username]))
297+
298+ def test_GET_by_user_lists_machines_with_summary(self):
299+ # The api allows for fetching the list of Machines.
300+ machine1 = factory.make_Node()
301+ machine2 = factory.make_Node(
302+ status=NODE_STATUS.ALLOCATED, owner=self.user)
303+ # This device should be ignored.
304+ factory.make_Device()
305+ response = self.client.get(reverse('machines_handler'), {
306+ 'op': 'by_user',
307+ 'summary': True
308+ })
309+ parsed_result = json.loads(
310+ response.content.decode(settings.DEFAULT_CHARSET))
311+ self.assertEqual(http.client.OK, response.status_code)
312+ self.assertItemsEqual(
313+ [machine2.system_id],
314+ extract_system_ids(parsed_result[self.user.username]))
315+ # Ensure each result is a summary (only contains two items)
316+ self.expectThat(len(parsed_result[self.user.username][0]), Equals(2))
317+
318+ def test_GET_by_user_lists_machines_with_count(self):
319+ # The api allows for fetching the list of Machines.
320+ num_hosts = random.choice([3, 5, 7, 9])
321+ for _ in range(num_hosts):
322+ factory.make_Node(status=NODE_STATUS.ALLOCATED, owner=self.user)
323+ factory.make_Node(status=NODE_STATUS.READY)
324+ factory.make_Node(status=NODE_STATUS.READY)
325+ # This device should be ignored.
326+ factory.make_Device()
327+ response = self.client.get(reverse('machines_handler'), {
328+ 'op': 'by_user',
329+ 'count': True
330+ })
331+ parsed_result = json.loads(
332+ response.content.decode(settings.DEFAULT_CHARSET))
333+ self.assertEqual(http.client.OK, response.status_code)
334+ self.expectThat(parsed_result[self.user.username], Equals(num_hosts))
335+
336+ def test_GET_by_status_lists_machines(self):
337+ # The api allows for fetching the list of Machines.
338+ ready = factory.make_Node(status=NODE_STATUS.READY)
339+ allocated = factory.make_Node(
340+ status=NODE_STATUS.ALLOCATED, owner=self.user)
341+ broken = factory.make_Node(status=NODE_STATUS.BROKEN)
342+ commissioning = factory.make_Node(status=NODE_STATUS.COMMISSIONING)
343+ deployed = factory.make_Node(
344+ status=NODE_STATUS.DEPLOYED, owner=self.user)
345+ disk_erasing = factory.make_Node(status=NODE_STATUS.DISK_ERASING)
346+ failed_commissioning = factory.make_Node(
347+ status=NODE_STATUS.FAILED_COMMISSIONING)
348+ response = self.client.get(reverse('machines_handler'), {
349+ 'op': 'by_status',
350+ 'numeric': True
351+ })
352+ parsed_result = json.loads(
353+ response.content.decode(settings.DEFAULT_CHARSET))
354+ self.assertEqual(http.client.OK, response.status_code)
355+ self.assertItemsEqual(
356+ [ready.system_id], extract_system_ids(
357+ parsed_result[str(NODE_STATUS.READY)]))
358+ self.assertItemsEqual(
359+ [allocated.system_id], extract_system_ids(
360+ parsed_result[str(NODE_STATUS.ALLOCATED)]))
361+ self.assertItemsEqual(
362+ [broken.system_id], extract_system_ids(
363+ parsed_result[str(NODE_STATUS.BROKEN)]))
364+ self.assertItemsEqual(
365+ [commissioning.system_id], extract_system_ids(
366+ parsed_result[str(NODE_STATUS.COMMISSIONING)]))
367+ self.assertItemsEqual(
368+ [deployed.system_id], extract_system_ids(
369+ parsed_result[str(NODE_STATUS.DEPLOYED)]))
370+ self.assertItemsEqual(
371+ [disk_erasing.system_id], extract_system_ids(
372+ parsed_result[str(NODE_STATUS.DISK_ERASING)]))
373+ self.assertItemsEqual(
374+ [failed_commissioning.system_id], extract_system_ids(
375+ parsed_result[str(NODE_STATUS.FAILED_COMMISSIONING)]))
376+
377+ def test_GET_by_status_lists_machines_with_summary(self):
378+ # The api allows for fetching the list of Machines.
379+ ready = factory.make_Node(status=NODE_STATUS.READY)
380+ allocated = factory.make_Node(
381+ status=NODE_STATUS.ALLOCATED, owner=self.user)
382+ broken = factory.make_Node(status=NODE_STATUS.BROKEN)
383+ commissioning = factory.make_Node(status=NODE_STATUS.COMMISSIONING)
384+ deployed = factory.make_Node(
385+ status=NODE_STATUS.DEPLOYED, owner=self.user)
386+ disk_erasing = factory.make_Node(status=NODE_STATUS.DISK_ERASING)
387+ failed_commissioning = factory.make_Node(
388+ status=NODE_STATUS.FAILED_COMMISSIONING)
389+ response = self.client.get(reverse('machines_handler'), {
390+ 'op': 'by_status',
391+ 'summary': True,
392+ 'numeric': True,
393+ })
394+ parsed_result = json.loads(
395+ response.content.decode(settings.DEFAULT_CHARSET))
396+ self.assertEqual(http.client.OK, response.status_code)
397+ self.assertItemsEqual(
398+ [ready.system_id], extract_system_ids(
399+ parsed_result[str(NODE_STATUS.READY)]))
400+ self.assertItemsEqual(
401+ [allocated.system_id], extract_system_ids(
402+ parsed_result[str(NODE_STATUS.ALLOCATED)]))
403+ self.assertItemsEqual(
404+ [broken.system_id], extract_system_ids(
405+ parsed_result[str(NODE_STATUS.BROKEN)]))
406+ self.assertItemsEqual(
407+ [commissioning.system_id], extract_system_ids(
408+ parsed_result[str(NODE_STATUS.COMMISSIONING)]))
409+ self.assertItemsEqual(
410+ [deployed.system_id], extract_system_ids(
411+ parsed_result[str(NODE_STATUS.DEPLOYED)]))
412+ self.assertItemsEqual(
413+ [disk_erasing.system_id], extract_system_ids(
414+ parsed_result[str(NODE_STATUS.DISK_ERASING)]))
415+ self.assertItemsEqual(
416+ [failed_commissioning.system_id], extract_system_ids(
417+ parsed_result[str(NODE_STATUS.FAILED_COMMISSIONING)]))
418+ # Ensure each result is a summary (only contains two items)
419+ self.expectThat(
420+ len(parsed_result[str(NODE_STATUS.READY)][0]), Equals(2))
421+ self.expectThat(
422+ len(parsed_result[str(NODE_STATUS.ALLOCATED)][0]), Equals(2))
423+ self.expectThat(
424+ len(parsed_result[str(NODE_STATUS.BROKEN)][0]), Equals(2))
425+ self.expectThat(
426+ len(parsed_result[str(NODE_STATUS.COMMISSIONING)][0]), Equals(2))
427+ self.expectThat(
428+ len(parsed_result[str(NODE_STATUS.DEPLOYED)][0]), Equals(2))
429+ self.expectThat(
430+ len(parsed_result[str(NODE_STATUS.DISK_ERASING)][0]), Equals(2))
431+ self.expectThat(
432+ len(parsed_result[str(NODE_STATUS.FAILED_COMMISSIONING)][0]),
433+ Equals(2))
434+
435+ def test_GET_by_status_lists_machines_with_count(self):
436+ factory.make_Node(status=NODE_STATUS.READY)
437+ factory.make_Node(status=NODE_STATUS.READY)
438+ factory.make_Node(
439+ status=NODE_STATUS.ALLOCATED, owner=self.user)
440+ factory.make_Node(status=NODE_STATUS.BROKEN)
441+ factory.make_Node(status=NODE_STATUS.BROKEN)
442+ factory.make_Node(status=NODE_STATUS.BROKEN)
443+ factory.make_Node(status=NODE_STATUS.COMMISSIONING)
444+ factory.make_Node(
445+ status=NODE_STATUS.DEPLOYED, owner=self.user)
446+ factory.make_Node(status=NODE_STATUS.DISK_ERASING)
447+ factory.make_Node(
448+ status=NODE_STATUS.FAILED_COMMISSIONING)
449+ response = self.client.get(reverse('machines_handler'), {
450+ 'op': 'by_status',
451+ 'count': True,
452+ 'numeric': True,
453+ })
454+ parsed_result = json.loads(
455+ response.content.decode(settings.DEFAULT_CHARSET))
456+ self.assertEqual(http.client.OK, response.status_code)
457+
458+ # Ensure each result is a count
459+ self.expectThat(parsed_result[str(NODE_STATUS.READY)], Equals(2))
460+ self.expectThat(parsed_result[str(NODE_STATUS.ALLOCATED)], Equals(1))
461+ self.expectThat(parsed_result[str(NODE_STATUS.BROKEN)], Equals(3))
462+ self.expectThat(
463+ parsed_result[str(NODE_STATUS.COMMISSIONING)], Equals(1))
464+ self.expectThat(parsed_result[str(NODE_STATUS.DEPLOYED)], Equals(1))
465+ self.expectThat(
466+ parsed_result[str(NODE_STATUS.DISK_ERASING)], Equals(1))
467+ self.expectThat(
468+ parsed_result[str(NODE_STATUS.FAILED_COMMISSIONING)], Equals(1))
469+
470+ def test_GET_by_status_lists_statuses_in_human_readable_format(self):
471+ factory.make_Node(status=NODE_STATUS.READY)
472+ factory.make_Node(status=NODE_STATUS.READY)
473+ factory.make_Node(status=NODE_STATUS.BROKEN)
474+ factory.make_Node(status=NODE_STATUS.BROKEN)
475+ factory.make_Node(status=NODE_STATUS.BROKEN)
476+ response = self.client.get(reverse('machines_handler'), {
477+ 'op': 'by_status',
478+ 'count': True,
479+ 'numeric': False,
480+ })
481+ parsed_result = json.loads(
482+ response.content.decode(settings.DEFAULT_CHARSET))
483+ self.assertEqual(http.client.OK, response.status_code)
484+
485+ # Ensure each result is a count mapped to a human readable status.
486+ self.expectThat(parsed_result["Ready"], Equals(2))
487+ self.expectThat(parsed_result["Broken"], Equals(3))
488+ self.expectThat(parsed_result["Commissioning"], Equals(0))
489+
490+ def test_GET_by_zone_lists_machines(self):
491+ zone1 = factory.make_Zone(name="zone1")
492+ zone2 = factory.make_Zone(name="zone2")
493+ machinez1 = factory.make_Node(zone=zone1)
494+ machinez2 = factory.make_Node(zone=zone2)
495+ # This device should be ignored.
496+ factory.make_Device()
497+ response = self.client.get(reverse('machines_handler'), {
498+ 'op': 'by_zone',
499+ })
500+ parsed_result = json.loads(
501+ response.content.decode(settings.DEFAULT_CHARSET))
502+ self.assertEqual(http.client.OK, response.status_code)
503+ self.assertItemsEqual(
504+ [machinez1.system_id], extract_system_ids(
505+ parsed_result[zone1.name]))
506+ self.assertItemsEqual(
507+ [machinez2.system_id], extract_system_ids(
508+ parsed_result[zone2.name]))
509+
510+ def test_GET_by_zone_lists_machines_with_summary(self):
511+ zone1 = factory.make_Zone(name="zone1")
512+ zone2 = factory.make_Zone(name="zone2")
513+ machinez1 = factory.make_Node(zone=zone1)
514+ machinez2 = factory.make_Node(zone=zone2)
515+ # This device should be ignored.
516+ factory.make_Device()
517+ response = self.client.get(reverse('machines_handler'), {
518+ 'op': 'by_zone',
519+ 'summary': True
520+ })
521+ parsed_result = json.loads(
522+ response.content.decode(settings.DEFAULT_CHARSET))
523+ self.assertEqual(http.client.OK, response.status_code)
524+ self.assertItemsEqual(
525+ [machinez1.system_id], extract_system_ids(
526+ parsed_result[zone1.name]))
527+ self.assertItemsEqual(
528+ [machinez2.system_id], extract_system_ids(
529+ parsed_result[zone2.name]))
530+ # Ensure each result is a summary (only contains two items)
531+ self.expectThat(len(parsed_result[zone1.name][0]), Equals(2))
532+ self.expectThat(len(parsed_result[zone2.name][0]), Equals(2))
533+
534+ def test_GET_by_zone_lists_machines_with_count(self):
535+ zone1 = factory.make_Zone(name="zone1")
536+ zone2 = factory.make_Zone(name="zone2")
537+ zone1_num_machines = random.choice([3, 5, 7])
538+ for _ in range(zone1_num_machines):
539+ factory.make_Node(zone=zone1)
540+ zone2_num_machines = random.choice([3, 5, 7])
541+ for _ in range(zone2_num_machines):
542+ factory.make_Node(zone=zone2)
543+ # This device should be ignored.
544+ factory.make_Device()
545+ response = self.client.get(reverse('machines_handler'), {
546+ 'op': 'by_zone',
547+ 'count': True
548+ })
549+ parsed_result = json.loads(
550+ response.content.decode(settings.DEFAULT_CHARSET))
551+ self.assertEqual(http.client.OK, response.status_code)
552+ # Ensure each result has the correct count
553+ self.expectThat(
554+ parsed_result[zone1.name], Equals(zone1_num_machines))
555+ self.expectThat(
556+ parsed_result[zone2.name], Equals(zone2_num_machines))
557+
558 def test_GET_machines_issues_constant_number_of_queries(self):
559 # XXX: GavinPanella 2014-10-03 bug=1377335
560 self.skip("Unreliable; something is causing varying counts.")