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
1diff --git a/src/maasserver/api/machines.py b/src/maasserver/api/machines.py
2index d68687a..4218bdc 100644
3--- a/src/maasserver/api/machines.py
4+++ b/src/maasserver/api/machines.py
5@@ -146,6 +146,16 @@ DISPLAYED_MACHINE_FIELDS = (
6 'current_commissioning_result_id',
7 'current_testing_result_id',
8 'current_installation_result_id',
9+ 'commissioning_status',
10+ 'commissioning_status_name',
11+ 'testing_status',
12+ 'testing_status_name',
13+ 'cpu_test_status',
14+ 'cpu_test_status_name',
15+ 'memory_test_status',
16+ 'memory_test_status_name',
17+ 'storage_test_status',
18+ 'storage_test_status_name',
19 )
20
21 # Limited set of machine fields exposed on the anonymous API.
22diff --git a/src/maasserver/api/nodes.py b/src/maasserver/api/nodes.py
23index 8743375..13d904b 100644
24--- a/src/maasserver/api/nodes.py
25+++ b/src/maasserver/api/nodes.py
26@@ -1,4 +1,4 @@
27-# Copyright 2012-2016 Canonical Ltd. This software is licensed under the
28+# Copyright 2012-2017 Canonical Ltd. This software is licensed under the
29 # GNU Affero General Public License version 3 (see the file LICENSE).
30
31 __all__ = [
32@@ -56,6 +56,13 @@ from maasserver.models import (
33 )
34 from maasserver.models.nodeprobeddetails import get_single_probed_details
35 from maasserver.utils.orm import prefetch_queryset
36+from metadataserver.enum import (
37+ HARDWARE_TYPE,
38+ RESULT_TYPE,
39+ SCRIPT_STATUS,
40+ SCRIPT_STATUS_CHOICES,
41+)
42+from metadataserver.models.scriptset import get_status_from_qs
43 from piston3.utils import rc
44 from provisioningserver.drivers.power import UNKNOWN_POWER_TYPE
45
46@@ -246,6 +253,38 @@ def is_registered(request):
47 return interfaces.exists()
48
49
50+def get_cached_script_results(node):
51+ """Load script results into cache and return the cached list."""
52+ if not hasattr(node, '_cached_script_results'):
53+ node._cached_script_results = list(node.get_latest_script_results)
54+ node._cached_commissioning_script_results = []
55+ node._cached_testing_script_results = []
56+ for script_result in node._cached_script_results:
57+ if (script_result.script_set.result_type ==
58+ RESULT_TYPE.INSTALLATION):
59+ # Don't include installation results in the health
60+ # status.
61+ continue
62+ elif script_result.status == SCRIPT_STATUS.ABORTED:
63+ # LP: #1724235 - Ignore aborted scripts.
64+ continue
65+ elif (script_result.script_set.result_type ==
66+ RESULT_TYPE.COMMISSIONING):
67+ node._cached_commissioning_script_results.append(script_result)
68+ elif (script_result.script_set.result_type ==
69+ RESULT_TYPE.TESTING):
70+ node._cached_testing_script_results.append(script_result)
71+
72+ return node._cached_script_results
73+
74+
75+def get_script_status_name(script_status):
76+ for id, name in SCRIPT_STATUS_CHOICES:
77+ if id == script_status:
78+ return name
79+ return 'Unknown'
80+
81+
82 class NodeHandler(OperationsHandler):
83 """Manage an individual Node.
84
85@@ -281,6 +320,60 @@ class NodeHandler(OperationsHandler):
86 def current_installation_result_id(handler, node):
87 return node.current_installation_script_set_id
88
89+ @classmethod
90+ def commissioning_status(handler, node):
91+ get_cached_script_results(node)
92+ return get_status_from_qs(node._cached_commissioning_script_results)
93+
94+ @classmethod
95+ def commissioning_status_name(handler, node):
96+ return get_script_status_name(handler.commissioning_status(node))
97+
98+ @classmethod
99+ def testing_status(handler, node):
100+ get_cached_script_results(node)
101+ return get_status_from_qs(node._cached_testing_script_results)
102+
103+ @classmethod
104+ def testing_status_name(handler, node):
105+ return get_script_status_name(handler.testing_status(node))
106+
107+ @classmethod
108+ def cpu_test_status(handler, node):
109+ get_cached_script_results(node)
110+ return get_status_from_qs([
111+ script_result for script_result
112+ in node._cached_testing_script_results
113+ if script_result.script.hardware_type == HARDWARE_TYPE.CPU])
114+
115+ @classmethod
116+ def cpu_test_status_name(handler, node):
117+ return get_script_status_name(handler.cpu_test_status(node))
118+
119+ @classmethod
120+ def memory_test_status(handler, node):
121+ get_cached_script_results(node)
122+ return get_status_from_qs([
123+ script_result for script_result
124+ in node._cached_testing_script_results
125+ if script_result.script.hardware_type == HARDWARE_TYPE.MEMORY])
126+
127+ @classmethod
128+ def memory_test_status_name(handler, node):
129+ return get_script_status_name(handler.memory_test_status(node))
130+
131+ @classmethod
132+ def storage_test_status(handler, node):
133+ get_cached_script_results(node)
134+ return get_status_from_qs([
135+ script_result for script_result
136+ in node._cached_testing_script_results
137+ if script_result.script.hardware_type == HARDWARE_TYPE.STORAGE])
138+
139+ @classmethod
140+ def storage_test_status_name(handler, node):
141+ return get_script_status_name(handler.storage_test_status(node))
142+
143 def read(self, request, system_id):
144 """Read a specific Node.
145
146diff --git a/src/maasserver/api/rackcontrollers.py b/src/maasserver/api/rackcontrollers.py
147index 5799f3a..d643f04 100644
148--- a/src/maasserver/api/rackcontrollers.py
149+++ b/src/maasserver/api/rackcontrollers.py
150@@ -58,6 +58,16 @@ DISPLAYED_RACK_CONTROLLER_FIELDS = (
151 'current_testing_result_id',
152 'current_installation_result_id',
153 'version',
154+ 'commissioning_status',
155+ 'commissioning_status_name',
156+ 'testing_status',
157+ 'testing_status_name',
158+ 'cpu_test_status',
159+ 'cpu_test_status_name',
160+ 'memory_test_status',
161+ 'memory_test_status_name',
162+ 'storage_test_status',
163+ 'storage_test_status_name',
164 )
165
166
167diff --git a/src/maasserver/api/regioncontrollers.py b/src/maasserver/api/regioncontrollers.py
168index 9a2d244..69c7b08 100644
169--- a/src/maasserver/api/regioncontrollers.py
170+++ b/src/maasserver/api/regioncontrollers.py
171@@ -41,6 +41,16 @@ DISPLAYED_REGION_CONTROLLER_FIELDS = (
172 'current_testing_result_id',
173 'current_installation_result_id',
174 'version',
175+ 'commissioning_status',
176+ 'commissioning_status_name',
177+ 'testing_status',
178+ 'testing_status_name',
179+ 'cpu_test_status',
180+ 'cpu_test_status_name',
181+ 'memory_test_status',
182+ 'memory_test_status_name',
183+ 'storage_test_status',
184+ 'storage_test_status_name',
185 )
186
187
188diff --git a/src/maasserver/api/tests/test_enlistment.py b/src/maasserver/api/tests/test_enlistment.py
189index 471eeb7..16bec8d 100644
190--- a/src/maasserver/api/tests/test_enlistment.py
191+++ b/src/maasserver/api/tests/test_enlistment.py
192@@ -559,6 +559,16 @@ class SimpleUserLoggedInEnlistmentAPITest(APITestCase.ForUser):
193 'current_commissioning_result_id',
194 'current_testing_result_id',
195 'current_installation_result_id',
196+ 'commissioning_status',
197+ 'commissioning_status_name',
198+ 'testing_status',
199+ 'testing_status_name',
200+ 'cpu_test_status',
201+ 'cpu_test_status_name',
202+ 'memory_test_status',
203+ 'memory_test_status_name',
204+ 'storage_test_status',
205+ 'storage_test_status_name',
206 ],
207 list(parsed_result))
208
209@@ -736,6 +746,16 @@ class AdminLoggedInEnlistmentAPITest(APITestCase.ForAdmin):
210 'current_commissioning_result_id',
211 'current_testing_result_id',
212 'current_installation_result_id',
213+ 'commissioning_status',
214+ 'commissioning_status_name',
215+ 'testing_status',
216+ 'testing_status_name',
217+ 'cpu_test_status',
218+ 'cpu_test_status_name',
219+ 'memory_test_status',
220+ 'memory_test_status_name',
221+ 'storage_test_status',
222+ 'storage_test_status_name',
223 ],
224 list(parsed_result))
225
226diff --git a/src/maasserver/api/tests/test_machines.py b/src/maasserver/api/tests/test_machines.py
227index c8199cd..0ebb9e1 100644
228--- a/src/maasserver/api/tests/test_machines.py
229+++ b/src/maasserver/api/tests/test_machines.py
230@@ -342,12 +342,12 @@ class TestMachinesAPI(APITestCase.ForUser):
231 len(extract_system_ids(parsed_result_2)),
232 ])
233
234- # Because of fields `status_action`, `status_message`, and
235- # `default_gateways`. The number of queries is not the same but it is
236- # proportional to the number of machines.
237+ # Because of fields `status_action`, `status_message`,
238+ # `default_gateways`, and `health_status` the number of queries is not
239+ # the same but it is proportional to the number of machines.
240 DEFAULT_NUM = 57
241- self.assertEqual(DEFAULT_NUM + (10 * 3), num_queries1)
242- self.assertEqual(DEFAULT_NUM + (20 * 3), num_queries2)
243+ self.assertEqual(DEFAULT_NUM + (10 * 4), num_queries1)
244+ self.assertEqual(DEFAULT_NUM + (20 * 4), num_queries2)
245
246 def test_GET_without_machines_returns_empty_list(self):
247 # If there are no machines to list, the "read" op still works but
248diff --git a/src/maasserver/api/tests/test_node.py b/src/maasserver/api/tests/test_node.py
249index 706a3a0..4fecb15 100644
250--- a/src/maasserver/api/tests/test_node.py
251+++ b/src/maasserver/api/tests/test_node.py
252@@ -1,10 +1,11 @@
253-# Copyright 2013-2016 Canonical Ltd. This software is licensed under the
254+# Copyright 2013-2017 Canonical Ltd. This software is licensed under the
255 # GNU Affero General Public License version 3 (see the file LICENSE).
256
257 """Tests for the Node API."""
258
259 __all__ = []
260
261+from functools import partial
262 import http.client
263 import random
264 from unittest.mock import (
265@@ -40,11 +41,14 @@ from maastesting.matchers import (
266 MockNotCalled,
267 )
268 from metadataserver.enum import (
269+ HARDWARE_TYPE,
270 RESULT_TYPE,
271 SCRIPT_STATUS,
272+ SCRIPT_STATUS_CHOICES,
273 SCRIPT_TYPE,
274 )
275 from metadataserver.models import NodeKey
276+from metadataserver.models.scriptset import get_status_from_qs
277 from metadataserver.nodeinituser import get_node_init_user
278 from provisioningserver.refresh.node_info_scripts import (
279 LLDP_OUTPUT_NAME,
280@@ -184,6 +188,70 @@ class TestNodeAPI(APITestCase.ForUser):
281 args=[parsed_result['system_id']]),
282 parsed_result['resource_uri'])
283
284+ def test_health_status(self):
285+ self.become_admin()
286+ machine = factory.make_Machine(owner=self.user)
287+ commissioning_script_set = factory.make_ScriptSet(
288+ result_type=RESULT_TYPE.COMMISSIONING, node=machine)
289+ testing_script_set = factory.make_ScriptSet(
290+ result_type=RESULT_TYPE.TESTING, node=machine)
291+ make_script_result = partial(
292+ factory.make_ScriptResult, script_set=testing_script_set,
293+ status=factory.pick_choice(
294+ SCRIPT_STATUS_CHOICES, but_not=[SCRIPT_STATUS.ABORTED]))
295+ commissioning_script_result = make_script_result(
296+ script_set=commissioning_script_set, script=factory.make_Script(
297+ script_type=SCRIPT_TYPE.COMMISSIONING))
298+ cpu_script_result = make_script_result(
299+ script=factory.make_Script(
300+ script_type=SCRIPT_TYPE.TESTING,
301+ hardware_type=HARDWARE_TYPE.CPU))
302+ memory_script_result = make_script_result(
303+ script=factory.make_Script(
304+ script_type=SCRIPT_TYPE.TESTING,
305+ hardware_type=HARDWARE_TYPE.MEMORY))
306+ storage_script_result = make_script_result(
307+ script=factory.make_Script(
308+ script_type=SCRIPT_TYPE.TESTING,
309+ hardware_type=HARDWARE_TYPE.STORAGE))
310+ testing_script_results = (
311+ machine.get_latest_testing_script_results.exclude(
312+ status=SCRIPT_STATUS.ABORTED))
313+ testing_status = get_status_from_qs(testing_script_results)
314+
315+ response = self.client.get(self.get_node_uri(machine))
316+ parsed_result = json_load_bytes(response.content)
317+
318+ status = lambda s: get_status_from_qs([s])
319+ status_name = lambda s: SCRIPT_STATUS_CHOICES[status(s)][1]
320+ self.assertThat(response, HasStatusCode(http.client.OK))
321+ self.assertEquals(
322+ status(commissioning_script_result),
323+ parsed_result['commissioning_status'])
324+ self.assertEquals(
325+ status_name(commissioning_script_result),
326+ parsed_result['commissioning_status_name'])
327+ self.assertEquals(testing_status, parsed_result['testing_status'])
328+ self.assertEquals(
329+ SCRIPT_STATUS_CHOICES[testing_status][1],
330+ parsed_result['testing_status_name'])
331+ self.assertEquals(
332+ status(cpu_script_result), parsed_result['cpu_test_status'])
333+ self.assertEquals(
334+ status_name(cpu_script_result),
335+ parsed_result['cpu_test_status_name'])
336+ self.assertEquals(
337+ status(memory_script_result), parsed_result['memory_test_status'])
338+ self.assertEquals(
339+ status_name(memory_script_result),
340+ parsed_result['memory_test_status_name'])
341+ self.assertEquals(
342+ status(storage_script_result),
343+ parsed_result['storage_test_status'])
344+ self.assertEquals(
345+ status_name(storage_script_result),
346+ parsed_result['storage_test_status_name'])
347+
348 def test_DELETE_deletes_node(self):
349 # The api allows to delete a Node.
350 self.become_admin()
351diff --git a/src/maasserver/api/tests/test_rackcontroller.py b/src/maasserver/api/tests/test_rackcontroller.py
352index a631d6c..5105c6d 100644
353--- a/src/maasserver/api/tests/test_rackcontroller.py
354+++ b/src/maasserver/api/tests/test_rackcontroller.py
355@@ -135,6 +135,16 @@ class TestRackControllersAPI(APITestCase.ForUser):
356 'current_testing_result_id',
357 'current_installation_result_id',
358 'version',
359+ 'commissioning_status',
360+ 'commissioning_status_name',
361+ 'testing_status',
362+ 'testing_status_name',
363+ 'cpu_test_status',
364+ 'cpu_test_status_name',
365+ 'memory_test_status',
366+ 'memory_test_status_name',
367+ 'storage_test_status',
368+ 'storage_test_status_name',
369 ],
370 list(parsed_result[0]))
371
372diff --git a/src/maasserver/api/tests/test_regioncontroller.py b/src/maasserver/api/tests/test_regioncontroller.py
373index fd02644..3854e6f 100644
374--- a/src/maasserver/api/tests/test_regioncontroller.py
375+++ b/src/maasserver/api/tests/test_regioncontroller.py
376@@ -83,5 +83,15 @@ class TestRegionControllersAPI(APITestCase.ForUser):
377 'current_testing_result_id',
378 'current_installation_result_id',
379 'version',
380+ 'commissioning_status',
381+ 'commissioning_status_name',
382+ 'testing_status',
383+ 'testing_status_name',
384+ 'cpu_test_status',
385+ 'cpu_test_status_name',
386+ 'memory_test_status',
387+ 'memory_test_status_name',
388+ 'storage_test_status',
389+ 'storage_test_status_name',
390 ],
391 list(parsed_result[0]))
392diff --git a/src/maasserver/api/tests/test_tag.py b/src/maasserver/api/tests/test_tag.py
393index ef0d546..4397dd3 100644
394--- a/src/maasserver/api/tests/test_tag.py
395+++ b/src/maasserver/api/tests/test_tag.py
396@@ -197,10 +197,10 @@ class TestTagAPI(APITestCase.ForUser):
397 len(extract_system_ids(parsed_result_2)),
398 ])
399
400- # Because of fields `status_action`, `status_message`, and
401- # `default_gateways`. The number of queries is not the same but it is
402- # proportional to the number of machines.
403- self.assertEquals(num_queries1, num_queries2 - (3 * 3))
404+ # Because of fields `status_action`, `status_message`,
405+ # `default_gateways`, and `health_status` the number of queries is not
406+ # the same but it is proportional to the number of machines.
407+ self.assertEquals(num_queries1, num_queries2 - (3 * 4))
408
409 def test_GET_machines_returns_machines(self):
410 tag = factory.make_Tag()
411@@ -257,10 +257,10 @@ class TestTagAPI(APITestCase.ForUser):
412 len(extract_system_ids(parsed_result_2)),
413 ])
414
415- # Because of fields `status_action`, `status_message`, and
416- # `default_gateways`. The number of queries is not the same but it is
417- # proportional to the number of machines.
418- self.assertEquals(num_queries1, num_queries2 - (3 * 3))
419+ # Because of fields `status_action`, `status_message`,
420+ # `default_gateways`, and `health_status` the number of queries is not
421+ # the same but it is proportional to the number of machines.
422+ self.assertEquals(num_queries1, num_queries2 - (3 * 4))
423
424 def test_GET_devices_returns_devices(self):
425 tag = factory.make_Tag()
426@@ -375,7 +375,7 @@ class TestTagAPI(APITestCase.ForUser):
427 len(extract_system_ids(parsed_result_1)),
428 len(extract_system_ids(parsed_result_2)),
429 ])
430- self.assertEquals(num_queries1, num_queries2 - (3 * 2))
431+ self.assertEquals(num_queries1, num_queries2 - (3 * 3))
432
433 def test_GET_rack_controllers_returns_no_rack_controllers_nonadmin(self):
434 tag = factory.make_Tag()
435@@ -456,7 +456,7 @@ class TestTagAPI(APITestCase.ForUser):
436 len(extract_system_ids(parsed_result_1)),
437 len(extract_system_ids(parsed_result_2)),
438 ])
439- self.assertEquals(num_queries1, num_queries2 - 3)
440+ self.assertEquals(num_queries1, num_queries2 - 6)
441
442 def test_GET_region_controllers_returns_no_controllers_nonadmin(self):
443 tag = factory.make_Tag()
444diff --git a/src/maasserver/static/js/angular/controllers/node_details.js b/src/maasserver/static/js/angular/controllers/node_details.js
445index 503d30a..ac96431 100644
446--- a/src/maasserver/static/js/angular/controllers/node_details.js
447+++ b/src/maasserver/static/js/angular/controllers/node_details.js
448@@ -1000,7 +1000,7 @@ angular.module('MAAS').controller('NodeDetailsController', [
449 var results = $scope.node.installation_results;
450 if(!angular.isArray(results) ||
451 results.length === 0 || results[0].output === "") {
452- switch($scope.node.installation_script_set_status) {
453+ switch($scope.node.installation_status) {
454 case 0:
455 return "System is booting...";
456 case 1:
457diff --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
458index ceeaef8..f670338 100644
459--- a/src/maasserver/static/js/angular/controllers/tests/test_node_details.js
460+++ b/src/maasserver/static/js/angular/controllers/tests/test_node_details.js
461@@ -2197,7 +2197,7 @@ describe("NodeDetailsController", function() {
462 it("returns status message when no output and status", function() {
463 var controller = makeController();
464 $scope.node = makeNode();
465- $scope.node.installation_script_set_status = makeInteger(0, 5);
466+ $scope.node.installation_status = makeInteger(0, 5);
467 expect($scope.getInstallationData()).not.toBe("");
468 });
469 });
470diff --git a/src/maasserver/static/partials/machines-table.html b/src/maasserver/static/partials/machines-table.html
471index e47b10d..24fb55a 100644
472--- a/src/maasserver/static/partials/machines-table.html
473+++ b/src/maasserver/static/partials/machines-table.html
474@@ -57,8 +57,14 @@
475 <td class="powerstate u-upper-case--first" aria-label="Power state">
476 <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 $}
477 </td>
478- <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>
479- <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>
480+ <td class="table-col--3 u-display--desktop">
481+ <i data-ng-if="showSpinner(node)" class="icon icon--loading u-animation--spin u-display--desktop"></i>
482+ <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>
483+ </td>
484+ <td class="status u-text--truncate" aria-label="{$ node.status_tooltip $}">{$ getStatusText(node) $} <span class="u-display--mobile">
485+ <i data-ng-if="showSpinner(node)" class="icon icon--loading u-animation--spin u-margin--left-tiny"></i>
486+ <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>
487+ </span></td>
488 <td class="table-col--10" aria-label="Owner">{$ node.owner $}</td>
489 <td class="u-align--right u-display--desktop" aria-label="CPU">
490 <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>
491diff --git a/src/maasserver/static/partials/node-details.html b/src/maasserver/static/partials/node-details.html
492index f8d8087..324f3d1 100755
493--- a/src/maasserver/static/partials/node-details.html
494+++ b/src/maasserver/static/partials/node-details.html
495@@ -206,13 +206,13 @@
496 data-ng-click="section.area = 'storage'">Storage</button>
497 <button role="tab" class="page-navigation__link" data-ng-if="node.commissioning_script_count > 0"
498 data-ng-class="{ 'is-active': section.area === 'commissioning'}"
499- data-ng-click="section.area = 'commissioning'"><span data-maas-script-status="script-status" data-script-status="node.commissioning_script_set_status"></span> Commissioning</button>
500+ data-ng-click="section.area = 'commissioning'"><span data-maas-script-status="script-status" data-script-status="node.commissioning_status"></span> Commissioning</button>
501 <button role="tab" class="page-navigation__link" data-ng-if="node.testing_script_count > 0"
502 data-ng-class="{ 'is-active': section.area === 'testing'}"
503- 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>
504+ data-ng-click="section.area = 'testing'"><span data-maas-script-status="script-status" data-script-status="node.testing_status"></span> Hardware tests</button>
505 <button role="tab" class="page-navigation__link" data-ng-if="machine_output.viewable"
506 data-ng-class="{ 'is-active': section.area === 'logs'}"
507- 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>
508+ 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>
509 <button role="tab" class="page-navigation__link" data-ng-if="!isDevice"
510 data-ng-class="{ 'is-active': section.area === 'events'}"
511 data-ng-click="section.area = 'events'">Events</button>
512diff --git a/src/maasserver/websockets/handlers/machine.py b/src/maasserver/websockets/handlers/machine.py
513index ac101fc..da51686 100644
514--- a/src/maasserver/websockets/handlers/machine.py
515+++ b/src/maasserver/websockets/handlers/machine.py
516@@ -79,11 +79,7 @@ from maasserver.websockets.handlers.node import (
517 node_prefetch,
518 NodeHandler,
519 )
520-from metadataserver.enum import (
521- HARDWARE_TYPE,
522- SCRIPT_STATUS,
523- SCRIPT_STATUS_CHOICES,
524-)
525+from metadataserver.enum import HARDWARE_TYPE
526 from metadataserver.models import ScriptResult
527 from metadataserver.models.scriptset import get_status_from_qs
528 from provisioningserver.logger import LegacyLogger
529@@ -179,44 +175,6 @@ class MachineHandler(NodeHandler):
530 return Machine.objects.get_nodes(
531 self.user, NODE_PERMISSION.VIEW, from_nodes=self._meta.queryset)
532
533- def dehydrate_hardware_status_tooltip(self, script_results):
534- script_statuses = {}
535- for script_result in script_results:
536- if script_result.status in script_statuses:
537- script_statuses[script_result.status].add(script_result.name)
538- else:
539- script_statuses[script_result.status] = {script_result.name}
540-
541- tooltip = ''
542- for status, scripts in script_statuses.items():
543- len_scripts = len(scripts)
544- if status in {
545- SCRIPT_STATUS.PENDING, SCRIPT_STATUS.RUNNING,
546- SCRIPT_STATUS.INSTALLING}:
547- verb = 'is' if len_scripts == 1 else 'are'
548- elif status in {
549- SCRIPT_STATUS.PASSED, SCRIPT_STATUS.FAILED,
550- SCRIPT_STATUS.TIMEDOUT, SCRIPT_STATUS.FAILED_INSTALLING}:
551- verb = 'has' if len_scripts == 1 else 'have'
552- else:
553- # Covers SCRIPT_STATUS.ABORTED, an else is used incase new
554- # statuses are ever added.
555- verb = 'was' if len_scripts == 1 else 'were'
556-
557- if tooltip != '':
558- tooltip += ' '
559- if len_scripts == 1:
560- tooltip += '1 test '
561- else:
562- tooltip += '%s tests ' % len_scripts
563- tooltip += '%s %s.' % (
564- verb, SCRIPT_STATUS_CHOICES[status][1].lower())
565-
566- if tooltip == '':
567- tooltip = 'No tests have been run.'
568-
569- return tooltip
570-
571 def list(self, params):
572 """List objects.
573
574diff --git a/src/maasserver/websockets/handlers/node.py b/src/maasserver/websockets/handlers/node.py
575index 8266b2a..81910f0 100644
576--- a/src/maasserver/websockets/handlers/node.py
577+++ b/src/maasserver/websockets/handlers/node.py
578@@ -45,8 +45,10 @@ from maasserver.websockets.handlers.timestampedmodel import (
579 TimestampedModelHandler,
580 )
581 from metadataserver.enum import (
582+ HARDWARE_TYPE,
583 RESULT_TYPE,
584 SCRIPT_STATUS,
585+ SCRIPT_STATUS_CHOICES,
586 )
587 from metadataserver.models.scriptset import get_status_from_qs
588 from provisioningserver.tags import merge_details_cleanly
589@@ -118,6 +120,44 @@ class NodeHandler(TimestampedModelHandler):
590 """Return power_parameters None if empty."""
591 return None if power_parameters == '' else power_parameters
592
593+ def dehydrate_hardware_status_tooltip(self, script_results):
594+ script_statuses = {}
595+ for script_result in script_results:
596+ if script_result.status in script_statuses:
597+ script_statuses[script_result.status].add(script_result.name)
598+ else:
599+ script_statuses[script_result.status] = {script_result.name}
600+
601+ tooltip = ''
602+ for status, scripts in script_statuses.items():
603+ len_scripts = len(scripts)
604+ if status in {
605+ SCRIPT_STATUS.PENDING, SCRIPT_STATUS.RUNNING,
606+ SCRIPT_STATUS.INSTALLING}:
607+ verb = 'is' if len_scripts == 1 else 'are'
608+ elif status in {
609+ SCRIPT_STATUS.PASSED, SCRIPT_STATUS.FAILED,
610+ SCRIPT_STATUS.TIMEDOUT, SCRIPT_STATUS.FAILED_INSTALLING}:
611+ verb = 'has' if len_scripts == 1 else 'have'
612+ else:
613+ # Covers SCRIPT_STATUS.ABORTED, an else is used incase new
614+ # statuses are ever added.
615+ verb = 'was' if len_scripts == 1 else 'were'
616+
617+ if tooltip != '':
618+ tooltip += ' '
619+ if len_scripts == 1:
620+ tooltip += '1 test '
621+ else:
622+ tooltip += '%s tests ' % len_scripts
623+ tooltip += '%s %s.' % (
624+ verb, SCRIPT_STATUS_CHOICES[status][1].lower())
625+
626+ if tooltip == '':
627+ tooltip = 'No tests have been run.'
628+
629+ return tooltip
630+
631 def dehydrate(self, obj, data, for_list=False):
632 """Add extra fields to `data`."""
633 data["fqdn"] = obj.fqdn
634@@ -174,6 +214,40 @@ class NodeHandler(TimestampedModelHandler):
635 data["distro_series"] = obj.get_distro_series(
636 default=self.default_distro_series)
637 data["dhcp_on"] = self.get_providing_dhcp(obj)
638+
639+ if obj.node_type != NODE_TYPE.DEVICE:
640+ commissioning_script_results = []
641+ testing_script_results = []
642+ for hw_type in self._script_results.get(obj.id, {}).values():
643+ for script_result in hw_type:
644+ if (script_result.script_set.result_type ==
645+ RESULT_TYPE.INSTALLATION):
646+ # Don't include installation results in the health
647+ # status.
648+ continue
649+ elif script_result.status == SCRIPT_STATUS.ABORTED:
650+ # LP: #1724235 - Ignore aborted scripts.
651+ continue
652+ elif (script_result.script_set.result_type ==
653+ RESULT_TYPE.COMMISSIONING):
654+ commissioning_script_results.append(script_result)
655+ elif (script_result.script_set.result_type ==
656+ RESULT_TYPE.TESTING):
657+ testing_script_results.append(script_result)
658+
659+ data["commissioning_script_count"] = len(
660+ commissioning_script_results)
661+ data["commissioning_status"] = get_status_from_qs(
662+ commissioning_script_results)
663+ data["commissioning_status_tooltip"] = (
664+ self.dehydrate_hardware_status_tooltip(
665+ commissioning_script_results).replace(
666+ 'test', 'commissioning script'))
667+ data["testing_script_count"] = len(testing_script_results)
668+ data["testing_status"] = get_status_from_qs(testing_script_results)
669+ data["testing_status_tooltip"] = (
670+ self.dehydrate_hardware_status_tooltip(testing_script_results))
671+
672 if not for_list:
673 data["on_network"] = obj.on_network()
674 if obj.node_type != NODE_TYPE.DEVICE:
675@@ -216,23 +290,12 @@ class NodeHandler(TimestampedModelHandler):
676 # Events
677 data["events"] = self.dehydrate_events(obj)
678
679- # Machine output
680+ # Machine logs
681 data = self.dehydrate_summary_output(obj, data)
682- data["commissioning_script_count"] = (
683- obj.get_latest_commissioning_script_results.count())
684- data["commissioning_script_set_status"] = get_status_from_qs(
685- obj.get_latest_commissioning_script_results.exclude(
686- status=SCRIPT_STATUS.ABORTED))
687- data["testing_script_count"] = (
688- obj.get_latest_testing_script_results.count())
689- data["testing_script_set_status"] = get_status_from_qs(
690- obj.get_latest_testing_script_results.exclude(
691- status=SCRIPT_STATUS.ABORTED))
692+ data["installation_status"] = self.dehydrate_script_set_status(
693+ obj.current_installation_script_set)
694 data["installation_results"] = self.dehydrate_script_set(
695 obj.current_installation_script_set)
696- data["installation_script_set_status"] = (
697- self.dehydrate_script_set_status(
698- obj.current_installation_script_set))
699
700 # Third party drivers
701 if Config.objects.get_config('enable_third_party_drivers'):
702@@ -255,11 +318,11 @@ class NodeHandler(TimestampedModelHandler):
703 qs = qs.exclude(status=SCRIPT_STATUS.ABORTED)
704 cleared_node_ids = []
705 for script_result in qs:
706- # Builtin commissioning scripts are not stored in the database.
707- if script_result.script is None:
708- continue
709 node_id = script_result.script_set.node_id
710- hardware_type = script_result.script.hardware_type
711+ if script_result.script is not None:
712+ hardware_type = script_result.script.hardware_type
713+ else:
714+ hardware_type = HARDWARE_TYPE.NODE
715
716 if node_id not in cleared_node_ids:
717 self._script_results[node_id] = {}
718diff --git a/src/maasserver/websockets/handlers/tests/test_device.py b/src/maasserver/websockets/handlers/tests/test_device.py
719index b6a879b..b0b4490 100644
720--- a/src/maasserver/websockets/handlers/tests/test_device.py
721+++ b/src/maasserver/websockets/handlers/tests/test_device.py
722@@ -186,18 +186,19 @@ class TestDeviceHandler(MAASTransactionServerTestCase):
723 if for_list:
724 allowed_fields = DeviceHandler.Meta.list_fields + [
725 "actions",
726- "fqdn",
727 "extra_macs",
728- "metadata",
729- "tags",
730- "primary_mac",
731+ "fabrics",
732+ "fqdn",
733+ "installation_status",
734 "ip_address",
735 "ip_assignment",
736- "node_type_display",
737 "link_type",
738- "subnets",
739+ "metadata",
740+ "node_type_display",
741+ "primary_mac",
742 "spaces",
743- "fabrics",
744+ "subnets",
745+ "tags",
746 ]
747 for key in list(data):
748 if key not in allowed_fields:
749diff --git a/src/maasserver/websockets/handlers/tests/test_machine.py b/src/maasserver/websockets/handlers/tests/test_machine.py
750index 0dd01f2..15ab758 100644
751--- a/src/maasserver/websockets/handlers/tests/test_machine.py
752+++ b/src/maasserver/websockets/handlers/tests/test_machine.py
753@@ -166,30 +166,36 @@ class TestMachineHandler(MAASServerTestCase):
754 ]
755 disks = sorted(disks, key=itemgetter("name"))
756 subnets = handler.get_all_subnets(node)
757+ commissioning_scripts = node.get_latest_commissioning_script_results
758+ commissioning_scripts = commissioning_scripts.exclude(
759+ status=SCRIPT_STATUS.ABORTED)
760+ testing_scripts = node.get_latest_testing_script_results
761+ testing_scripts = testing_scripts.exclude(
762+ status=SCRIPT_STATUS.ABORTED)
763 data = {
764 "actions": list(compile_node_actions(node, handler.user).keys()),
765 "architecture": node.architecture,
766 "bmc": node.bmc_id,
767 "boot_disk": node.boot_disk,
768 "bios_boot_method": node.bios_boot_method,
769- "commissioning_script_count": (
770- node.get_latest_commissioning_script_results.count()),
771- "commissioning_script_set_status": get_status_from_qs(
772- node.get_latest_commissioning_script_results.exclude(
773- status=SCRIPT_STATUS.ABORTED)),
774+ "commissioning_script_count": commissioning_scripts.count(),
775+ "commissioning_status": get_status_from_qs(commissioning_scripts),
776+ "commissioning_status_tooltip": (
777+ handler.dehydrate_hardware_status_tooltip(
778+ commissioning_scripts).replace(
779+ 'test', 'commissioning script')),
780 "current_commissioning_script_set": (
781 node.current_commissioning_script_set_id),
782- "testing_script_count": (
783- node.get_latest_testing_script_results.count()),
784- "testing_script_set_status": get_status_from_qs(
785- node.get_latest_testing_script_results.exclude(
786- status=SCRIPT_STATUS.ABORTED)),
787+ "testing_script_count": testing_scripts.count(),
788+ "testing_status": get_status_from_qs(testing_scripts),
789+ "testing_status_tooltip": (
790+ handler.dehydrate_hardware_status_tooltip(testing_scripts)),
791 "current_testing_script_set": node.current_testing_script_set_id,
792 "installation_results": handler.dehydrate_script_set(
793 node.current_installation_script_set),
794 "current_installation_script_set": (
795 node.current_installation_script_set_id),
796- "installation_script_set_status": (
797+ "installation_status": (
798 handler.dehydrate_script_set_status(
799 node.current_installation_script_set)),
800 "cpu_count": node.cpu_count,
801@@ -286,26 +292,32 @@ class TestMachineHandler(MAASServerTestCase):
802 allowed_fields = MachineHandler.Meta.list_fields + [
803 "actions",
804 "architecture",
805+ "commissioning_script_count",
806+ "commissioning_status",
807+ "commissioning_status_tooltip",
808+ "dhcp_on",
809+ "distro_series",
810+ "extra_macs",
811+ "fabrics",
812 "fqdn",
813+ "link_type",
814 "metadata",
815- "status",
816- "status_code",
817+ "node_type_display",
818+ "osystem",
819+ "physical_disk_count",
820+ "pod",
821 "pxe_mac",
822 "pxe_mac_vendor",
823- "extra_macs",
824- "tags",
825- "subnets",
826- "fabrics",
827 "spaces",
828- "physical_disk_count",
829+ "status",
830+ "status_code",
831 "storage",
832 "storage_tags",
833- "node_type_display",
834- "osystem",
835- "distro_series",
836- "dhcp_on",
837- "pod",
838- "link_type",
839+ "subnets",
840+ "tags",
841+ "testing_script_count",
842+ "testing_status",
843+ "testing_status_tooltip",
844 ]
845 for key in list(data):
846 if key not in allowed_fields:

Subscribers

People subscribed via source and target branches