Merge ~ltrager/maas:limit_listing_queries into maas:master

Proposed by Lee Trager
Status: Merged
Approved by: Lee Trager
Approved revision: c38c9fbf8d8b15380cd07a04bceb18ceee87219b
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~ltrager/maas:limit_listing_queries
Merge into: maas:master
Diff against target: 995 lines (+323/-165)
16 files modified
src/maasserver/third_party_drivers.py (+3/-2)
src/maasserver/websockets/base.py (+8/-4)
src/maasserver/websockets/handlers/controller.py (+17/-8)
src/maasserver/websockets/handlers/device.py (+15/-7)
src/maasserver/websockets/handlers/event.py (+1/-1)
src/maasserver/websockets/handlers/machine.py (+37/-43)
src/maasserver/websockets/handlers/node.py (+65/-49)
src/maasserver/websockets/handlers/notification.py (+2/-2)
src/maasserver/websockets/handlers/sshkey.py (+1/-1)
src/maasserver/websockets/handlers/switch.py (+7/-3)
src/maasserver/websockets/handlers/tests/test_controller.py (+69/-1)
src/maasserver/websockets/handlers/tests/test_device.py (+19/-4)
src/maasserver/websockets/handlers/tests/test_machine.py (+55/-16)
src/maasserver/websockets/handlers/tests/test_switch.py (+1/-20)
src/maasserver/websockets/handlers/user.py (+3/-3)
src/maasserver/websockets/tests/test_base.py (+20/-1)
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
MAAS Lander Needs Fixing
Review via email: mp+342384@code.launchpad.net

Commit message

LP: #1759091 - Limit the amount of queries used during a list operation.

Handlers can now specify a list_queryset which is used when the client calls
list(). The machine, device, and controller handlers have been updated to
only output data needed for the listing page for that node type. All node
types now have their query count tested for list and get.

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b limit_listing_queries lp:~ltrager/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/2198/console
COMMIT: 9c3d32c4d91bc6339b0c191d370d9083f37ce9d0

review: Needs Fixing
Revision history for this message
Blake Rouse (blake-rouse) :
review: Needs Fixing
~ltrager/maas:limit_listing_queries updated
37b692f... by Lee Trager

Merge branch 'master' into limit_listing_queries

c38c9fb... by Lee Trager

Add get_queryset tests

Revision history for this message
Lee Trager (ltrager) wrote :

Thanks for the review. We never had query_count tests for get(), only list(). The reason get() takes so many more queries then list() is due to storage and network interface dehydration. Get queries have gone down with this branch. get() on the MachineHandler went from 61 queries to 48.

I was relying on the query_count tests to test get_queryset, I've added explicit tests for get_queryset.

Revision history for this message
Blake Rouse (blake-rouse) wrote :

Thanks for the extra test and the clarification.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/third_party_drivers.py b/src/maasserver/third_party_drivers.py
2index ff2039a..6c7311b 100644
3--- a/src/maasserver/third_party_drivers.py
4+++ b/src/maasserver/third_party_drivers.py
5@@ -134,13 +134,14 @@ def populate_kernel_opts(driver):
6 return driver
7
8
9-def get_third_party_driver(node):
10+def get_third_party_driver(node, detected_aliases=None):
11 """Determine which, if any, third party driver is required.
12
13 Use the node's modaliases strings to determine if a third party
14 driver is required.
15 """
16- detected_aliases = node.modaliases
17+ if detected_aliases is None:
18+ detected_aliases = node.modaliases
19
20 third_party_drivers_config = DriversConfig.load_from_cache()
21 third_party_drivers = third_party_drivers_config['drivers']
22diff --git a/src/maasserver/websockets/base.py b/src/maasserver/websockets/base.py
23index 636d50a..d7327e4 100644
24--- a/src/maasserver/websockets/base.py
25+++ b/src/maasserver/websockets/base.py
26@@ -81,6 +81,7 @@ class HandlerOptions(object):
27 handler_name = None
28 object_class = None
29 queryset = None
30+ list_queryset = None
31 pk = 'id'
32 pk_type = int
33 fields = None
34@@ -281,19 +282,22 @@ class Handler(metaclass=HandlerMetaclass):
35 })
36 pk = params[self._meta.pk]
37 try:
38- obj = self.get_queryset().get(**{
39+ obj = self.get_queryset(for_list=False).get(**{
40 self._meta.pk: pk,
41 })
42 except self._meta.object_class.DoesNotExist:
43 raise HandlerDoesNotExistError(pk)
44 return obj
45
46- def get_queryset(self):
47+ def get_queryset(self, for_list=False):
48 """Return `QuerySet` used by this handler.
49
50 Override if you need to modify the queryset based on the current user.
51 """
52- return self._meta.queryset
53+ if for_list and self._meta.list_queryset is not None:
54+ return self._meta.list_queryset
55+ else:
56+ return self._meta.queryset
57
58 def get_form_class(self, action):
59 """Return the form class used for `action`.
60@@ -350,7 +354,7 @@ class Handler(metaclass=HandlerMetaclass):
61 :param offset: Offset into the queryset to return.
62 :param limit: Maximum number of objects to return.
63 """
64- queryset = self.get_queryset()
65+ queryset = self.get_queryset(for_list=True)
66 queryset = queryset.order_by(self._meta.batch_key)
67 if "start" in params:
68 queryset = queryset.filter(**{
69diff --git a/src/maasserver/websockets/handlers/controller.py b/src/maasserver/websockets/handlers/controller.py
70index 9fbe5c8..636c4d9 100644
71--- a/src/maasserver/websockets/handlers/controller.py
72+++ b/src/maasserver/websockets/handlers/controller.py
73@@ -24,8 +24,12 @@ class ControllerHandler(MachineHandler):
74 class Meta(MachineHandler.Meta):
75 abstract = False
76 queryset = node_prefetch(
77- Controller.controllers.all().prefetch_related("interface_set"),
78- 'controllerinfo'
79+ Controller.controllers.all().prefetch_related(
80+ "service_set"), "controllerinfo")
81+ list_queryset = (
82+ Controller.controllers.all()
83+ .select_related("controllerinfo", "domain")
84+ .prefetch_related("service_set")
85 )
86 allowed_methods = [
87 'list',
88@@ -90,10 +94,14 @@ class ControllerHandler(MachineHandler):
89 else:
90 raise HandlerError("Unknown action: %s" % action)
91
92- def get_queryset(self):
93+ def get_queryset(self, for_list=False):
94 """Return `QuerySet` for controllers only viewable by `user`."""
95+ if for_list:
96+ qs = self._meta.list_queryset
97+ else:
98+ qs = self._meta.queryset
99 return Controller.controllers.get_nodes(
100- self.user, NODE_PERMISSION.VIEW, from_nodes=self._meta.queryset)
101+ self.user, NODE_PERMISSION.VIEW, from_nodes=qs)
102
103 def dehydrate(self, obj, data, for_list=False):
104 obj = obj.as_self()
105@@ -108,14 +116,15 @@ class ControllerHandler(MachineHandler):
106 if version.is_snap:
107 long_version += " (snap)"
108 data["version__long"] = long_version
109- data["vlan_ids"] = [
110- interface.vlan_id
111- for interface in obj.interface_set.all()
112- ]
113 data["service_ids"] = [
114 service.id
115 for service in obj.service_set.all()
116 ]
117+ if not for_list:
118+ data["vlan_ids"] = [
119+ interface.vlan_id
120+ for interface in obj.interface_set.all()
121+ ]
122 return data
123
124 def check_images(self, params):
125diff --git a/src/maasserver/websockets/handlers/device.py b/src/maasserver/websockets/handlers/device.py
126index dc5374f..0407b0c 100644
127--- a/src/maasserver/websockets/handlers/device.py
128+++ b/src/maasserver/websockets/handlers/device.py
129@@ -37,10 +37,7 @@ from maasserver.websockets.base import (
130 HandlerPermissionError,
131 HandlerValidationError,
132 )
133-from maasserver.websockets.handlers.node import (
134- node_prefetch,
135- NodeHandler,
136-)
137+from maasserver.websockets.handlers.node import NodeHandler
138 from netaddr import EUI
139 from provisioningserver.logger import get_maas_logger
140
141@@ -65,7 +62,16 @@ class DeviceHandler(NodeHandler):
142
143 class Meta(NodeHandler.Meta):
144 abstract = False
145- queryset = node_prefetch(Device.objects.filter(parent=None))
146+ queryset = (
147+ Device.objects.filter(parent=None).select_related(
148+ 'boot_interface', 'owner', 'zone', 'domain')
149+ .prefetch_related(
150+ 'interface_set__ip_addresses__subnet__vlan__space')
151+ .prefetch_related(
152+ 'interface_set__ip_addresses__subnet__vlan__fabric')
153+ .prefetch_related('interface_set__vlan__fabric')
154+ .prefetch_related('tags')
155+ )
156 allowed_methods = [
157 'list',
158 'get',
159@@ -80,6 +86,7 @@ class DeviceHandler(NodeHandler):
160 'update',
161 'action']
162 exclude = [
163+ "bmc",
164 "creation_type",
165 "type",
166 "boot_interface",
167@@ -145,10 +152,11 @@ class DeviceHandler(NodeHandler):
168 getpk = attrgetter(self._meta.pk)
169 self.cache["loaded_pks"].update(getpk(obj) for obj in objs)
170
171- def get_queryset(self):
172+ def get_queryset(self, for_list=False):
173 """Return `QuerySet` for devices only viewable by `user`."""
174 return Device.objects.get_nodes(
175- self.user, NODE_PERMISSION.VIEW, from_nodes=self._meta.queryset)
176+ self.user, NODE_PERMISSION.VIEW, from_nodes=super().get_queryset(
177+ for_list=for_list))
178
179 def dehydrate_parent(self, parent):
180 if parent is None:
181diff --git a/src/maasserver/websockets/handlers/event.py b/src/maasserver/websockets/handlers/event.py
182index b7e4a80..0845b5f 100644
183--- a/src/maasserver/websockets/handlers/event.py
184+++ b/src/maasserver/websockets/handlers/event.py
185@@ -75,7 +75,7 @@ class EventHandler(TimestampedModelHandler):
186 """
187 node = self.get_node(params)
188 self.cache['node_ids'].append(node.id)
189- queryset = self.get_queryset()
190+ queryset = self.get_queryset(for_list=True)
191 queryset = queryset.filter(node=node)
192 queryset = queryset.order_by('-id')
193
194diff --git a/src/maasserver/websockets/handlers/machine.py b/src/maasserver/websockets/handlers/machine.py
195index 375b0fb..ca5cbeb 100644
196--- a/src/maasserver/websockets/handlers/machine.py
197+++ b/src/maasserver/websockets/handlers/machine.py
198@@ -54,7 +54,6 @@ from maasserver.forms.interface import (
199 from maasserver.forms.interface_link import InterfaceLinkForm
200 from maasserver.models.blockdevice import BlockDevice
201 from maasserver.models.cacheset import CacheSet
202-from maasserver.models.config import Config
203 from maasserver.models.filesystem import Filesystem
204 from maasserver.models.filesystemgroup import VolumeGroup
205 from maasserver.models.interface import Interface
206@@ -96,7 +95,20 @@ class MachineHandler(NodeHandler):
207
208 class Meta(NodeHandler.Meta):
209 abstract = False
210- queryset = node_prefetch(Machine.objects.all()).select_related('bmc')
211+ queryset = node_prefetch(Machine.objects.all())
212+ list_queryset = (
213+ Machine.objects.select_related(
214+ 'boot_interface', 'owner', 'zone', 'domain')
215+ .prefetch_related('blockdevice_set__iscsiblockdevice')
216+ .prefetch_related('blockdevice_set__physicalblockdevice')
217+ .prefetch_related('blockdevice_set__virtualblockdevice')
218+ .prefetch_related(
219+ 'interface_set__ip_addresses__subnet__vlan__space')
220+ .prefetch_related(
221+ 'interface_set__ip_addresses__subnet__vlan__fabric')
222+ .prefetch_related('interface_set__vlan__fabric')
223+ .prefetch_related('tags')
224+ )
225 allowed_methods = [
226 'list',
227 'get',
228@@ -177,54 +189,24 @@ class MachineHandler(NodeHandler):
229 "machine",
230 ]
231
232- def get_queryset(self):
233+ def get_queryset(self, for_list=False):
234 """Return `QuerySet` for devices only viewable by `user`."""
235 return Machine.objects.get_nodes(
236- self.user, NODE_PERMISSION.VIEW, from_nodes=self._meta.queryset)
237-
238- def list(self, params):
239- """List objects.
240-
241- Caches default_osystem and default_distro_series so only 1 queries are
242- made for the whole list of nodes.
243-
244- Caches the hardware status so only one additional query is needed for
245- all nodes.
246- """
247- configs = (
248- Config.objects.get_configs(
249- ['default_osystem', 'default_distro_series']))
250- self.default_osystem = configs['default_osystem']
251- self.default_distro_series = configs['default_distro_series']
252- return super(MachineHandler, self).list(params)
253+ self.user, NODE_PERMISSION.VIEW,
254+ from_nodes=super().get_queryset(for_list=for_list))
255
256 def dehydrate(self, obj, data, for_list=False):
257 """Add extra fields to `data`."""
258 data = super(MachineHandler, self).dehydrate(
259 obj, data, for_list=for_list)
260- data["locked"] = obj.locked
261- bmc = obj.bmc
262- if bmc is not None and bmc.bmc_type == BMC_TYPE.POD:
263- data['pod'] = self.dehydrate_pod(bmc)
264-
265- if (getattr(self, 'default_osystem', None) is not None and
266- getattr(self, 'default_distro_series', None) is not None):
267- data["osystem"] = obj.get_osystem(
268- default=self.default_osystem)
269- data["distro_series"] = obj.get_distro_series(
270- default=self.default_distro_series)
271- else:
272- data["osystem"] = obj.get_osystem()
273- data["distro_series"] = obj.get_distro_series()
274- if not for_list:
275- # Add info specific to a machine.
276- data["show_os_info"] = self.dehydrate_show_os_info(obj)
277- devices = [
278- self.dehydrate_device(device)
279- for device in obj.children.all()
280- ]
281- data["devices"] = sorted(
282- devices, key=itemgetter("fqdn"))
283+
284+ if obj.is_machine or not for_list:
285+ boot_interface = obj.get_boot_interface()
286+ if boot_interface is not None:
287+ data["pxe_mac"] = "%s" % boot_interface.mac_address
288+ data["pxe_mac_vendor"] = obj.get_pxe_mac_vendor()
289+ else:
290+ data["pxe_mac"] = data["pxe_mac_vendor"] = ""
291
292 cpu_script_results = [
293 script_result for script_result in
294@@ -279,6 +261,18 @@ class MachineHandler(NodeHandler):
295 else:
296 data["status_tooltip"] = ""
297
298+ if not for_list:
299+ if obj.bmc is not None and obj.bmc.bmc_type == BMC_TYPE.POD:
300+ data['pod'] = self.dehydrate_pod(obj.bmc)
301+ # Add info specific to a machine.
302+ data["show_os_info"] = self.dehydrate_show_os_info(obj)
303+ devices = [
304+ self.dehydrate_device(device)
305+ for device in obj.children.all()
306+ ]
307+ data["devices"] = sorted(
308+ devices, key=itemgetter("fqdn"))
309+
310 return data
311
312 def dehydrate_show_os_info(self, obj):
313diff --git a/src/maasserver/websockets/handlers/node.py b/src/maasserver/websockets/handlers/node.py
314index 92fc9b5..8f33bdc 100644
315--- a/src/maasserver/websockets/handlers/node.py
316+++ b/src/maasserver/websockets/handlers/node.py
317@@ -54,6 +54,9 @@ from metadataserver.enum import (
318 )
319 from metadataserver.models.scriptresult import ScriptResult
320 from metadataserver.models.scriptset import get_status_from_qs
321+from provisioningserver.refresh.node_info_scripts import (
322+ LIST_MODALIASES_OUTPUT_NAME,
323+)
324 from provisioningserver.tags import merge_details_cleanly
325
326
327@@ -69,7 +72,8 @@ NODE_TYPE_TO_LINK_TYPE = {
328 def node_prefetch(queryset, *args):
329 return (
330 queryset
331- .select_related('boot_interface', 'owner', 'zone', 'domain', *args)
332+ .select_related(
333+ 'boot_interface', 'owner', 'zone', 'domain', 'bmc', *args)
334 .prefetch_related('blockdevice_set__iscsiblockdevice')
335 .prefetch_related('blockdevice_set__physicalblockdevice')
336 .prefetch_related('blockdevice_set__virtualblockdevice')
337@@ -84,9 +88,6 @@ def node_prefetch(queryset, *args):
338
339 class NodeHandler(TimestampedModelHandler):
340
341- default_osystem = None
342- default_distro_series = None
343-
344 class Meta:
345 abstract = True
346 pk = 'system_id'
347@@ -186,35 +187,10 @@ class NodeHandler(TimestampedModelHandler):
348 data["node_type_display"] = obj.get_node_type_display()
349 data["link_type"] = NODE_TYPE_TO_LINK_TYPE[obj.node_type]
350
351- data["extra_macs"] = [
352- "%s" % mac_address
353- for mac_address in obj.get_extra_macs()
354- ]
355- subnets = self.get_all_subnets(obj)
356- data["subnets"] = [subnet.cidr for subnet in subnets]
357- data["fabrics"] = self.get_all_fabric_names(obj, subnets)
358- data["spaces"] = self.get_all_space_names(subnets)
359-
360- data["tags"] = [
361- tag.name
362- for tag in obj.tags.all()
363- ]
364- data["metadata"] = {
365- metadata.key: metadata.value
366- for metadata in obj.nodemetadata_set.all()
367- }
368- if obj.node_type != NODE_TYPE.DEVICE:
369- data["architecture"] = obj.architecture
370- data["memory"] = obj.display_memory()
371- data["status"] = obj.display_status()
372- data["status_code"] = obj.status
373- boot_interface = obj.get_boot_interface()
374- if boot_interface is not None:
375- data["pxe_mac"] = "%s" % boot_interface.mac_address
376- data["pxe_mac_vendor"] = obj.get_pxe_mac_vendor()
377- else:
378- data["pxe_mac"] = data["pxe_mac_vendor"] = ""
379-
380+ if obj.node_type == NODE_TYPE.MACHINE or (
381+ obj.is_controller and not for_list):
382+ # Disk count and storage amount is shown on the machine listing
383+ # page and the machine and controllers details page.
384 blockdevices = self.get_blockdevices_for(obj)
385 physical_blockdevices = [
386 blockdevice for blockdevice in blockdevices
387@@ -227,16 +203,6 @@ class NodeHandler(TimestampedModelHandler):
388 for blockdevice in physical_blockdevices
389 ) / (1000 ** 3))
390 data["storage_tags"] = self.get_all_storage_tags(blockdevices)
391- data["grouped_storages"] = self.get_grouped_storages(
392- physical_blockdevices)
393-
394- data["osystem"] = obj.get_osystem(
395- default=self.default_osystem)
396- data["distro_series"] = obj.get_distro_series(
397- default=self.default_distro_series)
398- data["dhcp_on"] = self.get_providing_dhcp(obj)
399-
400- if obj.node_type != NODE_TYPE.DEVICE:
401 commissioning_script_results = []
402 testing_script_results = []
403 log_results = set()
404@@ -254,12 +220,12 @@ class NodeHandler(TimestampedModelHandler):
405 RESULT_TYPE.COMMISSIONING):
406 commissioning_script_results.append(script_result)
407 if (script_result.name in script_output_nsmap and
408- script_result.status == SCRIPT_STATUS.PASSED):
409+ script_result.status ==
410+ SCRIPT_STATUS.PASSED):
411 log_results.add(script_result.name)
412 elif (script_result.script_set.result_type ==
413 RESULT_TYPE.TESTING):
414 testing_script_results.append(script_result)
415-
416 data["commissioning_script_count"] = len(
417 commissioning_script_results)
418 data["commissioning_status"] = get_status_from_qs(
419@@ -269,11 +235,41 @@ class NodeHandler(TimestampedModelHandler):
420 commissioning_script_results).replace(
421 'test', 'commissioning script'))
422 data["testing_script_count"] = len(testing_script_results)
423- data["testing_status"] = get_status_from_qs(testing_script_results)
424+ data["testing_status"] = get_status_from_qs(
425+ testing_script_results)
426 data["testing_status_tooltip"] = (
427- self.dehydrate_hardware_status_tooltip(testing_script_results))
428+ self.dehydrate_hardware_status_tooltip(
429+ testing_script_results))
430 data["has_logs"] = (
431- log_results.difference(script_output_nsmap.keys()) == set())
432+ log_results.difference(script_output_nsmap.keys()) ==
433+ set())
434+ else:
435+ blockdevices = []
436+
437+ if obj.node_type != NODE_TYPE.DEVICE:
438+ # These values are not defined on a device.
439+ data["architecture"] = obj.architecture
440+ data["osystem"] = obj.osystem
441+ data["distro_series"] = obj.distro_series
442+ data["memory"] = obj.display_memory()
443+ data["status"] = obj.display_status()
444+ data["status_code"] = obj.status
445+
446+ # Filters are only available on machines and devices.
447+ if not obj.is_controller:
448+ # For filters
449+ subnets = self.get_all_subnets(obj)
450+ data["subnets"] = [subnet.cidr for subnet in subnets]
451+ data["fabrics"] = self.get_all_fabric_names(obj, subnets)
452+ data["spaces"] = self.get_all_space_names(subnets)
453+ data["tags"] = [
454+ tag.name
455+ for tag in obj.tags.all()
456+ ]
457+ data["extra_macs"] = [
458+ "%s" % mac_address
459+ for mac_address in obj.get_extra_macs()
460+ ]
461
462 if not for_list:
463 data["on_network"] = obj.on_network()
464@@ -281,11 +277,18 @@ class NodeHandler(TimestampedModelHandler):
465 # XXX lamont 2017-02-15 Much of this should be split out into
466 # individual methods, rather than having this huge block of
467 # dense code here.
468+ # Status of the commissioning, testing, and logs tabs
469+ data["metadata"] = {
470+ metadata.key: metadata.value
471+ for metadata in obj.nodemetadata_set.all()
472+ }
473+
474 # Network
475 data["interfaces"] = [
476 self.dehydrate_interface(interface, obj)
477 for interface in obj.interface_set.all().order_by('name')
478 ]
479+ data["dhcp_on"] = self.get_providing_dhcp(obj)
480
481 data["hwe_kernel"] = make_hwe_kernel_ui_text(obj.hwe_kernel)
482
483@@ -313,6 +316,8 @@ class NodeHandler(TimestampedModelHandler):
484 self.dehydrate_filesystem(filesystem)
485 for filesystem in obj.special_filesystems.order_by("id")
486 ]
487+ data["grouped_storages"] = self.get_grouped_storages(
488+ physical_blockdevices)
489
490 # Events
491 data["events"] = self.dehydrate_events(obj)
492@@ -323,7 +328,18 @@ class NodeHandler(TimestampedModelHandler):
493
494 # Third party drivers
495 if Config.objects.get_config('enable_third_party_drivers'):
496- driver = get_third_party_driver(obj)
497+ # Pull modaliases from the cache
498+ modaliases = []
499+ for script_result in commissioning_script_results:
500+ if script_result.name == LIST_MODALIASES_OUTPUT_NAME:
501+ if script_result.status == SCRIPT_STATUS.PASSED:
502+ # STDOUT is deferred in the cache so load it.
503+ script_result = ScriptResult.objects.filter(
504+ id=script_result.id).only(
505+ 'id', 'stdout').first()
506+ modaliases = script_result.stdout.decode(
507+ 'utf-8').splitlines()
508+ driver = get_third_party_driver(obj, modaliases)
509 if "module" in driver and "comment" in driver:
510 data["third_party_driver"] = {
511 "module": driver["module"],
512diff --git a/src/maasserver/websockets/handlers/notification.py b/src/maasserver/websockets/handlers/notification.py
513index 7292e68..d189e75 100644
514--- a/src/maasserver/websockets/handlers/notification.py
515+++ b/src/maasserver/websockets/handlers/notification.py
516@@ -1,4 +1,4 @@
517-# Copyright 2017 Canonical Ltd. This software is licensed under the
518+# Copyright 2017-2018 Canonical Ltd. This software is licensed under the
519 # GNU Affero General Public License version 3 (see the file LICENSE).
520
521 """The notification handler for the WebSocket connection."""
522@@ -22,7 +22,7 @@ class NotificationHandler(TimestampedModelHandler):
523 exclude = list_exclude = {"context"}
524 listen_channels = {'notification', 'notificationdismissal'}
525
526- def get_queryset(self):
527+ def get_queryset(self, for_list=False):
528 """Return `Notifications` for the current user."""
529 return Notification.objects.find_for_user(self.user)
530
531diff --git a/src/maasserver/websockets/handlers/sshkey.py b/src/maasserver/websockets/handlers/sshkey.py
532index efac2e6..37503dc 100644
533--- a/src/maasserver/websockets/handlers/sshkey.py
534+++ b/src/maasserver/websockets/handlers/sshkey.py
535@@ -41,7 +41,7 @@ class SSHKeyHandler(TimestampedModelHandler):
536 "sshkey",
537 ]
538
539- def get_queryset(self):
540+ def get_queryset(self, for_list=False):
541 """Return `QuerySet` for SSH keys owned by `user`."""
542 return self._meta.queryset.filter(user=self.user)
543
544diff --git a/src/maasserver/websockets/handlers/switch.py b/src/maasserver/websockets/handlers/switch.py
545index e40f779..72accf5 100644
546--- a/src/maasserver/websockets/handlers/switch.py
547+++ b/src/maasserver/websockets/handlers/switch.py
548@@ -1,4 +1,4 @@
549-# Copyright 2017 Canonical Ltd. This software is licensed under the
550+# Copyright 2017-2018 Canonical Ltd. This software is licensed under the
551 # GNU Affero General Public License version 3 (see the file LICENSE).
552
553 """The switch handler for the WebSocket connection."""
554@@ -45,10 +45,14 @@ class SwitchHandler(NodeHandler):
555 'switch',
556 ]
557
558- def get_queryset(self):
559+ def get_queryset(self, for_list=False):
560 """Return `QuerySet` for devices only viewable by `user`."""
561+ # FIXME - Return a different query set when for_list is true. This
562+ # should contain only the items needed to display a switch when listing
563+ # in the UI.
564 return Node.objects.get_nodes(
565- self.user, NODE_PERMISSION.VIEW, from_nodes=self._meta.queryset)
566+ self.user, NODE_PERMISSION.VIEW,
567+ from_nodes=self._meta.queryset)
568
569 def get_object(self, params):
570 """Get object by using the `pk` in `params`."""
571diff --git a/src/maasserver/websockets/handlers/tests/test_controller.py b/src/maasserver/websockets/handlers/tests/test_controller.py
572index 18d2433..e86c19a 100644
573--- a/src/maasserver/websockets/handlers/tests/test_controller.py
574+++ b/src/maasserver/websockets/handlers/tests/test_controller.py
575@@ -1,4 +1,4 @@
576-# Copyright 2016-2017 Canonical Ltd. This software is licensed under the
577+# Copyright 2016-2018 Canonical Ltd. This software is licensed under the
578 # GNU Affero General Public License version 3 (see the file LICENSE).
579
580 """Tests for `maasserver.websockets.handlers.controller`"""
581@@ -12,6 +12,11 @@ from maasserver.testing.factory import factory
582 from maasserver.testing.testcase import MAASServerTestCase
583 from maasserver.websockets.base import dehydrate_datetime
584 from maasserver.websockets.handlers.controller import ControllerHandler
585+from maastesting.djangotestcase import count_queries
586+from metadataserver.enum import (
587+ RESULT_TYPE,
588+ SCRIPT_STATUS,
589+)
590 from testscenarios import multiply_scenarios
591 from testtools.matchers import (
592 ContainsDict,
593@@ -65,6 +70,69 @@ class TestControllerHandler(MAASServerTestCase):
594 self.assertEqual(1, len(result))
595 self.assertEqual(NODE_TYPE.RACK_CONTROLLER, result[0].get('node_type'))
596
597+ def test_list_num_queries_is_the_expected_number(self):
598+ owner = factory.make_admin()
599+ for _ in range(10):
600+ node = factory.make_RegionRackController(owner=owner)
601+ commissioning_script_set = factory.make_ScriptSet(
602+ node=node, result_type=RESULT_TYPE.COMMISSIONING)
603+ testing_script_set = factory.make_ScriptSet(
604+ node=node, result_type=RESULT_TYPE.TESTING)
605+ node.current_commissioning_script_set = commissioning_script_set
606+ node.current_testing_script_set = testing_script_set
607+ node.save()
608+ for __ in range(10):
609+ factory.make_ScriptResult(
610+ status=SCRIPT_STATUS.PASSED,
611+ script_set=commissioning_script_set)
612+ factory.make_ScriptResult(
613+ status=SCRIPT_STATUS.PASSED,
614+ script_set=testing_script_set)
615+
616+ handler = ControllerHandler(owner, {})
617+ queries_one, _ = count_queries(handler.list, {'limit': 1})
618+ queries_total, _ = count_queries(handler.list, {})
619+ # This check is to notify the developer that a change was made that
620+ # affects the number of queries performed when doing a node listing.
621+ # It is important to keep this number as low as possible. A larger
622+ # number means regiond has to do more work slowing down its process
623+ # and slowing down the client waiting for the response.
624+ self.assertEqual(
625+ queries_one, 3,
626+ "Number of queries has changed; make sure this is expected.")
627+ self.assertEqual(
628+ queries_total, 3,
629+ "Number of queries has changed; make sure this is expected.")
630+
631+ def test_get_num_queries_is_the_expected_number(self):
632+ owner = factory.make_admin()
633+ node = factory.make_RegionRackController(owner=owner)
634+ commissioning_script_set = factory.make_ScriptSet(
635+ node=node, result_type=RESULT_TYPE.COMMISSIONING)
636+ testing_script_set = factory.make_ScriptSet(
637+ node=node, result_type=RESULT_TYPE.TESTING)
638+ node.current_commissioning_script_set = commissioning_script_set
639+ node.current_testing_script_set = testing_script_set
640+ node.save()
641+ for __ in range(10):
642+ factory.make_ScriptResult(
643+ status=SCRIPT_STATUS.PASSED,
644+ script_set=commissioning_script_set)
645+ factory.make_ScriptResult(
646+ status=SCRIPT_STATUS.PASSED,
647+ script_set=testing_script_set)
648+
649+ handler = ControllerHandler(owner, {})
650+ queries, _ = count_queries(handler.get, {'system_id': node.system_id})
651+ # This check is to notify the developer that a change was made that
652+ # affects the number of queries performed when doing a node get.
653+ # It is important to keep this number as low as possible. A larger
654+ # number means regiond has to do more work slowing down its process
655+ # and slowing down the client waiting for the response.
656+ self.assertEqual(
657+ queries, 28,
658+ "Number of queries has changed; make sure this is expected.")
659+
660 def test_get_form_class_for_create(self):
661 user = factory.make_admin()
662 handler = ControllerHandler(user, {})
663diff --git a/src/maasserver/websockets/handlers/tests/test_device.py b/src/maasserver/websockets/handlers/tests/test_device.py
664index ab26620..d6af973 100644
665--- a/src/maasserver/websockets/handlers/tests/test_device.py
666+++ b/src/maasserver/websockets/handlers/tests/test_device.py
667@@ -137,7 +137,6 @@ class TestDeviceHandler(MAASTransactionServerTestCase):
668 boot_interface = node.get_boot_interface()
669 data = {
670 "actions": list(compile_node_actions(node, user).keys()),
671- "bmc": node.bmc_id,
672 "created": dehydrate_datetime(node.created),
673 "domain": {
674 "id": node.domain.id,
675@@ -149,7 +148,6 @@ class TestDeviceHandler(MAASTransactionServerTestCase):
676 ],
677 "fqdn": node.fqdn,
678 "hostname": node.hostname,
679- "metadata": {},
680 "node_type_display": node.get_node_type_display(),
681 "link_type": NODE_TYPE_TO_LINK_TYPE[node.node_type],
682 "id": node.id,
683@@ -195,7 +193,6 @@ class TestDeviceHandler(MAASTransactionServerTestCase):
684 "ip_address",
685 "ip_assignment",
686 "link_type",
687- "metadata",
688 "node_type_display",
689 "primary_mac",
690 "spaces",
691@@ -253,6 +250,24 @@ class TestDeviceHandler(MAASTransactionServerTestCase):
692 handler.get({"system_id": device.system_id}))
693
694 @transactional
695+ def test_get_num_queries_is_the_expected_number(self):
696+ owner = factory.make_User()
697+ handler = DeviceHandler(owner, {})
698+ device = self.make_device_with_ip_address(
699+ owner=owner, ip_assignment=DEVICE_IP_ASSIGNMENT_TYPE.STATIC)
700+ queries, _ = count_queries(
701+ handler.get, {"system_id": device.system_id})
702+
703+ # This check is to notify the developer that a change was made that
704+ # affects the number of queries performed when doing a node get.
705+ # It is important to keep this number as low as possible. A larger
706+ # number means regiond has to do more work slowing down its process
707+ # and slowing down the client waiting for the response.
708+ self.assertEqual(
709+ queries, 19,
710+ "Number of queries has changed; make sure this is expected.")
711+
712+ @transactional
713 def test_list(self):
714 owner = factory.make_User()
715 handler = DeviceHandler(owner, {})
716@@ -301,7 +316,7 @@ class TestDeviceHandler(MAASTransactionServerTestCase):
717 # number means regiond has to do more work slowing down its process
718 # and slowing down the client waiting for the response.
719 self.assertEqual(
720- query_10_count, 13,
721+ query_10_count, 10,
722 "Number of queries has changed; make sure this is expected.")
723
724 @transactional
725diff --git a/src/maasserver/websockets/handlers/tests/test_machine.py b/src/maasserver/websockets/handlers/tests/test_machine.py
726index 27bc195..3b333ed 100644
727--- a/src/maasserver/websockets/handlers/tests/test_machine.py
728+++ b/src/maasserver/websockets/handlers/tests/test_machine.py
729@@ -150,11 +150,12 @@ class TestMachineHandler(MAASServerTestCase):
730
731 boot_interface = node.get_boot_interface()
732 pxe_mac_vendor = node.get_pxe_mac_vendor()
733+ subnets = handler.get_all_subnets(node)
734+
735 blockdevices = [
736 blockdevice.actual_instance
737 for blockdevice in node.blockdevice_set.all()
738 ]
739- driver = get_third_party_driver(node)
740 disks = [
741 handler.dehydrate_blockdevice(blockdevice, node)
742 for blockdevice in blockdevices
743@@ -167,8 +168,10 @@ class TestMachineHandler(MAASServerTestCase):
744 for cache_set in CacheSet.objects.get_cache_sets_for_node(node)
745 ]
746 disks = sorted(disks, key=itemgetter("name"))
747- subnets = handler.get_all_subnets(node)
748- commissioning_scripts = node.get_latest_commissioning_script_results
749+ driver = get_third_party_driver(node)
750+
751+ commissioning_scripts = (
752+ node.get_latest_commissioning_script_results)
753 commissioning_scripts = commissioning_scripts.exclude(
754 status=SCRIPT_STATUS.ABORTED)
755 testing_scripts = node.get_latest_testing_script_results
756@@ -179,6 +182,7 @@ class TestMachineHandler(MAASServerTestCase):
757 if (script_result.name in script_output_nsmap and
758 script_result.status == SCRIPT_STATUS.PASSED):
759 log_results.add(script_result.name)
760+
761 data = {
762 "actions": list(compile_node_actions(node, handler.user).keys()),
763 "architecture": node.architecture,
764@@ -186,25 +190,27 @@ class TestMachineHandler(MAASServerTestCase):
765 "boot_disk": node.boot_disk,
766 "bios_boot_method": node.bios_boot_method,
767 "commissioning_script_count": commissioning_scripts.count(),
768- "commissioning_status": get_status_from_qs(commissioning_scripts),
769+ "commissioning_status": get_status_from_qs(
770+ commissioning_scripts),
771 "commissioning_status_tooltip": (
772 handler.dehydrate_hardware_status_tooltip(
773 commissioning_scripts).replace(
774 'test', 'commissioning script')),
775 "current_commissioning_script_set": (
776 node.current_commissioning_script_set_id),
777+ "current_testing_script_set": node.current_testing_script_set_id,
778 "testing_script_count": testing_scripts.count(),
779 "testing_status": get_status_from_qs(testing_scripts),
780 "testing_status_tooltip": (
781- handler.dehydrate_hardware_status_tooltip(testing_scripts)),
782- "current_testing_script_set": node.current_testing_script_set_id,
783+ handler.dehydrate_hardware_status_tooltip(
784+ testing_scripts)),
785 "current_installation_script_set": (
786 node.current_installation_script_set_id),
787 "installation_status": (
788 handler.dehydrate_script_set_status(
789 node.current_installation_script_set)),
790- "has_logs": (
791- log_results.difference(script_output_nsmap.keys()) == set()),
792+ "has_logs": (log_results.difference(
793+ script_output_nsmap.keys()) == set()),
794 "locked": node.locked,
795 "cpu_count": node.cpu_count,
796 "cpu_speed": node.cpu_speed,
797@@ -231,7 +237,7 @@ class TestMachineHandler(MAASServerTestCase):
798 "supported_filesystems": [
799 {'key': key, 'ui': ui}
800 for key, ui in FILESYSTEM_FORMAT_TYPE_CHOICES],
801- "distro_series": node.get_distro_series(),
802+ "distro_series": node.distro_series,
803 "error": node.error,
804 "error_description": node.error_description,
805 "events": handler.dehydrate_events(node),
806@@ -251,10 +257,9 @@ class TestMachineHandler(MAASServerTestCase):
807 "license_key": node.license_key,
808 "link_type": NODE_TYPE_TO_LINK_TYPE[node.node_type],
809 "memory": node.display_memory(),
810- "metadata": {},
811 "node_type_display": node.get_node_type_display(),
812 "min_hwe_kernel": node.min_hwe_kernel,
813- "osystem": node.get_osystem(),
814+ "osystem": node.osystem,
815 "owner": handler.dehydrate_owner(node.owner),
816 "power_parameters": handler.dehydrate_power_parameters(
817 node.power_parameters),
818@@ -292,7 +297,6 @@ class TestMachineHandler(MAASServerTestCase):
819 "zone": handler.dehydrate_zone(node.zone),
820 "pool": handler.dehydrate_pool(node.pool),
821 "default_user": node.default_user,
822- "dhcp_on": node.interface_set.filter(vlan__dhcp_on=True).exists(),
823 }
824 bmc = node.bmc
825 if bmc is not None and bmc.bmc_type == BMC_TYPE.POD:
826@@ -333,6 +337,13 @@ class TestMachineHandler(MAASServerTestCase):
827 for key in list(data):
828 if key not in allowed_fields:
829 del data[key]
830+ else:
831+ data.update({
832+ "dhcp_on": node.interface_set.filter(
833+ vlan__dhcp_on=True).exists(),
834+ "grouped_storages": handler.get_grouped_storages(blockdevices),
835+ "metadata": {},
836+ })
837
838 cpu_script_results = [
839 script_result for script_result in
840@@ -395,8 +406,6 @@ class TestMachineHandler(MAASServerTestCase):
841 else:
842 data["status_tooltip"] = ""
843
844- data["grouped_storages"] = handler.get_grouped_storages(blockdevices)
845-
846 return data
847
848 def make_nodes(self, number):
849@@ -495,10 +504,39 @@ class TestMachineHandler(MAASServerTestCase):
850 # number means regiond has to do more work slowing down its process
851 # and slowing down the client waiting for the response.
852 self.assertEqual(
853- queries_one, 11,
854+ queries_one, 8,
855+ "Number of queries has changed; make sure this is expected.")
856+ self.assertEqual(
857+ queries_total, 8,
858 "Number of queries has changed; make sure this is expected.")
859+
860+ def test_get_num_queries_is_the_expected_number(self):
861+ owner = factory.make_User()
862+ node = factory.make_Node(owner=owner)
863+ commissioning_script_set = factory.make_ScriptSet(
864+ node=node, result_type=RESULT_TYPE.COMMISSIONING)
865+ testing_script_set = factory.make_ScriptSet(
866+ node=node, result_type=RESULT_TYPE.TESTING)
867+ node.current_commissioning_script_set = commissioning_script_set
868+ node.current_testing_script_set = testing_script_set
869+ node.save()
870+ for __ in range(10):
871+ factory.make_ScriptResult(
872+ status=SCRIPT_STATUS.PASSED,
873+ script_set=commissioning_script_set)
874+ factory.make_ScriptResult(
875+ status=SCRIPT_STATUS.PASSED,
876+ script_set=testing_script_set)
877+
878+ handler = MachineHandler(owner, {})
879+ queries, _ = count_queries(handler.get, {'system_id': node.system_id})
880+ # This check is to notify the developer that a change was made that
881+ # affects the number of queries performed when doing a node get.
882+ # It is important to keep this number as low as possible. A larger
883+ # number means regiond has to do more work slowing down its process
884+ # and slowing down the client waiting for the response.
885 self.assertEqual(
886- queries_total, 11,
887+ queries, 48,
888 "Number of queries has changed; make sure this is expected.")
889
890 def test_trigger_update_updates_script_result_cache(self):
891@@ -521,6 +559,7 @@ class TestMachineHandler(MAASServerTestCase):
892
893 handler = MachineHandler(owner, {})
894 # Simulate a trigger pushing an update to the UI
895+ handler.cache = {'active_pk': node.system_id}
896 _, _, ret = handler.on_listen_for_active_pk(
897 'update', node.system_id, node)
898 self.assertEquals(ret['commissioning_script_count'], 10)
899diff --git a/src/maasserver/websockets/handlers/tests/test_switch.py b/src/maasserver/websockets/handlers/tests/test_switch.py
900index 6313a5f..d3f4689 100644
901--- a/src/maasserver/websockets/handlers/tests/test_switch.py
902+++ b/src/maasserver/websockets/handlers/tests/test_switch.py
903@@ -5,10 +5,7 @@
904
905 __all__ = []
906
907-from maasserver.enum import (
908- NODE_METADATA,
909- NODE_TYPE,
910-)
911+from maasserver.enum import NODE_TYPE
912 from maasserver.exceptions import NodeActionError
913 from maasserver.testing.factory import factory
914 from maasserver.testing.testcase import MAASTransactionServerTestCase
915@@ -67,22 +64,6 @@ class TestSwitchHandler(MAASTransactionServerTestCase):
916 [result['system_id'] for result in handler.list({})])
917
918 @transactional
919- def test_list_switches_includes_metadata(self):
920- owner = factory.make_User()
921- handler = SwitchHandler(owner, {})
922- machine = factory.make_Machine(owner=owner)
923- metadata = {
924- NODE_METADATA.VENDOR_NAME: "Canonical",
925- NODE_METADATA.PHYSICAL_MODEL_NAME: "Cloud-in-a-box"
926- }
927- for key, value in metadata.items():
928- factory.make_NodeMetadata(node=machine, key=key, value=value)
929- factory.make_Switch(node=machine)
930- self.assertItemsEqual(
931- [metadata],
932- [result['metadata'] for result in handler.list({})])
933-
934- @transactional
935 def test_list_ignores_nodes_that_arent_switches(self):
936 owner = factory.make_User()
937 handler = SwitchHandler(owner, {})
938diff --git a/src/maasserver/websockets/handlers/user.py b/src/maasserver/websockets/handlers/user.py
939index 8c10046..cc8759a 100644
940--- a/src/maasserver/websockets/handlers/user.py
941+++ b/src/maasserver/websockets/handlers/user.py
942@@ -1,4 +1,4 @@
943-# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
944+# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
945 # GNU Affero General Public License version 3 (see the file LICENSE).
946
947 """The user handler for the WebSocket connection."""
948@@ -35,9 +35,9 @@ class UserHandler(Handler):
949 "user",
950 ]
951
952- def get_queryset(self):
953+ def get_queryset(self, for_list=False):
954 """Return `QuerySet` for users only viewable by `user`."""
955- users = super(UserHandler, self).get_queryset()
956+ users = super(UserHandler, self).get_queryset(for_list=for_list)
957 if reload_object(self.user).is_superuser:
958 # Super users can view all users, except for the built-in users
959 return users.exclude(username__in=SYSTEM_USERS)
960diff --git a/src/maasserver/websockets/tests/test_base.py b/src/maasserver/websockets/tests/test_base.py
961index 9a3001f..ee71feb 100644
962--- a/src/maasserver/websockets/tests/test_base.py
963+++ b/src/maasserver/websockets/tests/test_base.py
964@@ -1,4 +1,4 @@
965-# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
966+# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
967 # GNU Affero General Public License version 3 (see the file LICENSE).
968
969 """Tests for `maasserver.websockets.base`"""
970@@ -337,6 +337,25 @@ class TestHandler(MAASServerTestCase):
971 HandlerDoesNotExistError,
972 handler.get_object, {"system_id": machine.system_id})
973
974+ def test_get_queryset(self):
975+ queryset = MagicMock()
976+ list_queryset = MagicMock()
977+ handler = make_handler(
978+ "TestHandler", queryset=queryset, list_queryset=list_queryset)
979+ self.assertEqual(queryset, handler.get_queryset())
980+
981+ def test_get_queryset_list(self):
982+ queryset = MagicMock()
983+ list_queryset = MagicMock()
984+ handler = make_handler(
985+ "TestHandler", queryset=queryset, list_queryset=list_queryset)
986+ self.assertEqual(list_queryset, handler.get_queryset(for_list=True))
987+
988+ def test_get_queryset_list_only_if_avail(self):
989+ queryset = MagicMock()
990+ handler = make_handler("TestHandler", queryset=queryset)
991+ self.assertEqual(queryset, handler.get_queryset(for_list=True))
992+
993 def test_execute_only_allows_meta_allowed_methods(self):
994 handler = self.make_nodes_handler(allowed_methods=['list'])
995 with ExpectedException(HandlerNoSuchMethodError):

Subscribers

People subscribed via source and target branches