Merge lp:~mpontillo/maas/node-statistics-bug-1584926--part2 into lp:~maas-committers/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-committers/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.
Revision history for this message
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

Add tests for devices by-parent.

5145. By Mike Pontillo

Add tests for machines by-zone.

5144. By Mike Pontillo

Add tests for machines by-status.

5143. By Mike Pontillo

Add tests for machines by-user.

5142. By Mike Pontillo

Reinstate API changes for devices and machines.

5141. By Mike Pontillo

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

5140. By Mike Pontillo

Fix lint, review comments.

5139. By Mike Pontillo

More tests.

5138. By Mike Pontillo

Start adding test cases.

5137. By Mike Pontillo

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.")