Merge ~ltrager/maas:health_status into maas:master

Proposed by Lee Trager
Status: Merged
Approved by: Lee Trager
Approved revision: 1f3b9c2ff223ab7b6aa235d4cea0e92c632df7be
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~ltrager/maas:health_status
Merge into: maas:master
Diff against target: 846 lines (+387/-116)
18 files modified
src/maasserver/api/machines.py (+10/-0)
src/maasserver/api/nodes.py (+94/-1)
src/maasserver/api/rackcontrollers.py (+10/-0)
src/maasserver/api/regioncontrollers.py (+10/-0)
src/maasserver/api/tests/test_enlistment.py (+20/-0)
src/maasserver/api/tests/test_machines.py (+5/-5)
src/maasserver/api/tests/test_node.py (+69/-1)
src/maasserver/api/tests/test_rackcontroller.py (+10/-0)
src/maasserver/api/tests/test_regioncontroller.py (+10/-0)
src/maasserver/api/tests/test_tag.py (+10/-10)
src/maasserver/static/js/angular/controllers/node_details.js (+1/-1)
src/maasserver/static/js/angular/controllers/tests/test_node_details.js (+1/-1)
src/maasserver/static/partials/machines-table.html (+8/-2)
src/maasserver/static/partials/node-details.html (+3/-3)
src/maasserver/websockets/handlers/machine.py (+1/-43)
src/maasserver/websockets/handlers/node.py (+81/-18)
src/maasserver/websockets/handlers/tests/test_device.py (+8/-7)
src/maasserver/websockets/handlers/tests/test_machine.py (+36/-24)
Reviewer Review Type Date Requested Status
MAAS Lander Needs Fixing
Andres Rodriguez (community) Approve
Review via email: mp+332770@code.launchpad.net

Commit message

LP: #1721824, #1721823 - Add health status

The UI now shows the health status of the node when the spinner isn't being
shown. The health status is the overall status of all commissioning and
testing scripts. Like the health status icons for CPU, memory, and storage
the health status icon will only display if there is a problem.

The commissioning, testing, CPU, memory, storage, and overall health are now
reported on the API. Like the websocket ScriptResults are cached so they are
only loaded once.

Description of the change

Screenshot: https://screenshots.firefox.com/2AkiOet6vkZQt31m/10.0.0.2
A tooltip is also set but isn't being shown due to LP: #1718776

To post a comment you must log in.
Revision history for this message
Andres Rodriguez (andreserl) wrote :

Let's minimize this branch to *only* include hardware test stuff.

You can put a different branch for commissioning, but thinking about it, we wont really need provided that commissioning will always have to finish successfully for a machine to be marked ready. e.g. there are no instances where a failed commissioning script would allow a machine to transition to ready state.

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

I've split including commissioning results in the overall health status into its own review.

https://code.launchpad.net/~ltrager/maas/+git/maas/+merge/332883

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

Still needs fixing. This branch still includes commissioning status related stuff.

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

more comments

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b health_status lp:~ltrager/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/551/console
COMMIT: c2a9049d9bedfd56b4f90cad7a81b329ca949eba

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

As discussed this branch no longer calculates the overall health status by looking at the testing and commissioning results. Instead the health status is the testing status.

To produce the testing_status, cpu_status, memory_status and storage_status in the API I need to cache and iterate through all ScriptResults on the node. As I'm doing that I capture the overall commissioning status and output everything in the API. This gives the API and UI feature parity with regards to status.

In the websocket I noticed we were not calculating the testing_status or commissioning_status from cache. I modified the code to calculate both from cache the values are still the same.

Revision history for this message
Andres Rodriguez (andreserl) :
review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b health_status lp:~ltrager/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/560/console
COMMIT: 6a8ddb9741edc1813f81a51f7745c32faeac7475

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :
~ltrager/maas:health_status updated
1f3b9c2... by Lee Trager

Fix lint

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/src/maasserver/api/machines.py b/src/maasserver/api/machines.py
index d68687a..4218bdc 100644
--- a/src/maasserver/api/machines.py
+++ b/src/maasserver/api/machines.py
@@ -146,6 +146,16 @@ DISPLAYED_MACHINE_FIELDS = (
146 'current_commissioning_result_id',146 'current_commissioning_result_id',
147 'current_testing_result_id',147 'current_testing_result_id',
148 'current_installation_result_id',148 'current_installation_result_id',
149 'commissioning_status',
150 'commissioning_status_name',
151 'testing_status',
152 'testing_status_name',
153 'cpu_test_status',
154 'cpu_test_status_name',
155 'memory_test_status',
156 'memory_test_status_name',
157 'storage_test_status',
158 'storage_test_status_name',
149)159)
150160
151# Limited set of machine fields exposed on the anonymous API.161# Limited set of machine fields exposed on the anonymous API.
diff --git a/src/maasserver/api/nodes.py b/src/maasserver/api/nodes.py
index 8743375..13d904b 100644
--- a/src/maasserver/api/nodes.py
+++ b/src/maasserver/api/nodes.py
@@ -1,4 +1,4 @@
1# Copyright 2012-2016 Canonical Ltd. This software is licensed under the1# Copyright 2012-2017 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__all__ = [4__all__ = [
@@ -56,6 +56,13 @@ from maasserver.models import (
56)56)
57from maasserver.models.nodeprobeddetails import get_single_probed_details57from maasserver.models.nodeprobeddetails import get_single_probed_details
58from maasserver.utils.orm import prefetch_queryset58from maasserver.utils.orm import prefetch_queryset
59from metadataserver.enum import (
60 HARDWARE_TYPE,
61 RESULT_TYPE,
62 SCRIPT_STATUS,
63 SCRIPT_STATUS_CHOICES,
64)
65from metadataserver.models.scriptset import get_status_from_qs
59from piston3.utils import rc66from piston3.utils import rc
60from provisioningserver.drivers.power import UNKNOWN_POWER_TYPE67from provisioningserver.drivers.power import UNKNOWN_POWER_TYPE
6168
@@ -246,6 +253,38 @@ def is_registered(request):
246 return interfaces.exists()253 return interfaces.exists()
247254
248255
256def get_cached_script_results(node):
257 """Load script results into cache and return the cached list."""
258 if not hasattr(node, '_cached_script_results'):
259 node._cached_script_results = list(node.get_latest_script_results)
260 node._cached_commissioning_script_results = []
261 node._cached_testing_script_results = []
262 for script_result in node._cached_script_results:
263 if (script_result.script_set.result_type ==
264 RESULT_TYPE.INSTALLATION):
265 # Don't include installation results in the health
266 # status.
267 continue
268 elif script_result.status == SCRIPT_STATUS.ABORTED:
269 # LP: #1724235 - Ignore aborted scripts.
270 continue
271 elif (script_result.script_set.result_type ==
272 RESULT_TYPE.COMMISSIONING):
273 node._cached_commissioning_script_results.append(script_result)
274 elif (script_result.script_set.result_type ==
275 RESULT_TYPE.TESTING):
276 node._cached_testing_script_results.append(script_result)
277
278 return node._cached_script_results
279
280
281def get_script_status_name(script_status):
282 for id, name in SCRIPT_STATUS_CHOICES:
283 if id == script_status:
284 return name
285 return 'Unknown'
286
287
249class NodeHandler(OperationsHandler):288class NodeHandler(OperationsHandler):
250 """Manage an individual Node.289 """Manage an individual Node.
251290
@@ -281,6 +320,60 @@ class NodeHandler(OperationsHandler):
281 def current_installation_result_id(handler, node):320 def current_installation_result_id(handler, node):
282 return node.current_installation_script_set_id321 return node.current_installation_script_set_id
283322
323 @classmethod
324 def commissioning_status(handler, node):
325 get_cached_script_results(node)
326 return get_status_from_qs(node._cached_commissioning_script_results)
327
328 @classmethod
329 def commissioning_status_name(handler, node):
330 return get_script_status_name(handler.commissioning_status(node))
331
332 @classmethod
333 def testing_status(handler, node):
334 get_cached_script_results(node)
335 return get_status_from_qs(node._cached_testing_script_results)
336
337 @classmethod
338 def testing_status_name(handler, node):
339 return get_script_status_name(handler.testing_status(node))
340
341 @classmethod
342 def cpu_test_status(handler, node):
343 get_cached_script_results(node)
344 return get_status_from_qs([
345 script_result for script_result
346 in node._cached_testing_script_results
347 if script_result.script.hardware_type == HARDWARE_TYPE.CPU])
348
349 @classmethod
350 def cpu_test_status_name(handler, node):
351 return get_script_status_name(handler.cpu_test_status(node))
352
353 @classmethod
354 def memory_test_status(handler, node):
355 get_cached_script_results(node)
356 return get_status_from_qs([
357 script_result for script_result
358 in node._cached_testing_script_results
359 if script_result.script.hardware_type == HARDWARE_TYPE.MEMORY])
360
361 @classmethod
362 def memory_test_status_name(handler, node):
363 return get_script_status_name(handler.memory_test_status(node))
364
365 @classmethod
366 def storage_test_status(handler, node):
367 get_cached_script_results(node)
368 return get_status_from_qs([
369 script_result for script_result
370 in node._cached_testing_script_results
371 if script_result.script.hardware_type == HARDWARE_TYPE.STORAGE])
372
373 @classmethod
374 def storage_test_status_name(handler, node):
375 return get_script_status_name(handler.storage_test_status(node))
376
284 def read(self, request, system_id):377 def read(self, request, system_id):
285 """Read a specific Node.378 """Read a specific Node.
286379
diff --git a/src/maasserver/api/rackcontrollers.py b/src/maasserver/api/rackcontrollers.py
index 5799f3a..d643f04 100644
--- a/src/maasserver/api/rackcontrollers.py
+++ b/src/maasserver/api/rackcontrollers.py
@@ -58,6 +58,16 @@ DISPLAYED_RACK_CONTROLLER_FIELDS = (
58 'current_testing_result_id',58 'current_testing_result_id',
59 'current_installation_result_id',59 'current_installation_result_id',
60 'version',60 'version',
61 'commissioning_status',
62 'commissioning_status_name',
63 'testing_status',
64 'testing_status_name',
65 'cpu_test_status',
66 'cpu_test_status_name',
67 'memory_test_status',
68 'memory_test_status_name',
69 'storage_test_status',
70 'storage_test_status_name',
61)71)
6272
6373
diff --git a/src/maasserver/api/regioncontrollers.py b/src/maasserver/api/regioncontrollers.py
index 9a2d244..69c7b08 100644
--- a/src/maasserver/api/regioncontrollers.py
+++ b/src/maasserver/api/regioncontrollers.py
@@ -41,6 +41,16 @@ DISPLAYED_REGION_CONTROLLER_FIELDS = (
41 'current_testing_result_id',41 'current_testing_result_id',
42 'current_installation_result_id',42 'current_installation_result_id',
43 'version',43 'version',
44 'commissioning_status',
45 'commissioning_status_name',
46 'testing_status',
47 'testing_status_name',
48 'cpu_test_status',
49 'cpu_test_status_name',
50 'memory_test_status',
51 'memory_test_status_name',
52 'storage_test_status',
53 'storage_test_status_name',
44)54)
4555
4656
diff --git a/src/maasserver/api/tests/test_enlistment.py b/src/maasserver/api/tests/test_enlistment.py
index 471eeb7..16bec8d 100644
--- a/src/maasserver/api/tests/test_enlistment.py
+++ b/src/maasserver/api/tests/test_enlistment.py
@@ -559,6 +559,16 @@ class SimpleUserLoggedInEnlistmentAPITest(APITestCase.ForUser):
559 'current_commissioning_result_id',559 'current_commissioning_result_id',
560 'current_testing_result_id',560 'current_testing_result_id',
561 'current_installation_result_id',561 'current_installation_result_id',
562 'commissioning_status',
563 'commissioning_status_name',
564 'testing_status',
565 'testing_status_name',
566 'cpu_test_status',
567 'cpu_test_status_name',
568 'memory_test_status',
569 'memory_test_status_name',
570 'storage_test_status',
571 'storage_test_status_name',
562 ],572 ],
563 list(parsed_result))573 list(parsed_result))
564574
@@ -736,6 +746,16 @@ class AdminLoggedInEnlistmentAPITest(APITestCase.ForAdmin):
736 'current_commissioning_result_id',746 'current_commissioning_result_id',
737 'current_testing_result_id',747 'current_testing_result_id',
738 'current_installation_result_id',748 'current_installation_result_id',
749 'commissioning_status',
750 'commissioning_status_name',
751 'testing_status',
752 'testing_status_name',
753 'cpu_test_status',
754 'cpu_test_status_name',
755 'memory_test_status',
756 'memory_test_status_name',
757 'storage_test_status',
758 'storage_test_status_name',
739 ],759 ],
740 list(parsed_result))760 list(parsed_result))
741761
diff --git a/src/maasserver/api/tests/test_machines.py b/src/maasserver/api/tests/test_machines.py
index c8199cd..0ebb9e1 100644
--- a/src/maasserver/api/tests/test_machines.py
+++ b/src/maasserver/api/tests/test_machines.py
@@ -342,12 +342,12 @@ class TestMachinesAPI(APITestCase.ForUser):
342 len(extract_system_ids(parsed_result_2)),342 len(extract_system_ids(parsed_result_2)),
343 ])343 ])
344344
345 # Because of fields `status_action`, `status_message`, and345 # Because of fields `status_action`, `status_message`,
346 # `default_gateways`. The number of queries is not the same but it is346 # `default_gateways`, and `health_status` the number of queries is not
347 # proportional to the number of machines.347 # the same but it is proportional to the number of machines.
348 DEFAULT_NUM = 57348 DEFAULT_NUM = 57
349 self.assertEqual(DEFAULT_NUM + (10 * 3), num_queries1)349 self.assertEqual(DEFAULT_NUM + (10 * 4), num_queries1)
350 self.assertEqual(DEFAULT_NUM + (20 * 3), num_queries2)350 self.assertEqual(DEFAULT_NUM + (20 * 4), num_queries2)
351351
352 def test_GET_without_machines_returns_empty_list(self):352 def test_GET_without_machines_returns_empty_list(self):
353 # If there are no machines to list, the "read" op still works but353 # If there are no machines to list, the "read" op still works but
diff --git a/src/maasserver/api/tests/test_node.py b/src/maasserver/api/tests/test_node.py
index 706a3a0..4fecb15 100644
--- a/src/maasserver/api/tests/test_node.py
+++ b/src/maasserver/api/tests/test_node.py
@@ -1,10 +1,11 @@
1# Copyright 2013-2016 Canonical Ltd. This software is licensed under the1# Copyright 2013-2017 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for the Node API."""4"""Tests for the Node API."""
55
6__all__ = []6__all__ = []
77
8from functools import partial
8import http.client9import http.client
9import random10import random
10from unittest.mock import (11from unittest.mock import (
@@ -40,11 +41,14 @@ from maastesting.matchers import (
40 MockNotCalled,41 MockNotCalled,
41)42)
42from metadataserver.enum import (43from metadataserver.enum import (
44 HARDWARE_TYPE,
43 RESULT_TYPE,45 RESULT_TYPE,
44 SCRIPT_STATUS,46 SCRIPT_STATUS,
47 SCRIPT_STATUS_CHOICES,
45 SCRIPT_TYPE,48 SCRIPT_TYPE,
46)49)
47from metadataserver.models import NodeKey50from metadataserver.models import NodeKey
51from metadataserver.models.scriptset import get_status_from_qs
48from metadataserver.nodeinituser import get_node_init_user52from metadataserver.nodeinituser import get_node_init_user
49from provisioningserver.refresh.node_info_scripts import (53from provisioningserver.refresh.node_info_scripts import (
50 LLDP_OUTPUT_NAME,54 LLDP_OUTPUT_NAME,
@@ -184,6 +188,70 @@ class TestNodeAPI(APITestCase.ForUser):
184 args=[parsed_result['system_id']]),188 args=[parsed_result['system_id']]),
185 parsed_result['resource_uri'])189 parsed_result['resource_uri'])
186190
191 def test_health_status(self):
192 self.become_admin()
193 machine = factory.make_Machine(owner=self.user)
194 commissioning_script_set = factory.make_ScriptSet(
195 result_type=RESULT_TYPE.COMMISSIONING, node=machine)
196 testing_script_set = factory.make_ScriptSet(
197 result_type=RESULT_TYPE.TESTING, node=machine)
198 make_script_result = partial(
199 factory.make_ScriptResult, script_set=testing_script_set,
200 status=factory.pick_choice(
201 SCRIPT_STATUS_CHOICES, but_not=[SCRIPT_STATUS.ABORTED]))
202 commissioning_script_result = make_script_result(
203 script_set=commissioning_script_set, script=factory.make_Script(
204 script_type=SCRIPT_TYPE.COMMISSIONING))
205 cpu_script_result = make_script_result(
206 script=factory.make_Script(
207 script_type=SCRIPT_TYPE.TESTING,
208 hardware_type=HARDWARE_TYPE.CPU))
209 memory_script_result = make_script_result(
210 script=factory.make_Script(
211 script_type=SCRIPT_TYPE.TESTING,
212 hardware_type=HARDWARE_TYPE.MEMORY))
213 storage_script_result = make_script_result(
214 script=factory.make_Script(
215 script_type=SCRIPT_TYPE.TESTING,
216 hardware_type=HARDWARE_TYPE.STORAGE))
217 testing_script_results = (
218 machine.get_latest_testing_script_results.exclude(
219 status=SCRIPT_STATUS.ABORTED))
220 testing_status = get_status_from_qs(testing_script_results)
221
222 response = self.client.get(self.get_node_uri(machine))
223 parsed_result = json_load_bytes(response.content)
224
225 status = lambda s: get_status_from_qs([s])
226 status_name = lambda s: SCRIPT_STATUS_CHOICES[status(s)][1]
227 self.assertThat(response, HasStatusCode(http.client.OK))
228 self.assertEquals(
229 status(commissioning_script_result),
230 parsed_result['commissioning_status'])
231 self.assertEquals(
232 status_name(commissioning_script_result),
233 parsed_result['commissioning_status_name'])
234 self.assertEquals(testing_status, parsed_result['testing_status'])
235 self.assertEquals(
236 SCRIPT_STATUS_CHOICES[testing_status][1],
237 parsed_result['testing_status_name'])
238 self.assertEquals(
239 status(cpu_script_result), parsed_result['cpu_test_status'])
240 self.assertEquals(
241 status_name(cpu_script_result),
242 parsed_result['cpu_test_status_name'])
243 self.assertEquals(
244 status(memory_script_result), parsed_result['memory_test_status'])
245 self.assertEquals(
246 status_name(memory_script_result),
247 parsed_result['memory_test_status_name'])
248 self.assertEquals(
249 status(storage_script_result),
250 parsed_result['storage_test_status'])
251 self.assertEquals(
252 status_name(storage_script_result),
253 parsed_result['storage_test_status_name'])
254
187 def test_DELETE_deletes_node(self):255 def test_DELETE_deletes_node(self):
188 # The api allows to delete a Node.256 # The api allows to delete a Node.
189 self.become_admin()257 self.become_admin()
diff --git a/src/maasserver/api/tests/test_rackcontroller.py b/src/maasserver/api/tests/test_rackcontroller.py
index a631d6c..5105c6d 100644
--- a/src/maasserver/api/tests/test_rackcontroller.py
+++ b/src/maasserver/api/tests/test_rackcontroller.py
@@ -135,6 +135,16 @@ class TestRackControllersAPI(APITestCase.ForUser):
135 'current_testing_result_id',135 'current_testing_result_id',
136 'current_installation_result_id',136 'current_installation_result_id',
137 'version',137 'version',
138 'commissioning_status',
139 'commissioning_status_name',
140 'testing_status',
141 'testing_status_name',
142 'cpu_test_status',
143 'cpu_test_status_name',
144 'memory_test_status',
145 'memory_test_status_name',
146 'storage_test_status',
147 'storage_test_status_name',
138 ],148 ],
139 list(parsed_result[0]))149 list(parsed_result[0]))
140150
diff --git a/src/maasserver/api/tests/test_regioncontroller.py b/src/maasserver/api/tests/test_regioncontroller.py
index fd02644..3854e6f 100644
--- a/src/maasserver/api/tests/test_regioncontroller.py
+++ b/src/maasserver/api/tests/test_regioncontroller.py
@@ -83,5 +83,15 @@ class TestRegionControllersAPI(APITestCase.ForUser):
83 'current_testing_result_id',83 'current_testing_result_id',
84 'current_installation_result_id',84 'current_installation_result_id',
85 'version',85 'version',
86 'commissioning_status',
87 'commissioning_status_name',
88 'testing_status',
89 'testing_status_name',
90 'cpu_test_status',
91 'cpu_test_status_name',
92 'memory_test_status',
93 'memory_test_status_name',
94 'storage_test_status',
95 'storage_test_status_name',
86 ],96 ],
87 list(parsed_result[0]))97 list(parsed_result[0]))
diff --git a/src/maasserver/api/tests/test_tag.py b/src/maasserver/api/tests/test_tag.py
index ef0d546..4397dd3 100644
--- a/src/maasserver/api/tests/test_tag.py
+++ b/src/maasserver/api/tests/test_tag.py
@@ -197,10 +197,10 @@ class TestTagAPI(APITestCase.ForUser):
197 len(extract_system_ids(parsed_result_2)),197 len(extract_system_ids(parsed_result_2)),
198 ])198 ])
199199
200 # Because of fields `status_action`, `status_message`, and200 # Because of fields `status_action`, `status_message`,
201 # `default_gateways`. The number of queries is not the same but it is201 # `default_gateways`, and `health_status` the number of queries is not
202 # proportional to the number of machines.202 # the same but it is proportional to the number of machines.
203 self.assertEquals(num_queries1, num_queries2 - (3 * 3))203 self.assertEquals(num_queries1, num_queries2 - (3 * 4))
204204
205 def test_GET_machines_returns_machines(self):205 def test_GET_machines_returns_machines(self):
206 tag = factory.make_Tag()206 tag = factory.make_Tag()
@@ -257,10 +257,10 @@ class TestTagAPI(APITestCase.ForUser):
257 len(extract_system_ids(parsed_result_2)),257 len(extract_system_ids(parsed_result_2)),
258 ])258 ])
259259
260 # Because of fields `status_action`, `status_message`, and260 # Because of fields `status_action`, `status_message`,
261 # `default_gateways`. The number of queries is not the same but it is261 # `default_gateways`, and `health_status` the number of queries is not
262 # proportional to the number of machines.262 # the same but it is proportional to the number of machines.
263 self.assertEquals(num_queries1, num_queries2 - (3 * 3))263 self.assertEquals(num_queries1, num_queries2 - (3 * 4))
264264
265 def test_GET_devices_returns_devices(self):265 def test_GET_devices_returns_devices(self):
266 tag = factory.make_Tag()266 tag = factory.make_Tag()
@@ -375,7 +375,7 @@ class TestTagAPI(APITestCase.ForUser):
375 len(extract_system_ids(parsed_result_1)),375 len(extract_system_ids(parsed_result_1)),
376 len(extract_system_ids(parsed_result_2)),376 len(extract_system_ids(parsed_result_2)),
377 ])377 ])
378 self.assertEquals(num_queries1, num_queries2 - (3 * 2))378 self.assertEquals(num_queries1, num_queries2 - (3 * 3))
379379
380 def test_GET_rack_controllers_returns_no_rack_controllers_nonadmin(self):380 def test_GET_rack_controllers_returns_no_rack_controllers_nonadmin(self):
381 tag = factory.make_Tag()381 tag = factory.make_Tag()
@@ -456,7 +456,7 @@ class TestTagAPI(APITestCase.ForUser):
456 len(extract_system_ids(parsed_result_1)),456 len(extract_system_ids(parsed_result_1)),
457 len(extract_system_ids(parsed_result_2)),457 len(extract_system_ids(parsed_result_2)),
458 ])458 ])
459 self.assertEquals(num_queries1, num_queries2 - 3)459 self.assertEquals(num_queries1, num_queries2 - 6)
460460
461 def test_GET_region_controllers_returns_no_controllers_nonadmin(self):461 def test_GET_region_controllers_returns_no_controllers_nonadmin(self):
462 tag = factory.make_Tag()462 tag = factory.make_Tag()
diff --git a/src/maasserver/static/js/angular/controllers/node_details.js b/src/maasserver/static/js/angular/controllers/node_details.js
index 503d30a..ac96431 100644
--- a/src/maasserver/static/js/angular/controllers/node_details.js
+++ b/src/maasserver/static/js/angular/controllers/node_details.js
@@ -1000,7 +1000,7 @@ angular.module('MAAS').controller('NodeDetailsController', [
1000 var results = $scope.node.installation_results;1000 var results = $scope.node.installation_results;
1001 if(!angular.isArray(results) ||1001 if(!angular.isArray(results) ||
1002 results.length === 0 || results[0].output === "") {1002 results.length === 0 || results[0].output === "") {
1003 switch($scope.node.installation_script_set_status) {1003 switch($scope.node.installation_status) {
1004 case 0:1004 case 0:
1005 return "System is booting...";1005 return "System is booting...";
1006 case 1:1006 case 1:
diff --git a/src/maasserver/static/js/angular/controllers/tests/test_node_details.js b/src/maasserver/static/js/angular/controllers/tests/test_node_details.js
index ceeaef8..f670338 100644
--- a/src/maasserver/static/js/angular/controllers/tests/test_node_details.js
+++ b/src/maasserver/static/js/angular/controllers/tests/test_node_details.js
@@ -2197,7 +2197,7 @@ describe("NodeDetailsController", function() {
2197 it("returns status message when no output and status", function() {2197 it("returns status message when no output and status", function() {
2198 var controller = makeController();2198 var controller = makeController();
2199 $scope.node = makeNode();2199 $scope.node = makeNode();
2200 $scope.node.installation_script_set_status = makeInteger(0, 5);2200 $scope.node.installation_status = makeInteger(0, 5);
2201 expect($scope.getInstallationData()).not.toBe("");2201 expect($scope.getInstallationData()).not.toBe("");
2202 });2202 });
2203 });2203 });
diff --git a/src/maasserver/static/partials/machines-table.html b/src/maasserver/static/partials/machines-table.html
index e47b10d..24fb55a 100644
--- a/src/maasserver/static/partials/machines-table.html
+++ b/src/maasserver/static/partials/machines-table.html
@@ -57,8 +57,14 @@
57 <td class="powerstate u-upper-case--first" aria-label="Power state">57 <td class="powerstate u-upper-case--first" aria-label="Power state">
58 <span title="{$ node.power_state $}" data-ng-if="node.power_state != 'unknown'" class="icon icon--power-{$ node.power_state $} u-margin--right-tiny"></span> {$ node.power_state $}58 <span title="{$ node.power_state $}" data-ng-if="node.power_state != 'unknown'" class="icon icon--power-{$ node.power_state $} u-margin--right-tiny"></span> {$ node.power_state $}
59 </td>59 </td>
60 <td class="table-col--3 u-display--desktop"><i data-ng-if="showSpinner(node)" class="icon icon--loading u-animation--spin u-display--desktop"></i></td>60 <td class="table-col--3 u-display--desktop">
61 <td class="status u-text--truncate" aria-label="{$ node.status_tooltip $}">{$ getStatusText(node) $} <span class="u-display--mobile"><i data-ng-if="showSpinner(node)" class="icon icon--loading u-animation--spin u-margin--left-tiny"></i></span></td>61 <i data-ng-if="showSpinner(node)" class="icon icon--loading u-animation--spin u-display--desktop"></i>
62 <i data-ng-if="!showSpinner(node) && node.testing_status !== 2" data-maas-script-status="script-status" data-script-status="node.testing_status" aria-label="{$ node.testing_status_tooltip $}"></i>
63 </td>
64 <td class="status u-text--truncate" aria-label="{$ node.status_tooltip $}">{$ getStatusText(node) $} <span class="u-display--mobile">
65 <i data-ng-if="showSpinner(node)" class="icon icon--loading u-animation--spin u-margin--left-tiny"></i>
66 <i data-ng-if="!showSpinner(node) && node.testing_status !== 2" data-maas-script-status="script-status" data-script-status="node.testing_status" aria-label="{$ node.testing_status_tooltip $}"></i>
67 </span></td>
62 <td class="table-col--10" aria-label="Owner">{$ node.owner $}</td>68 <td class="table-col--10" aria-label="Owner">{$ node.owner $}</td>
63 <td class="u-align--right u-display--desktop" aria-label="CPU">69 <td class="u-align--right u-display--desktop" aria-label="CPU">
64 <span class="tooltip" data-maas-script-status="script-status" data-script-status="node.cpu_test_status" aria-label="{$ node.cpu_test_status_tooltip $}" data-ng-if="node.cpu_test_status !== 2"></span>70 <span class="tooltip" data-maas-script-status="script-status" data-script-status="node.cpu_test_status" aria-label="{$ node.cpu_test_status_tooltip $}" data-ng-if="node.cpu_test_status !== 2"></span>
diff --git a/src/maasserver/static/partials/node-details.html b/src/maasserver/static/partials/node-details.html
index f8d8087..324f3d1 100755
--- a/src/maasserver/static/partials/node-details.html
+++ b/src/maasserver/static/partials/node-details.html
@@ -206,13 +206,13 @@
206 data-ng-click="section.area = 'storage'">Storage</button>206 data-ng-click="section.area = 'storage'">Storage</button>
207 <button role="tab" class="page-navigation__link" data-ng-if="node.commissioning_script_count > 0"207 <button role="tab" class="page-navigation__link" data-ng-if="node.commissioning_script_count > 0"
208 data-ng-class="{ 'is-active': section.area === 'commissioning'}"208 data-ng-class="{ 'is-active': section.area === 'commissioning'}"
209 data-ng-click="section.area = 'commissioning'"><span data-maas-script-status="script-status" data-script-status="node.commissioning_script_set_status"></span> Commissioning</button>209 data-ng-click="section.area = 'commissioning'"><span data-maas-script-status="script-status" data-script-status="node.commissioning_status"></span> Commissioning</button>
210 <button role="tab" class="page-navigation__link" data-ng-if="node.testing_script_count > 0"210 <button role="tab" class="page-navigation__link" data-ng-if="node.testing_script_count > 0"
211 data-ng-class="{ 'is-active': section.area === 'testing'}"211 data-ng-class="{ 'is-active': section.area === 'testing'}"
212 data-ng-click="section.area = 'testing'"><span data-maas-script-status="script-status" data-script-status="node.testing_script_set_status"></span> Hardware tests</button>212 data-ng-click="section.area = 'testing'"><span data-maas-script-status="script-status" data-script-status="node.testing_status"></span> Hardware tests</button>
213 <button role="tab" class="page-navigation__link" data-ng-if="machine_output.viewable"213 <button role="tab" class="page-navigation__link" data-ng-if="machine_output.viewable"
214 data-ng-class="{ 'is-active': section.area === 'logs'}"214 data-ng-class="{ 'is-active': section.area === 'logs'}"
215 data-ng-click="section.area = 'logs'"><span data-maas-script-status="script-status" data-script-status="node.installation_script_set_status" data-ng-if="node.installation_results.length > 0"></span> Logs</button>215 data-ng-click="section.area = 'logs'"><span data-maas-script-status="script-status" data-script-status="node.installation_status" data-ng-if="node.installation_results.length > 0"></span> Logs</button>
216 <button role="tab" class="page-navigation__link" data-ng-if="!isDevice"216 <button role="tab" class="page-navigation__link" data-ng-if="!isDevice"
217 data-ng-class="{ 'is-active': section.area === 'events'}"217 data-ng-class="{ 'is-active': section.area === 'events'}"
218 data-ng-click="section.area = 'events'">Events</button>218 data-ng-click="section.area = 'events'">Events</button>
diff --git a/src/maasserver/websockets/handlers/machine.py b/src/maasserver/websockets/handlers/machine.py
index ac101fc..da51686 100644
--- a/src/maasserver/websockets/handlers/machine.py
+++ b/src/maasserver/websockets/handlers/machine.py
@@ -79,11 +79,7 @@ from maasserver.websockets.handlers.node import (
79 node_prefetch,79 node_prefetch,
80 NodeHandler,80 NodeHandler,
81)81)
82from metadataserver.enum import (82from metadataserver.enum import HARDWARE_TYPE
83 HARDWARE_TYPE,
84 SCRIPT_STATUS,
85 SCRIPT_STATUS_CHOICES,
86)
87from metadataserver.models import ScriptResult83from metadataserver.models import ScriptResult
88from metadataserver.models.scriptset import get_status_from_qs84from metadataserver.models.scriptset import get_status_from_qs
89from provisioningserver.logger import LegacyLogger85from provisioningserver.logger import LegacyLogger
@@ -179,44 +175,6 @@ class MachineHandler(NodeHandler):
179 return Machine.objects.get_nodes(175 return Machine.objects.get_nodes(
180 self.user, NODE_PERMISSION.VIEW, from_nodes=self._meta.queryset)176 self.user, NODE_PERMISSION.VIEW, from_nodes=self._meta.queryset)
181177
182 def dehydrate_hardware_status_tooltip(self, script_results):
183 script_statuses = {}
184 for script_result in script_results:
185 if script_result.status in script_statuses:
186 script_statuses[script_result.status].add(script_result.name)
187 else:
188 script_statuses[script_result.status] = {script_result.name}
189
190 tooltip = ''
191 for status, scripts in script_statuses.items():
192 len_scripts = len(scripts)
193 if status in {
194 SCRIPT_STATUS.PENDING, SCRIPT_STATUS.RUNNING,
195 SCRIPT_STATUS.INSTALLING}:
196 verb = 'is' if len_scripts == 1 else 'are'
197 elif status in {
198 SCRIPT_STATUS.PASSED, SCRIPT_STATUS.FAILED,
199 SCRIPT_STATUS.TIMEDOUT, SCRIPT_STATUS.FAILED_INSTALLING}:
200 verb = 'has' if len_scripts == 1 else 'have'
201 else:
202 # Covers SCRIPT_STATUS.ABORTED, an else is used incase new
203 # statuses are ever added.
204 verb = 'was' if len_scripts == 1 else 'were'
205
206 if tooltip != '':
207 tooltip += ' '
208 if len_scripts == 1:
209 tooltip += '1 test '
210 else:
211 tooltip += '%s tests ' % len_scripts
212 tooltip += '%s %s.' % (
213 verb, SCRIPT_STATUS_CHOICES[status][1].lower())
214
215 if tooltip == '':
216 tooltip = 'No tests have been run.'
217
218 return tooltip
219
220 def list(self, params):178 def list(self, params):
221 """List objects.179 """List objects.
222180
diff --git a/src/maasserver/websockets/handlers/node.py b/src/maasserver/websockets/handlers/node.py
index 8266b2a..81910f0 100644
--- a/src/maasserver/websockets/handlers/node.py
+++ b/src/maasserver/websockets/handlers/node.py
@@ -45,8 +45,10 @@ from maasserver.websockets.handlers.timestampedmodel import (
45 TimestampedModelHandler,45 TimestampedModelHandler,
46)46)
47from metadataserver.enum import (47from metadataserver.enum import (
48 HARDWARE_TYPE,
48 RESULT_TYPE,49 RESULT_TYPE,
49 SCRIPT_STATUS,50 SCRIPT_STATUS,
51 SCRIPT_STATUS_CHOICES,
50)52)
51from metadataserver.models.scriptset import get_status_from_qs53from metadataserver.models.scriptset import get_status_from_qs
52from provisioningserver.tags import merge_details_cleanly54from provisioningserver.tags import merge_details_cleanly
@@ -118,6 +120,44 @@ class NodeHandler(TimestampedModelHandler):
118 """Return power_parameters None if empty."""120 """Return power_parameters None if empty."""
119 return None if power_parameters == '' else power_parameters121 return None if power_parameters == '' else power_parameters
120122
123 def dehydrate_hardware_status_tooltip(self, script_results):
124 script_statuses = {}
125 for script_result in script_results:
126 if script_result.status in script_statuses:
127 script_statuses[script_result.status].add(script_result.name)
128 else:
129 script_statuses[script_result.status] = {script_result.name}
130
131 tooltip = ''
132 for status, scripts in script_statuses.items():
133 len_scripts = len(scripts)
134 if status in {
135 SCRIPT_STATUS.PENDING, SCRIPT_STATUS.RUNNING,
136 SCRIPT_STATUS.INSTALLING}:
137 verb = 'is' if len_scripts == 1 else 'are'
138 elif status in {
139 SCRIPT_STATUS.PASSED, SCRIPT_STATUS.FAILED,
140 SCRIPT_STATUS.TIMEDOUT, SCRIPT_STATUS.FAILED_INSTALLING}:
141 verb = 'has' if len_scripts == 1 else 'have'
142 else:
143 # Covers SCRIPT_STATUS.ABORTED, an else is used incase new
144 # statuses are ever added.
145 verb = 'was' if len_scripts == 1 else 'were'
146
147 if tooltip != '':
148 tooltip += ' '
149 if len_scripts == 1:
150 tooltip += '1 test '
151 else:
152 tooltip += '%s tests ' % len_scripts
153 tooltip += '%s %s.' % (
154 verb, SCRIPT_STATUS_CHOICES[status][1].lower())
155
156 if tooltip == '':
157 tooltip = 'No tests have been run.'
158
159 return tooltip
160
121 def dehydrate(self, obj, data, for_list=False):161 def dehydrate(self, obj, data, for_list=False):
122 """Add extra fields to `data`."""162 """Add extra fields to `data`."""
123 data["fqdn"] = obj.fqdn163 data["fqdn"] = obj.fqdn
@@ -174,6 +214,40 @@ class NodeHandler(TimestampedModelHandler):
174 data["distro_series"] = obj.get_distro_series(214 data["distro_series"] = obj.get_distro_series(
175 default=self.default_distro_series)215 default=self.default_distro_series)
176 data["dhcp_on"] = self.get_providing_dhcp(obj)216 data["dhcp_on"] = self.get_providing_dhcp(obj)
217
218 if obj.node_type != NODE_TYPE.DEVICE:
219 commissioning_script_results = []
220 testing_script_results = []
221 for hw_type in self._script_results.get(obj.id, {}).values():
222 for script_result in hw_type:
223 if (script_result.script_set.result_type ==
224 RESULT_TYPE.INSTALLATION):
225 # Don't include installation results in the health
226 # status.
227 continue
228 elif script_result.status == SCRIPT_STATUS.ABORTED:
229 # LP: #1724235 - Ignore aborted scripts.
230 continue
231 elif (script_result.script_set.result_type ==
232 RESULT_TYPE.COMMISSIONING):
233 commissioning_script_results.append(script_result)
234 elif (script_result.script_set.result_type ==
235 RESULT_TYPE.TESTING):
236 testing_script_results.append(script_result)
237
238 data["commissioning_script_count"] = len(
239 commissioning_script_results)
240 data["commissioning_status"] = get_status_from_qs(
241 commissioning_script_results)
242 data["commissioning_status_tooltip"] = (
243 self.dehydrate_hardware_status_tooltip(
244 commissioning_script_results).replace(
245 'test', 'commissioning script'))
246 data["testing_script_count"] = len(testing_script_results)
247 data["testing_status"] = get_status_from_qs(testing_script_results)
248 data["testing_status_tooltip"] = (
249 self.dehydrate_hardware_status_tooltip(testing_script_results))
250
177 if not for_list:251 if not for_list:
178 data["on_network"] = obj.on_network()252 data["on_network"] = obj.on_network()
179 if obj.node_type != NODE_TYPE.DEVICE:253 if obj.node_type != NODE_TYPE.DEVICE:
@@ -216,23 +290,12 @@ class NodeHandler(TimestampedModelHandler):
216 # Events290 # Events
217 data["events"] = self.dehydrate_events(obj)291 data["events"] = self.dehydrate_events(obj)
218292
219 # Machine output293 # Machine logs
220 data = self.dehydrate_summary_output(obj, data)294 data = self.dehydrate_summary_output(obj, data)
221 data["commissioning_script_count"] = (295 data["installation_status"] = self.dehydrate_script_set_status(
222 obj.get_latest_commissioning_script_results.count())296 obj.current_installation_script_set)
223 data["commissioning_script_set_status"] = get_status_from_qs(
224 obj.get_latest_commissioning_script_results.exclude(
225 status=SCRIPT_STATUS.ABORTED))
226 data["testing_script_count"] = (
227 obj.get_latest_testing_script_results.count())
228 data["testing_script_set_status"] = get_status_from_qs(
229 obj.get_latest_testing_script_results.exclude(
230 status=SCRIPT_STATUS.ABORTED))
231 data["installation_results"] = self.dehydrate_script_set(297 data["installation_results"] = self.dehydrate_script_set(
232 obj.current_installation_script_set)298 obj.current_installation_script_set)
233 data["installation_script_set_status"] = (
234 self.dehydrate_script_set_status(
235 obj.current_installation_script_set))
236299
237 # Third party drivers300 # Third party drivers
238 if Config.objects.get_config('enable_third_party_drivers'):301 if Config.objects.get_config('enable_third_party_drivers'):
@@ -255,11 +318,11 @@ class NodeHandler(TimestampedModelHandler):
255 qs = qs.exclude(status=SCRIPT_STATUS.ABORTED)318 qs = qs.exclude(status=SCRIPT_STATUS.ABORTED)
256 cleared_node_ids = []319 cleared_node_ids = []
257 for script_result in qs:320 for script_result in qs:
258 # Builtin commissioning scripts are not stored in the database.
259 if script_result.script is None:
260 continue
261 node_id = script_result.script_set.node_id321 node_id = script_result.script_set.node_id
262 hardware_type = script_result.script.hardware_type322 if script_result.script is not None:
323 hardware_type = script_result.script.hardware_type
324 else:
325 hardware_type = HARDWARE_TYPE.NODE
263326
264 if node_id not in cleared_node_ids:327 if node_id not in cleared_node_ids:
265 self._script_results[node_id] = {}328 self._script_results[node_id] = {}
diff --git a/src/maasserver/websockets/handlers/tests/test_device.py b/src/maasserver/websockets/handlers/tests/test_device.py
index b6a879b..b0b4490 100644
--- a/src/maasserver/websockets/handlers/tests/test_device.py
+++ b/src/maasserver/websockets/handlers/tests/test_device.py
@@ -186,18 +186,19 @@ class TestDeviceHandler(MAASTransactionServerTestCase):
186 if for_list:186 if for_list:
187 allowed_fields = DeviceHandler.Meta.list_fields + [187 allowed_fields = DeviceHandler.Meta.list_fields + [
188 "actions",188 "actions",
189 "fqdn",
190 "extra_macs",189 "extra_macs",
191 "metadata",190 "fabrics",
192 "tags",191 "fqdn",
193 "primary_mac",192 "installation_status",
194 "ip_address",193 "ip_address",
195 "ip_assignment",194 "ip_assignment",
196 "node_type_display",
197 "link_type",195 "link_type",
198 "subnets",196 "metadata",
197 "node_type_display",
198 "primary_mac",
199 "spaces",199 "spaces",
200 "fabrics",200 "subnets",
201 "tags",
201 ]202 ]
202 for key in list(data):203 for key in list(data):
203 if key not in allowed_fields:204 if key not in allowed_fields:
diff --git a/src/maasserver/websockets/handlers/tests/test_machine.py b/src/maasserver/websockets/handlers/tests/test_machine.py
index 0dd01f2..15ab758 100644
--- a/src/maasserver/websockets/handlers/tests/test_machine.py
+++ b/src/maasserver/websockets/handlers/tests/test_machine.py
@@ -166,30 +166,36 @@ class TestMachineHandler(MAASServerTestCase):
166 ]166 ]
167 disks = sorted(disks, key=itemgetter("name"))167 disks = sorted(disks, key=itemgetter("name"))
168 subnets = handler.get_all_subnets(node)168 subnets = handler.get_all_subnets(node)
169 commissioning_scripts = node.get_latest_commissioning_script_results
170 commissioning_scripts = commissioning_scripts.exclude(
171 status=SCRIPT_STATUS.ABORTED)
172 testing_scripts = node.get_latest_testing_script_results
173 testing_scripts = testing_scripts.exclude(
174 status=SCRIPT_STATUS.ABORTED)
169 data = {175 data = {
170 "actions": list(compile_node_actions(node, handler.user).keys()),176 "actions": list(compile_node_actions(node, handler.user).keys()),
171 "architecture": node.architecture,177 "architecture": node.architecture,
172 "bmc": node.bmc_id,178 "bmc": node.bmc_id,
173 "boot_disk": node.boot_disk,179 "boot_disk": node.boot_disk,
174 "bios_boot_method": node.bios_boot_method,180 "bios_boot_method": node.bios_boot_method,
175 "commissioning_script_count": (181 "commissioning_script_count": commissioning_scripts.count(),
176 node.get_latest_commissioning_script_results.count()),182 "commissioning_status": get_status_from_qs(commissioning_scripts),
177 "commissioning_script_set_status": get_status_from_qs(183 "commissioning_status_tooltip": (
178 node.get_latest_commissioning_script_results.exclude(184 handler.dehydrate_hardware_status_tooltip(
179 status=SCRIPT_STATUS.ABORTED)),185 commissioning_scripts).replace(
186 'test', 'commissioning script')),
180 "current_commissioning_script_set": (187 "current_commissioning_script_set": (
181 node.current_commissioning_script_set_id),188 node.current_commissioning_script_set_id),
182 "testing_script_count": (189 "testing_script_count": testing_scripts.count(),
183 node.get_latest_testing_script_results.count()),190 "testing_status": get_status_from_qs(testing_scripts),
184 "testing_script_set_status": get_status_from_qs(191 "testing_status_tooltip": (
185 node.get_latest_testing_script_results.exclude(192 handler.dehydrate_hardware_status_tooltip(testing_scripts)),
186 status=SCRIPT_STATUS.ABORTED)),
187 "current_testing_script_set": node.current_testing_script_set_id,193 "current_testing_script_set": node.current_testing_script_set_id,
188 "installation_results": handler.dehydrate_script_set(194 "installation_results": handler.dehydrate_script_set(
189 node.current_installation_script_set),195 node.current_installation_script_set),
190 "current_installation_script_set": (196 "current_installation_script_set": (
191 node.current_installation_script_set_id),197 node.current_installation_script_set_id),
192 "installation_script_set_status": (198 "installation_status": (
193 handler.dehydrate_script_set_status(199 handler.dehydrate_script_set_status(
194 node.current_installation_script_set)),200 node.current_installation_script_set)),
195 "cpu_count": node.cpu_count,201 "cpu_count": node.cpu_count,
@@ -286,26 +292,32 @@ class TestMachineHandler(MAASServerTestCase):
286 allowed_fields = MachineHandler.Meta.list_fields + [292 allowed_fields = MachineHandler.Meta.list_fields + [
287 "actions",293 "actions",
288 "architecture",294 "architecture",
295 "commissioning_script_count",
296 "commissioning_status",
297 "commissioning_status_tooltip",
298 "dhcp_on",
299 "distro_series",
300 "extra_macs",
301 "fabrics",
289 "fqdn",302 "fqdn",
303 "link_type",
290 "metadata",304 "metadata",
291 "status",305 "node_type_display",
292 "status_code",306 "osystem",
307 "physical_disk_count",
308 "pod",
293 "pxe_mac",309 "pxe_mac",
294 "pxe_mac_vendor",310 "pxe_mac_vendor",
295 "extra_macs",
296 "tags",
297 "subnets",
298 "fabrics",
299 "spaces",311 "spaces",
300 "physical_disk_count",312 "status",
313 "status_code",
301 "storage",314 "storage",
302 "storage_tags",315 "storage_tags",
303 "node_type_display",316 "subnets",
304 "osystem",317 "tags",
305 "distro_series",318 "testing_script_count",
306 "dhcp_on",319 "testing_status",
307 "pod",320 "testing_status_tooltip",
308 "link_type",
309 ]321 ]
310 for key in list(data):322 for key in list(data):
311 if key not in allowed_fields:323 if key not in allowed_fields:

Subscribers

People subscribed via source and target branches