Merge ~ltrager/maas:script_result_skip into maas:master

Proposed by Lee Trager
Status: Merged
Approved by: Andres Rodriguez
Approved revision: f7b546b38aee71d52902b23f60f4910c28ce53aa
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~ltrager/maas:script_result_skip
Merge into: maas:master
Diff against target: 295 lines (+74/-17)
10 files modified
src/maasserver/static/js/angular/directives/script_status.js (+5/-1)
src/maasserver/static/js/angular/directives/tests/test_script_status.js (+6/-0)
src/maasserver/static/partials/script-results-list.html (+4/-4)
src/metadataserver/builtin_scripts/smartctl.py (+7/-0)
src/metadataserver/builtin_scripts/tests/test_smartctl.py (+10/-1)
src/metadataserver/enum.py (+3/-1)
src/metadataserver/migrations/builtin/0018_script_result_skipped.py (+23/-0)
src/metadataserver/models/scriptresult.py (+7/-4)
src/metadataserver/models/scriptset.py (+2/-0)
src/metadataserver/models/tests/test_scriptresult.py (+7/-6)
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
MAAS Lander Approve
Review via email: mp+343139@code.launchpad.net

Commit message

LP: #1760919 - Allow ScriptResults to be skipped

Scripts may now signal 'skipped' in the result YAML. This tells MAAS the
script was unable to run but does not change the state of the node. The SMART
tests use 'skipped' when SMART is unavailable on a device.

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

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

STATUS: SUCCESS
COMMIT: f7b546b38aee71d52902b23f60f4910c28ce53aa

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

Looks good.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/src/maasserver/static/js/angular/directives/script_status.js b/src/maasserver/static/js/angular/directives/script_status.js
index 10d6316..fd8e793 100644
--- a/src/maasserver/static/js/angular/directives/script_status.js
+++ b/src/maasserver/static/js/angular/directives/script_status.js
@@ -1,4 +1,4 @@
1/* Copyright 2017 Canonical Ltd. This software is licensed under the1/* Copyright 2017-2018 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).
3 *3 *
4 * Script status icon select directive.4 * Script status icon select directive.
@@ -51,6 +51,10 @@ angular.module('MAAS').directive('maasScriptStatus', function() {
51 case 4:51 case 4:
52 $scope.icon = 'p-icon--timed-out';52 $scope.icon = 'p-icon--timed-out';
53 break;53 break;
54 // SCRIPT_STATUS.SKIPPED
55 case 9:
56 $scope.icon = 'p-icon--warning';
57 break;
54 case -1:58 case -1:
55 // No scripts have been run.59 // No scripts have been run.
56 $scope.show = false;60 $scope.show = false;
diff --git a/src/maasserver/static/js/angular/directives/tests/test_script_status.js b/src/maasserver/static/js/angular/directives/tests/test_script_status.js
index c43d192..d51b0d4 100644
--- a/src/maasserver/static/js/angular/directives/tests/test_script_status.js
+++ b/src/maasserver/static/js/angular/directives/tests/test_script_status.js
@@ -87,6 +87,12 @@ describe("maasScriptStatus", function() {
87 expect(select.attr("class")).toBe("p-icon--timed-out");87 expect(select.attr("class")).toBe("p-icon--timed-out");
88 });88 });
8989
90 it("SCRIPT_STATUS.SKIPPED", function() {
91 var directive = compileDirective("9");
92 var select = directive.find("span");
93 expect(select.attr("class")).toBe("p-icon--warning");
94 });
95
90 it("NONE", function() {96 it("NONE", function() {
91 var directive = compileDirective("-1");97 var directive = compileDirective("-1");
92 var select = directive.find("span");98 var select = directive.find("span");
diff --git a/src/maasserver/static/partials/script-results-list.html b/src/maasserver/static/partials/script-results-list.html
index 1c1fa4c..187a8cb 100644
--- a/src/maasserver/static/partials/script-results-list.html
+++ b/src/maasserver/static/partials/script-results-list.html
@@ -32,8 +32,8 @@
32 <td class="col-3" aria-label="Date"><span data-ng-hide="result.showing_history">{$ result.updated $}</span></td>32 <td class="col-3" aria-label="Date"><span data-ng-hide="result.showing_history">{$ result.updated $}</span></td>
33 <td class="col-2" aria-label="Result">33 <td class="col-2" aria-label="Result">
34 <span data-ng-hide="result.showing_history">34 <span data-ng-hide="result.showing_history">
35 <!-- Only link to the testing result when we've received it. This is indicated with status 2(passed), 3(failed), 4(timedout), 6(degraded), 8(failed installing)-->35 <!-- Only link to the testing result when we've received it. This is indicated with status 2(passed), 3(failed), 4(timedout), 6(degraded), 8(failed installing), 9(skipped)-->
36 {$ result.status_name $} <a data-ng-if="result.status === 2 || result.status === 3 || result.status === 4 || result.status === 6 || result.status === 8" href="#/{$ type_name $}/{$ node.system_id $}/{$ section.area $}/{$ result.id $}">View log</a>36 {$ result.status_name $} <a data-ng-if="result.status === 2 || result.status === 3 || result.status === 4 || result.status === 6 || result.status === 8 || result.status === 9" href="#/{$ type_name $}/{$ node.system_id $}/{$ section.area $}/{$ result.id $}">View log</a>
37 </span>37 </span>
38 </td>38 </td>
39 <td class="col-1">39 <td class="col-1">
@@ -84,8 +84,8 @@
84 <td class="col-1" aria-label="Runtime"><span data-maas-script-run-time="script-runtime" data-start-time="item.starttime" data-run-time="{{item.runtime}}" data-estimated-run-time="{{item.estimated_runtime}}" data-script-status="item.status"></span></td>84 <td class="col-1" aria-label="Runtime"><span data-maas-script-run-time="script-runtime" data-start-time="item.starttime" data-run-time="{{item.runtime}}" data-estimated-run-time="{{item.estimated_runtime}}" data-script-status="item.status"></span></td>
85 <td class="col-3" aria-label="Date">{$ item.updated $}</td>85 <td class="col-3" aria-label="Date">{$ item.updated $}</td>
86 <td class="col-2" aria-label="Status">86 <td class="col-2" aria-label="Status">
87 <!-- Only link to the testing result when we've received it. This is indicated with status 2(passed), 3(failed), 4(timedout), 6(degraded), 8(failed installing)-->87 <!-- Only link to the testing result when we've received it. This is indicated with status 2(passed), 3(failed), 4(timedout), 6(degraded), 8(failed installing), 9(skipped)-->
88 {$ item.status_name $} <a data-ng-if="item.status === 2 || item.status === 3 || item.status === 4 || item.status === 6 || item.status === 8" href="#/{$ type_name $}/{$ node.system_id $}/{$ section.area $}/{$ item.id $}">View log</a>88 {$ item.status_name $} <a data-ng-if="item.status === 2 || item.status === 3 || item.status === 4 || item.status === 6 || item.status === 8 || item.status === 9" href="#/{$ type_name $}/{$ node.system_id $}/{$ section.area $}/{$ item.id $}">View log</a>
89 </td>89 </td>
90 </tr>90 </tr>
91 </tbody>91 </tbody>
diff --git a/src/metadataserver/builtin_scripts/smartctl.py b/src/metadataserver/builtin_scripts/smartctl.py
index 5cff6d8..6b747f7 100644
--- a/src/metadataserver/builtin_scripts/smartctl.py
+++ b/src/metadataserver/builtin_scripts/smartctl.py
@@ -35,6 +35,7 @@
35# --- End MAAS 1.0 script metadata ---35# --- End MAAS 1.0 script metadata ---
3636
37import argparse37import argparse
38import os
38import re39import re
39from subprocess import (40from subprocess import (
40 CalledProcessError,41 CalledProcessError,
@@ -49,6 +50,8 @@ from subprocess import (
49import sys50import sys
50from time import sleep51from time import sleep
5152
53import yaml
54
52# We're just reading the SMART data or asking the drive to run a self test.55# We're just reading the SMART data or asking the drive to run a self test.
53# If this takes more then a minute there is something wrong the with drive.56# If this takes more then a minute there is something wrong the with drive.
54TIMEOUT = 6057TIMEOUT = 60
@@ -84,6 +87,10 @@ def check_SMART_support(storage):
84 print(87 print(
85 'INFO: Unable to run test. The following drive '88 'INFO: Unable to run test. The following drive '
86 'does not support SMART: %s\n' % storage)89 'does not support SMART: %s\n' % storage)
90 result_path = os.environ.get('RESULT_PATH')
91 if result_path is not None:
92 with open(result_path, 'w') as results_file:
93 yaml.safe_dump({'status': 'skipped'}, results_file)
87 sys.exit()94 sys.exit()
88 print('INFO: SMART support is available; continuing...')95 print('INFO: SMART support is available; continuing...')
8996
diff --git a/src/metadataserver/builtin_scripts/tests/test_smartctl.py b/src/metadataserver/builtin_scripts/tests/test_smartctl.py
index 9781549..11767a3 100644
--- a/src/metadataserver/builtin_scripts/tests/test_smartctl.py
+++ b/src/metadataserver/builtin_scripts/tests/test_smartctl.py
@@ -1,10 +1,11 @@
1# Copyright 2017 Canonical Ltd. This software is licensed under the1# Copyright 2017-2018 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"""Test smartctl functions."""4"""Test smartctl functions."""
55
6__all__ = []6__all__ = []
77
8import io
8from subprocess import (9from subprocess import (
9 CalledProcessError,10 CalledProcessError,
10 DEVNULL,11 DEVNULL,
@@ -81,6 +82,11 @@ class TestSmartCTL(MAASTestCase):
81 mock_check_output = self.patch(smartctl, "check_output")82 mock_check_output = self.patch(smartctl, "check_output")
82 mock_check_output.return_value = b"SMART support is not available."83 mock_check_output.return_value = b"SMART support is not available."
83 mock_print = self.patch(smartctl, "print")84 mock_print = self.patch(smartctl, "print")
85 result_path = factory.make_name("result_path")
86 self.patch(smartctl.os, "environ", {"RESULT_PATH": result_path})
87 mock_open = self.patch(smartctl, "open")
88 mock_open.return_value = io.StringIO()
89 mock_yaml_safe_dump = self.patch(smartctl.yaml, "safe_dump")
8490
85 self.assertRaises(SystemExit, smartctl.check_SMART_support, storage)91 self.assertRaises(SystemExit, smartctl.check_SMART_support, storage)
86 self.assertThat(mock_check_output, MockCalledOnceWith(92 self.assertThat(mock_check_output, MockCalledOnceWith(
@@ -95,6 +101,9 @@ class TestSmartCTL(MAASTestCase):
95 'sudo -n smartctl --all %s\n' % storage),101 'sudo -n smartctl --all %s\n' % storage),
96 call('INFO: Unable to run test. The following drive does '102 call('INFO: Unable to run test. The following drive does '
97 'not support SMART: %s\n' % storage)))103 'not support SMART: %s\n' % storage)))
104 self.assertThat(mock_open, MockCalledOnceWith(result_path, "w"))
105 self.assertThat(mock_yaml_safe_dump, MockCalledOnceWith(
106 {'status': 'skipped'}, mock_open.return_value))
98107
99 def test_run_smartctl_selftest(self):108 def test_run_smartctl_selftest(self):
100 storage = factory.make_name('storage')109 storage = factory.make_name('storage')
diff --git a/src/metadataserver/enum.py b/src/metadataserver/enum.py
index 7188914..d9a362c 100644
--- a/src/metadataserver/enum.py
+++ b/src/metadataserver/enum.py
@@ -76,6 +76,7 @@ class SCRIPT_STATUS:
76 DEGRADED = 676 DEGRADED = 6
77 INSTALLING = 777 INSTALLING = 7
78 FAILED_INSTALLING = 878 FAILED_INSTALLING = 8
79 SKIPPED = 9
7980
8081
81SCRIPT_STATUS_CHOICES = (82SCRIPT_STATUS_CHOICES = (
@@ -87,7 +88,8 @@ SCRIPT_STATUS_CHOICES = (
87 (SCRIPT_STATUS.ABORTED, "Aborted"),88 (SCRIPT_STATUS.ABORTED, "Aborted"),
88 (SCRIPT_STATUS.DEGRADED, "Degraded"),89 (SCRIPT_STATUS.DEGRADED, "Degraded"),
89 (SCRIPT_STATUS.INSTALLING, "Installing dependencies"),90 (SCRIPT_STATUS.INSTALLING, "Installing dependencies"),
90 (SCRIPT_STATUS.FAILED_INSTALLING, "Failed installing dependencies")91 (SCRIPT_STATUS.FAILED_INSTALLING, "Failed installing dependencies"),
92 (SCRIPT_STATUS.SKIPPED, "Skipped"),
91)93)
9294
9395
diff --git a/src/metadataserver/migrations/builtin/0018_script_result_skipped.py b/src/metadataserver/migrations/builtin/0018_script_result_skipped.py
94new file mode 10064496new file mode 100644
index 0000000..0e3eca6
--- /dev/null
+++ b/src/metadataserver/migrations/builtin/0018_script_result_skipped.py
@@ -0,0 +1,23 @@
1# -*- coding: utf-8 -*-
2# Generated by Django 1.11.11 on 2018-04-12 20:44
3from __future__ import unicode_literals
4
5from django.db import (
6 migrations,
7 models,
8)
9
10
11class Migration(migrations.Migration):
12
13 dependencies = [
14 ('metadataserver', '0017_store_requested_scripts'),
15 ]
16
17 operations = [
18 migrations.AlterField(
19 model_name='scriptresult',
20 name='status',
21 field=models.IntegerField(choices=[(0, 'Pending'), (1, 'Running'), (2, 'Passed'), (3, 'Failed'), (4, 'Timed out'), (5, 'Aborted'), (6, 'Degraded'), (7, 'Installing dependencies'), (8, 'Failed installing dependencies'), (9, 'Skipped')], default=0),
22 ),
23 ]
diff --git a/src/metadataserver/models/scriptresult.py b/src/metadataserver/models/scriptresult.py
index 9cd657f..add7c94 100644
--- a/src/metadataserver/models/scriptresult.py
+++ b/src/metadataserver/models/scriptresult.py
@@ -189,10 +189,10 @@ class ScriptResult(CleanSave, TimestampedModel):
189 raise ValidationError('YAML must be a dictionary.')189 raise ValidationError('YAML must be a dictionary.')
190190
191 if parsed_yaml.get('status') not in [191 if parsed_yaml.get('status') not in [
192 'passed', 'failed', 'degraded', 'timedout', None]:192 'passed', 'failed', 'degraded', 'timedout', 'skipped', None]:
193 raise ValidationError(193 raise ValidationError(
194 'status must be "passed", "failed", "degraded", or '194 'status must be "passed", "failed", "degraded", '
195 '"timedout".')195 '"timedout", or "skipped".')
196196
197 results = parsed_yaml.get('results')197 results = parsed_yaml.get('results')
198 if results is None:198 if results is None:
@@ -274,6 +274,8 @@ class ScriptResult(CleanSave, TimestampedModel):
274 self.status = SCRIPT_STATUS.DEGRADED274 self.status = SCRIPT_STATUS.DEGRADED
275 elif status == 'timedout':275 elif status == 'timedout':
276 self.status = SCRIPT_STATUS.TIMEDOUT276 self.status = SCRIPT_STATUS.TIMEDOUT
277 elif status == 'skipped':
278 self.status = SCRIPT_STATUS.SKIPPED
277279
278 if self.script:280 if self.script:
279 if script_version_id is not None:281 if script_version_id is not None:
@@ -351,7 +353,8 @@ class ScriptResult(CleanSave, TimestampedModel):
351 elif self.ended is None and self.status in {353 elif self.ended is None and self.status in {
352 SCRIPT_STATUS.PASSED, SCRIPT_STATUS.FAILED,354 SCRIPT_STATUS.PASSED, SCRIPT_STATUS.FAILED,
353 SCRIPT_STATUS.TIMEDOUT, SCRIPT_STATUS.ABORTED,355 SCRIPT_STATUS.TIMEDOUT, SCRIPT_STATUS.ABORTED,
354 SCRIPT_STATUS.DEGRADED, SCRIPT_STATUS.FAILED_INSTALLING}:356 SCRIPT_STATUS.DEGRADED, SCRIPT_STATUS.FAILED_INSTALLING,
357 SCRIPT_STATUS.SKIPPED}:
355 self.ended = datetime.now()358 self.ended = datetime.now()
356 if 'update_fields' in kwargs:359 if 'update_fields' in kwargs:
357 kwargs['update_fields'].append('ended')360 kwargs['update_fields'].append('ended')
diff --git a/src/metadataserver/models/scriptset.py b/src/metadataserver/models/scriptset.py
index 6836b57..f84399f 100644
--- a/src/metadataserver/models/scriptset.py
+++ b/src/metadataserver/models/scriptset.py
@@ -68,6 +68,8 @@ def get_status_from_qs(qs):
68 if count == 0:68 if count == 0:
69 return -169 return -1
70 # The status order below represents the order of precedence.70 # The status order below represents the order of precedence.
71 # Skipped is omitted here otherwise one skipped test will show
72 # a warning icon in the UI on the test tab.
71 for status in (73 for status in (
72 SCRIPT_STATUS.RUNNING, SCRIPT_STATUS.INSTALLING,74 SCRIPT_STATUS.RUNNING, SCRIPT_STATUS.INSTALLING,
73 SCRIPT_STATUS.PENDING, SCRIPT_STATUS.ABORTED,75 SCRIPT_STATUS.PENDING, SCRIPT_STATUS.ABORTED,
diff --git a/src/metadataserver/models/tests/test_scriptresult.py b/src/metadataserver/models/tests/test_scriptresult.py
index fedeb6d..ca72e72 100644
--- a/src/metadataserver/models/tests/test_scriptresult.py
+++ b/src/metadataserver/models/tests/test_scriptresult.py
@@ -196,6 +196,7 @@ class TestScriptResult(MAASServerTestCase):
196 'failed': SCRIPT_STATUS.FAILED,196 'failed': SCRIPT_STATUS.FAILED,
197 'degraded': SCRIPT_STATUS.DEGRADED,197 'degraded': SCRIPT_STATUS.DEGRADED,
198 'timedout': SCRIPT_STATUS.TIMEDOUT,198 'timedout': SCRIPT_STATUS.TIMEDOUT,
199 'skipped': SCRIPT_STATUS.SKIPPED,
199 }200 }
200 status = random.choice(list(status_choices.keys()))201 status = random.choice(list(status_choices.keys()))
201 status_yaml = {'status': status}202 status_yaml = {'status': status}
@@ -351,7 +352,7 @@ class TestScriptResult(MAASServerTestCase):
351 script_result.status = random.choice([352 script_result.status = random.choice([
352 SCRIPT_STATUS.PASSED, SCRIPT_STATUS.FAILED, SCRIPT_STATUS.TIMEDOUT,353 SCRIPT_STATUS.PASSED, SCRIPT_STATUS.FAILED, SCRIPT_STATUS.TIMEDOUT,
353 SCRIPT_STATUS.ABORTED, SCRIPT_STATUS.DEGRADED,354 SCRIPT_STATUS.ABORTED, SCRIPT_STATUS.DEGRADED,
354 SCRIPT_STATUS.FAILED_INSTALLING])355 SCRIPT_STATUS.FAILED_INSTALLING, SCRIPT_STATUS.SKIPPED])
355 script_result.save(update_fields=['status'])356 script_result.save(update_fields=['status'])
356 self.assertIsNotNone(reload_object(script_result).ended)357 self.assertIsNotNone(reload_object(script_result).ended)
357358
@@ -362,7 +363,7 @@ class TestScriptResult(MAASServerTestCase):
362 script_result.status = random.choice([363 script_result.status = random.choice([
363 SCRIPT_STATUS.PASSED, SCRIPT_STATUS.FAILED, SCRIPT_STATUS.TIMEDOUT,364 SCRIPT_STATUS.PASSED, SCRIPT_STATUS.FAILED, SCRIPT_STATUS.TIMEDOUT,
364 SCRIPT_STATUS.ABORTED, SCRIPT_STATUS.DEGRADED,365 SCRIPT_STATUS.ABORTED, SCRIPT_STATUS.DEGRADED,
365 SCRIPT_STATUS.FAILED_INSTALLING])366 SCRIPT_STATUS.FAILED_INSTALLING, SCRIPT_STATUS.SKIPPED])
366 script_result.save(update_fields=['status'])367 script_result.save(update_fields=['status'])
367 script_result = reload_object(script_result)368 script_result = reload_object(script_result)
368 self.assertIsNotNone(script_result.started)369 self.assertIsNotNone(script_result.started)
@@ -502,7 +503,7 @@ class TestScriptResult(MAASServerTestCase):
502 def test_read_results(self):503 def test_read_results(self):
503 results = {504 results = {
504 'status': random.choice(505 'status': random.choice(
505 ['passed', 'failed', 'degraded', 'timedout']),506 ['passed', 'failed', 'degraded', 'timedout', 'skipped']),
506 'results': {507 'results': {
507 factory.make_name('key'): factory.make_name('value'),508 factory.make_name('key'): factory.make_name('value'),
508 factory.make_name('key'): [509 factory.make_name('key'): [
@@ -524,7 +525,7 @@ class TestScriptResult(MAASServerTestCase):
524525
525 def test_read_results_does_not_require_results(self):526 def test_read_results_does_not_require_results(self):
526 result = {'status': random.choice(527 result = {'status': random.choice(
527 ['passed', 'failed', 'degraded', 'timedout'])}528 ['passed', 'failed', 'degraded', 'timedout', 'skipped'])}
528 script_result = factory.make_ScriptResult(529 script_result = factory.make_ScriptResult(
529 result=yaml.safe_dump(result).encode())530 result=yaml.safe_dump(result).encode())
530 self.assertDictEqual(result, script_result.read_results())531 self.assertDictEqual(result, script_result.read_results())
@@ -547,8 +548,8 @@ class TestScriptResult(MAASServerTestCase):
547 result=yaml.safe_dump(result).encode())548 result=yaml.safe_dump(result).encode())
548 with self.assertRaisesRegex(549 with self.assertRaisesRegex(
549 ValidationError,550 ValidationError,
550 'status must be "passed", "failed", "degraded", or '551 'status must be "passed", "failed", "degraded", '
551 '"timedout".'):552 '"timedout", or "skipped".'):
552 script_result.read_results()553 script_result.read_results()
553554
554 def test_read_results_errors_when_dict_keys_not_str(self):555 def test_read_results_errors_when_dict_keys_not_str(self):

Subscribers

People subscribed via source and target branches