Merge lp:~ltrager/maas/lp1573644 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: 4953
Proposed branch: lp:~ltrager/maas/lp1573644
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 304 lines (+165/-20)
7 files modified
src/maasserver/api/tags.py (+45/-7)
src/maasserver/api/tests/test_tag.py (+98/-4)
src/maasserver/models/node.py (+4/-0)
src/maasserver/models/tests/test_node.py (+12/-0)
src/maasserver/websockets/handlers/controller.py (+4/-5)
src/maasserver/websockets/handlers/device.py (+1/-2)
src/maasserver/websockets/handlers/machine.py (+1/-2)
To merge this branch: bzr merge lp:~ltrager/maas/lp1573644
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+292688@code.launchpad.net

Commit message

Allow showing tags by node, machine, device, rack-controller, or region-controller

Description of the change

This adds machines, devices, rack-controllers, and region-controllers operations on the tag API endpoint. Each will only show the specifies node type which has the specified tags.

While writing this I noticed that Node.objects.get_nodes() was not checking that the specified from_nodes query set only contained nodes for that managers node type. For example

Machine.objects.get_nodes(admin, NODE_PERMISSION.VIEW, from_nodes=[Machine, Device, RackController]) would return [Machine, Device, RackController] instead of [Machine].

I patched get_nodes to filter for the managers specified node_type.

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

Nicely done. Looks great.

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/tags.py'
2--- src/maasserver/api/tags.py 2016-04-11 16:23:26 +0000
3+++ src/maasserver/api/tags.py 2016-04-22 21:25:30 +0000
4@@ -30,8 +30,11 @@
5 )
6 from maasserver.forms import TagForm
7 from maasserver.models import (
8+ Device,
9+ Machine,
10 Node,
11 RackController,
12+ RegionController,
13 Tag,
14 )
15 from maasserver.models.node import typecast_to_node_type
16@@ -121,12 +124,7 @@
17 tag.delete()
18 return rc.DELETED
19
20- @operation(idempotent=True)
21- def nodes(self, request, name):
22- """Get the list of nodes that have this tag.
23-
24- Returns 404 if the tag is not found.
25- """
26+ def _get_node_type(self, model, request, name):
27 # Workaround an issue where piston3 will try to use the fields from
28 # this handler instead of the fields defined for the returned object.
29 # This is done because this operation actually returns a list of nodes
30@@ -135,11 +133,51 @@
31 tag = Tag.objects.get_tag_or_404(name=name, user=request.user)
32 return [
33 typecast_to_node_type(node)
34- for node in Node.objects.get_nodes(
35+ for node in model.objects.get_nodes(
36 request.user, NODE_PERMISSION.VIEW,
37 from_nodes=tag.node_set.all())
38 ]
39
40+ @operation(idempotent=True)
41+ def nodes(self, request, name):
42+ """Get the list of nodes that have this tag.
43+
44+ Returns 404 if the tag is not found.
45+ """
46+ return self._get_node_type(Node, request, name)
47+
48+ @operation(idempotent=True)
49+ def machines(self, request, name):
50+ """Get the list of machines that have this tag.
51+
52+ Returns 404 if the tag is not found.
53+ """
54+ return self._get_node_type(Machine, request, name)
55+
56+ @operation(idempotent=True)
57+ def devices(self, request, name):
58+ """Get the list of devices that have this tag.
59+
60+ Returns 404 if the tag is not found.
61+ """
62+ return self._get_node_type(Device, request, name)
63+
64+ @operation(idempotent=True)
65+ def rack_controllers(self, request, name):
66+ """Get the list of rack controllers that have this tag.
67+
68+ Returns 404 if the tag is not found.
69+ """
70+ return self._get_node_type(RackController, request, name)
71+
72+ @operation(idempotent=True)
73+ def region_controllers(self, request, name):
74+ """Get the list of region controllers that have this tag.
75+
76+ Returns 404 if the tag is not found.
77+ """
78+ return self._get_node_type(RegionController, request, name)
79+
80 def _get_nodes_for(self, request, param):
81 system_ids = get_list_from_dict_or_multidict(request.data, param)
82 if system_ids:
83
84=== modified file 'src/maasserver/api/tests/test_tag.py'
85--- src/maasserver/api/tests/test_tag.py 2016-02-26 18:39:26 +0000
86+++ src/maasserver/api/tests/test_tag.py 2016-04-22 21:25:30 +0000
87@@ -130,17 +130,111 @@
88
89 def test_GET_nodes_returns_nodes(self):
90 tag = factory.make_Tag()
91- node1 = factory.make_Node()
92+ machine = factory.make_Node()
93+ device = factory.make_Device()
94+ rack = factory.make_RackController()
95+ region = factory.make_RegionController()
96 # Create a second node that isn't tagged.
97 factory.make_Node()
98- node1.tags.add(tag)
99+ machine.tags.add(tag)
100+ device.tags.add(tag)
101+ rack.tags.add(tag)
102+ region.tags.add(tag)
103 response = self.client.get(self.get_tag_uri(tag), {'op': 'nodes'})
104
105 self.assertEqual(http.client.OK, response.status_code)
106 parsed_result = json.loads(
107 response.content.decode(settings.DEFAULT_CHARSET))
108- self.assertEqual([node1.system_id],
109- [r['system_id'] for r in parsed_result])
110+ self.assertItemsEqual(
111+ [machine.system_id, device.system_id, rack.system_id,
112+ region.system_id],
113+ [r['system_id'] for r in parsed_result])
114+
115+ def test_GET_machines_returns_machines(self):
116+ tag = factory.make_Tag()
117+ machine = factory.make_Node()
118+ device = factory.make_Device()
119+ rack = factory.make_RackController()
120+ region = factory.make_RegionController()
121+ # Create a second node that isn't tagged.
122+ factory.make_Node()
123+ machine.tags.add(tag)
124+ device.tags.add(tag)
125+ rack.tags.add(tag)
126+ region.tags.add(tag)
127+ response = self.client.get(self.get_tag_uri(tag), {'op': 'machines'})
128+
129+ self.assertEqual(http.client.OK, response.status_code)
130+ parsed_result = json.loads(
131+ response.content.decode(settings.DEFAULT_CHARSET))
132+ self.assertItemsEqual(
133+ [machine.system_id],
134+ [r['system_id'] for r in parsed_result])
135+
136+ def test_GET_devices_returns_devices(self):
137+ tag = factory.make_Tag()
138+ machine = factory.make_Node()
139+ device = factory.make_Device()
140+ rack = factory.make_RackController()
141+ region = factory.make_RegionController()
142+ # Create a second node that isn't tagged.
143+ factory.make_Node()
144+ machine.tags.add(tag)
145+ device.tags.add(tag)
146+ rack.tags.add(tag)
147+ region.tags.add(tag)
148+ response = self.client.get(self.get_tag_uri(tag), {'op': 'devices'})
149+
150+ self.assertEqual(http.client.OK, response.status_code)
151+ parsed_result = json.loads(
152+ response.content.decode(settings.DEFAULT_CHARSET))
153+ self.assertItemsEqual(
154+ [device.system_id],
155+ [r['system_id'] for r in parsed_result])
156+
157+ def test_GET_rack_controllers_returns_rack_controllers(self):
158+ tag = factory.make_Tag()
159+ machine = factory.make_Node()
160+ device = factory.make_Device()
161+ rack = factory.make_RackController()
162+ region = factory.make_RegionController()
163+ # Create a second node that isn't tagged.
164+ factory.make_Node()
165+ machine.tags.add(tag)
166+ device.tags.add(tag)
167+ rack.tags.add(tag)
168+ region.tags.add(tag)
169+ response = self.client.get(
170+ self.get_tag_uri(tag), {'op': 'rack_controllers'})
171+
172+ self.assertEqual(http.client.OK, response.status_code)
173+ parsed_result = json.loads(
174+ response.content.decode(settings.DEFAULT_CHARSET))
175+ self.assertItemsEqual(
176+ [rack.system_id],
177+ [r['system_id'] for r in parsed_result])
178+
179+ def test_GET_region_controllers_returns_region_controllers(self):
180+ tag = factory.make_Tag()
181+ machine = factory.make_Node()
182+ device = factory.make_Device()
183+ rack = factory.make_RackController()
184+ region = factory.make_RegionController()
185+ # Create a second node that isn't tagged.
186+ factory.make_Node()
187+ machine.tags.add(tag)
188+ device.tags.add(tag)
189+ rack.tags.add(tag)
190+ region.tags.add(tag)
191+ response = self.client.get(
192+ self.get_tag_uri(tag), {'op': 'region_controllers'})
193+
194+ self.assertEqual(http.client.OK, response.status_code)
195+ parsed_result = json.loads(
196+ response.content.decode(settings.DEFAULT_CHARSET))
197+ self.assertItemsEqual(
198+ [region.system_id],
199+ [r['system_id'] for r in parsed_result])
200
201 def test_GET_nodes_hides_invisible_nodes(self):
202 user2 = factory.make_User()
203
204=== modified file 'src/maasserver/models/node.py'
205--- src/maasserver/models/node.py 2016-04-22 08:42:53 +0000
206+++ src/maasserver/models/node.py 2016-04-22 21:25:30 +0000
207@@ -442,6 +442,10 @@
208 """
209 if from_nodes is None:
210 from_nodes = self.all()
211+ else:
212+ # Make sure even if given a query set of multiple node types
213+ # get_nodes only returns nodes applicable to this manager.
214+ from_nodes = from_nodes.filter(**self.extra_filters)
215 nodes = self._filter_visible_nodes(from_nodes, user, perm)
216 return self.filter_by_ids(nodes, ids)
217
218
219=== modified file 'src/maasserver/models/tests/test_node.py'
220--- src/maasserver/models/tests/test_node.py 2016-04-22 08:42:53 +0000
221+++ src/maasserver/models/tests/test_node.py 2016-04-22 21:25:30 +0000
222@@ -3263,6 +3263,18 @@
223 user=None, perm=NODE_PERMISSION.EDIT, ids=[node.system_id])
224 self.assertItemsEqual([], observed)
225
226+ def test_get_nodes_only_returns_managed_nodes(self):
227+ user = factory.make_User()
228+ machine = self.make_node(user)
229+ for _ in range(3):
230+ self.make_node(user, node_type=NODE_TYPE.DEVICE)
231+ self.assertItemsEqual(
232+ [machine],
233+ Machine.objects.get_nodes(
234+ user=user, perm=NODE_PERMISSION.VIEW,
235+ from_nodes=Node.objects.all())
236+ )
237+
238 def test_filter_nodes_by_spaces(self):
239 # Create a throwaway node and a throwaway space.
240 # (to ensure they are filtered out.)
241
242=== modified file 'src/maasserver/websockets/handlers/controller.py'
243--- src/maasserver/websockets/handlers/controller.py 2016-04-21 01:26:45 +0000
244+++ src/maasserver/websockets/handlers/controller.py 2016-04-22 21:25:30 +0000
245@@ -10,7 +10,7 @@
246 from maasserver.enum import NODE_PERMISSION
247 from maasserver.forms import AdminMachineWithMACAddressesForm
248 from maasserver.models.node import (
249- Node,
250+ Controller,
251 RackController,
252 typecast_to_node_type,
253 )
254@@ -23,7 +23,7 @@
255 class Meta(MachineHandler.Meta):
256 abstract = False
257 queryset = node_prefetch(
258- Node.controllers.all().prefetch_related("interface_set"))
259+ Controller.controllers.all().prefetch_related("interface_set"))
260 allowed_methods = [
261 'list',
262 'get',
263@@ -76,9 +76,8 @@
264
265 def get_queryset(self):
266 """Return `QuerySet` for controllers only viewable by `user`."""
267- controllers = super(ControllerHandler, self).get_queryset()
268- return Node.controllers.get_nodes(
269- self.user, NODE_PERMISSION.VIEW, from_nodes=controllers)
270+ return Controller.controllers.get_nodes(
271+ self.user, NODE_PERMISSION.VIEW, from_nodes=self._meta.queryset)
272
273 def dehydrate(self, obj, data, for_list=False):
274 data = super().dehydrate(obj, data, for_list=for_list)
275
276=== modified file 'src/maasserver/websockets/handlers/device.py'
277--- src/maasserver/websockets/handlers/device.py 2016-03-28 13:54:47 +0000
278+++ src/maasserver/websockets/handlers/device.py 2016-04-22 21:25:30 +0000
279@@ -117,9 +117,8 @@
280
281 def get_queryset(self):
282 """Return `QuerySet` for devices only viewable by `user`."""
283- devices = super(DeviceHandler, self).get_queryset()
284 return Device.objects.get_nodes(
285- self.user, NODE_PERMISSION.VIEW, from_nodes=devices)
286+ self.user, NODE_PERMISSION.VIEW, from_nodes=self._meta.queryset)
287
288 def dehydrate(self, obj, data, for_list=False):
289 """Add extra fields to `data`."""
290
291=== modified file 'src/maasserver/websockets/handlers/machine.py'
292--- src/maasserver/websockets/handlers/machine.py 2016-03-31 23:34:55 +0000
293+++ src/maasserver/websockets/handlers/machine.py 2016-04-22 21:25:30 +0000
294@@ -156,9 +156,8 @@
295
296 def get_queryset(self):
297 """Return `QuerySet` for devices only viewable by `user`."""
298- nodes = super(MachineHandler, self).get_queryset()
299 return Machine.objects.get_nodes(
300- self.user, NODE_PERMISSION.VIEW, from_nodes=nodes)
301+ self.user, NODE_PERMISSION.VIEW, from_nodes=self._meta.queryset)
302
303 def dehydrate(self, obj, data, for_list=False):
304 """Add extra fields to `data`."""