Merge ~mpontillo/maas:preseed-per-interface-routes--bug-1758919--2.3 into maas:master

Proposed by Mike Pontillo on 2018-03-27
Status: Superseded
Proposed branch: ~mpontillo/maas:preseed-per-interface-routes--bug-1758919--2.3
Merge into: maas:master
Diff against target: 1785 lines (+1501/-2) (has conflicts)
17 files modified
debian/changelog (+10/-0)
snap/snapcraft.yaml (+9/-0)
src/maasserver/bootsources.py (+3/-0)
src/maasserver/models/tests/test_userprofile.py (+3/-0)
src/maasserver/rpc/boot.py (+5/-0)
src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js (+17/-0)
src/maasserver/static/partials/ipranges.html (+17/-0)
src/maasserver/static/partials/node-details.html (+134/-0)
src/maasserver/static/partials/nodes-list.html (+1017/-0)
src/maasserver/static/partials/script-results-list.html (+76/-0)
src/maasserver/tests/test_bootsources.py (+3/-0)
src/maasserver/triggers/tests/test_websocket_listener.py (+5/-0)
src/provisioningserver/import_images/boot_resources.py (+61/-0)
src/provisioningserver/import_images/tests/test_boot_resources.py (+92/-0)
src/provisioningserver/import_images/tests/test_download_resources.py (+35/-0)
src/provisioningserver/utils/tests/test_network.py (+3/-0)
utilities/release-build (+11/-2)
Conflict in debian/changelog
Conflict in snap/snapcraft.yaml
Conflict in src/maasserver/bootsources.py
Conflict in src/maasserver/models/tests/test_userprofile.py
Conflict in src/maasserver/rpc/boot.py
Conflict in src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js
Conflict in src/maasserver/static/partials/ipranges.html
Conflict in src/maasserver/static/partials/node-details.html
Conflict in src/maasserver/static/partials/nodes-list.html
Conflict in src/maasserver/static/partials/script-results-list.html
Conflict in src/maasserver/tests/test_bootsources.py
Conflict in src/maasserver/triggers/tests/test_websocket_listener.py
Conflict in src/provisioningserver/import_images/boot_resources.py
Conflict in src/provisioningserver/import_images/tests/test_boot_resources.py
Conflict in src/provisioningserver/import_images/tests/test_download_resources.py
Conflict in src/provisioningserver/utils/tests/test_network.py
Conflict in utilities/release-build
Reviewer Review Type Date Requested Status
MAAS Maintainers 2018-03-27 Pending
Review via email: mp+342242@code.launchpad.net

Commit message

LP: #1758919 - Move routes to interface context within preseed.

Backports: 9fbf7eb682c3c3ff1de08b33bb967fffdf4a61bf

To post a comment you must log in.

Unmerged commits

434ff94... by Mike Pontillo on 2018-03-27

LP: #1758919 - Move routes to interface context within preseed.

Backports: 9fbf7eb682c3c3ff1de08b33bb967fffdf4a61bf

b830483... by Newell Jensen on 2018-03-08

Backport of 611eaccf792a388fc7b5d8e242050228a6bc0e57

LP: #1753874 -- Fix typo in virsh pod driver for get_pod_pool_size_map.

a318f7c... by Newell Jensen on 2018-03-07

Backport 476dcf44f1370625d5c5637d3b459cdd6511db0c

Convert number to exponential representation in RSD Pod driver.

b3b05a9... by Andres Rodriguez on 2018-03-05

Update to reflect 2.3.1 release

036d646... by Andres Rodriguez on 2018-03-02

Clean-up build scripts

8289431... by Andres Rodriguez on 2018-03-02

debian/changelog: Update in preparation for 2.3.1 release.

d5c58de... by Lee Trager on 2018-03-02

LP: #1751946 - Only cleanup ScriptSets of the same type.

Backport 1f9751aec9041dc079b83db4e9e81485cd188ef6 from master

5a7cb12... by Newell Jensen on 2018-02-27

Backport of c5a810187578776c9dacf386d2628f23678a6aa2

LP: #1741165 -- Remove --delete-snapshots flag to workaround volumes not being deleted for node decomposition.

8442748... by Andres Rodriguez on 2018-02-24

LP: #1711203 - Use grub instead of shim to workaround issues with Secure Boot deployment

Backport 1223c03c6ae23967561ec7218344daaf3a95698d from master

d5bd3b1... by Lee Trager on 2018-02-22

Backport 02b1bf4 - LP: #1750160 - Confirm running tests on deployed hardware in the UI.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/debian/changelog b/debian/changelog
2index 09263b4..ab17966 100644
3--- a/debian/changelog
4+++ b/debian/changelog
5@@ -1,3 +1,4 @@
6+<<<<<<< debian/changelog
7 maas (2.4.0~beta1-0ubuntu1) UNRELEASED; urgency=medium
8
9 * UNRELEASED
10@@ -28,6 +29,15 @@ maas (2.4.0~alpha1-6573-g12ee2331b-0ubuntu1) bionic; urgency=medium
11 -- Andres Rodriguez <andreserl@ubuntu.com> Fri, 09 Feb 2018 18:50:10 -0500
12
13 maas (2.3.0-6434-gd354690-0ubuntu1) bionic; urgency=medium
14+=======
15+maas (2.3.1-6470-g036d646-0ubuntu1) artful; urgency=medium
16+
17+ * New upstream release, MAAS 2.3.1
18+
19+ -- Andres Rodriguez <andreserl@ubuntu.com> Mon, 05 Mar 2018 10:25:44 -0500
20+
21+maas (2.3.0-6434-gd354690-0ubuntu1) artful; urgency=medium
22+>>>>>>> debian/changelog
23
24 * New upstream release, MAAS 2.3.0:
25 - Add support for CentOS & Windows networking.
26diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
27index 4c39956..3b31301 100644
28--- a/snap/snapcraft.yaml
29+++ b/snap/snapcraft.yaml
30@@ -133,6 +133,15 @@ parts:
31 source: src/maasserver/static
32 organize:
33 '*': usr/share/maas/web/static/
34+<<<<<<< snap/snapcraft.yaml
35+=======
36+ twisted-plugins:
37+ plugin: dump
38+ source: twisted/plugins
39+ organize:
40+ maasrackd.py: usr/lib/python3/dist-packages/twisted/plugins/maasrackd.py
41+ maasregiond.py: usr/lib/python3/dist-packages/twisted/plugins/maasregiond.py
42+>>>>>>> snap/snapcraft.yaml
43 snap:
44 plugin: dump
45 source: snap
46diff --git a/src/maasserver/bootsources.py b/src/maasserver/bootsources.py
47index f71040f..1e11784 100644
48--- a/src/maasserver/bootsources.py
49+++ b/src/maasserver/bootsources.py
50@@ -26,7 +26,10 @@ from maasserver.models import (
51 Config,
52 Notification,
53 )
54+<<<<<<< src/maasserver/bootsources.py
55 from maasserver.models.timestampedmodel import now
56+=======
57+>>>>>>> src/maasserver/bootsources.py
58 from maasserver.utils import get_maas_user_agent
59 from maasserver.utils.orm import transactional
60 from maasserver.utils.threads import deferToDatabase
61diff --git a/src/maasserver/models/tests/test_userprofile.py b/src/maasserver/models/tests/test_userprofile.py
62index 8a0efac..4bf4730 100644
63--- a/src/maasserver/models/tests/test_userprofile.py
64+++ b/src/maasserver/models/tests/test_userprofile.py
65@@ -139,6 +139,7 @@ class UserProfileTest(MAASServerTestCase):
66 self.assertEqual(reload_object(node).owner, new_user)
67 self.assertEqual(reload_object(ipaddress).user, new_user)
68 self.assertEqual(reload_object(iprange).user, new_user)
69+<<<<<<< src/maasserver/models/tests/test_userprofile.py
70
71 def test_transfer_resources_missing_target_access(self):
72 user = factory.make_User()
73@@ -153,6 +154,8 @@ class UserProfileTest(MAASServerTestCase):
74 " resource pool(s)")
75 # owner didn't change
76 self.assertEqual(reload_object(node).owner, user)
77+=======
78+>>>>>>> src/maasserver/models/tests/test_userprofile.py
79
80 def test_manager_all_users(self):
81 users = set(factory.make_User() for _ in range(3))
82diff --git a/src/maasserver/rpc/boot.py b/src/maasserver/rpc/boot.py
83index 5b5f1de..9a51bf3 100644
84--- a/src/maasserver/rpc/boot.py
85+++ b/src/maasserver/rpc/boot.py
86@@ -280,10 +280,15 @@ def get_config(
87 osystem = configs['commissioning_osystem']
88 series = configs['commissioning_distro_series']
89 else:
90+<<<<<<< src/maasserver/rpc/boot.py
91 osystem = machine.get_osystem(
92 default=configs['default_osystem'])
93 series = machine.get_distro_series(
94 default=configs['default_distro_series'])
95+=======
96+ osystem = machine.get_osystem()
97+ series = machine.get_distro_series()
98+>>>>>>> src/maasserver/rpc/boot.py
99 # XXX: roaksoax LP: #1739761 - Since the switch to squashfs (and
100 # drop of iscsi), precise is no longer deployable. To address a
101 # squashfs image is made available allowing it to be deployed in
102diff --git a/src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js b/src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js
103index 0a48138..8f8743a 100644
104--- a/src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js
105+++ b/src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js
106@@ -1580,6 +1580,7 @@ describe("NodesListController", function() {
107 it("sets showing_confirmation with testOptions",
108 function() {
109 var controller = makeController();
110+<<<<<<< src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js
111 var object = makeObject("machines");
112 object.status_code = 6;
113 var spy = spyOn(
114@@ -1594,6 +1595,22 @@ describe("NodesListController", function() {
115 true);
116 expect($scope.tabs[
117 "machines"].actionProgress.affected_nodes).toBe(1);
118+=======
119+ var object = makeObject("nodes");
120+ object.status_code = 6;
121+ var spy = spyOn(
122+ $scope.tabs.nodes.manager,
123+ "performAction").and.returnValue(
124+ $q.defer().promise);
125+ $scope.tabs.nodes.actionOption = { name: "test" };
126+ $scope.tabs.nodes.selectedItems = [object];
127+ $scope.actionGo("nodes");
128+ expect($scope.tabs[
129+ "nodes"].actionProgress.showing_confirmation).toBe(
130+ true);
131+ expect($scope.tabs[
132+ "nodes"].actionProgress.affected_nodes).toBe(1);
133+>>>>>>> src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js
134 expect(spy).not.toHaveBeenCalled();
135 });
136
137diff --git a/src/maasserver/static/partials/ipranges.html b/src/maasserver/static/partials/ipranges.html
138index 897d959..3d48a68 100755
139--- a/src/maasserver/static/partials/ipranges.html
140+++ b/src/maasserver/static/partials/ipranges.html
141@@ -15,6 +15,7 @@
142 <tbody>
143 <tr data-ng-repeat="iprange in (subnetIPRanges = ipranges | filterBySubnetOrVlan:subnet:vlan) | orderBy:ipRangeSort"
144 data-ng-class="{ 'is-active': isIPRangeInEditMode(iprange) || isIPRangeInDeleteMode(iprange)}">
145+<<<<<<< src/maasserver/static/partials/ipranges.html
146 <td class="col-2" aria-label="Start IP Address">{$ iprange.start_ip $}</td>
147 <td class="col-2" aria-label="End IP Address">{$ iprange.end_ip $}</td>
148 <td class="col-1" aria-label="Owner">{$ iprange.type == "dynamic" ? "MAAS" : iprange.user $}</td>
149@@ -26,6 +27,22 @@
150 data-ng-click="toggleMenu(); ipRangeToggleEditMode(iprange)">Edit</button>
151 <button class="table__controls-action u-text--error" aria-label="Remove range"
152 data-ng-click="toggleMenu(); ipRangeEnterDeleteMode(iprange)">Remove</button>
153+=======
154+ <div class="table__data table-col--20" aria-label="Start IP Address">{$ iprange.start_ip $}</div>
155+ <div class="table__data table-col--20" aria-label="End IP Address">{$ iprange.end_ip $}</div>
156+ <div class="table__data table-col--10" aria-label="Owner">{$ iprange.type == "dynamic" ? "MAAS" : iprange.user $}</div>
157+ <div class="table__data table-col--10" aria-label="Type">{$ iprange.type == "dynamic" ? "Dynamic" : "Reserved" $}</div>
158+ <div class="table__data table-col--31" aria-label="Comment">{$ iprange.type == "dynamic" ? "Dynamic" : iprange.comment $}</div>
159+ <div class="table__data table-col--9 table--mobile-controls">
160+ <div class="table__controls" toggle-ctrl data-ng-if="ipRangeCanBeModified(iprange)">
161+ <button class="table__controls-toggle" data-ng-click="toggleMenu()">View actions</button>
162+ <div class="table__controls-menu" role="menu" data-ng-show="isToggled">
163+ <button class="table__controls-action" aria-label="Edit row"
164+ data-ng-click="toggleMenu(); ipRangeToggleEditMode(iprange)">Edit reserved range</button>
165+ <button class="table__controls-action u-text--error" aria-label="Remove"
166+ data-ng-click="toggleMenu(); ipRangeEnterDeleteMode(iprange)">Remove range</button>
167+ </div>
168+>>>>>>> src/maasserver/static/partials/ipranges.html
169 </div>
170 </td>
171 <td class="is-active p-table-expanding__panel col-12" col-span="6" data-ng-if="isIPRangeInDeleteMode(iprange)">
172diff --git a/src/maasserver/static/partials/node-details.html b/src/maasserver/static/partials/node-details.html
173index 33688a5..6f4de0f 100755
174--- a/src/maasserver/static/partials/node-details.html
175+++ b/src/maasserver/static/partials/node-details.html
176@@ -98,6 +98,7 @@
177 </ul>
178 </div>
179
180+<<<<<<< src/maasserver/static/partials/node-details.html
181 <div class="col-8" data-ng-show="action.option.name === 'deploy' || action.option.name === 'release'">
182 <div class="ng-hide" data-ng-show="action.option.name === 'deploy'">
183 <div class="p-form__group">
184@@ -236,6 +237,139 @@
185 </div>
186 </div>
187 </div>
188+=======
189+ <!-- XXX blake_r 2015-02-19 - Need to add e2e test. -->
190+ <div class="page-header__dropdown" data-ng-class="{ 'is-open': action.option }">
191+ <div class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-if="!isDevice && !node.dhcp_on">
192+ <p class="page-header__message page-header__message--warning">MAAS is not providing DHCP.</p>
193+ </div>
194+
195+ <!-- XXX blake_r 2015-02-19 - Need to add e2e test. -->
196+ <div class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-hide="isActionError() || isDeployError() || isSSHKeyError() || hasActionPowerError(action.option.name)">
197+ <form class="form form--inline u-display--inline">
198+ <fieldset class="form__fieldset ng-hide" data-ng-show="action.option.name === 'commission' || action.option.name === 'test'">
199+ <div class="form__group">
200+ <input class="checkbox" id="enableSSH" type="checkbox"
201+ data-ng-model="commissionOptions.enableSSH">
202+ <label for="enableSSH">Allow SSH access and prevent machine from powering off</label>
203+ </div>
204+ </fieldset>
205+ <fieldset class="form__fieldset ng-hide" data-ng-show="action.option.name === 'commission'">
206+ <div class="form__group">
207+ <input class="checkbox" id="skipNetworking" type="checkbox"
208+ data-ng-model="commissionOptions.skipNetworking">
209+ <label for="skipNetworking">Retain network configuration</label>
210+ </div>
211+ <div class="form__group">
212+ <input class="checkbox" id="skipStorage" type="checkbox"
213+ data-ng-model="commissionOptions.skipStorage">
214+ <label for="skipStorage">Retain storage configuration</label>
215+ </div>
216+ </fieldset>
217+ <fieldset class="form__fieldset ng-hide" data-ng-show="action.option.name === 'deploy'">
218+ <div class="form__group">
219+ <label class="form__group-label">Choose your image</label>
220+ <div class="form__group-input" data-maas-os-select="osinfo" data-ng-model="osSelection"></div>
221+ </div>
222+ </fieldset>
223+ <fieldset class="form__fieldset" data-ng-if="action.option.name === 'release'">
224+ <div class="form__group">
225+ <div data-maas-release-options="releaseOptions"></div>
226+ </div>
227+ </fieldset>
228+ <div class="page-header__controls" data-ng-if="action.option.name !== 'commission' && action.option.name !== 'test'">
229+ <button class="button--base button--inline" data-ng-click="actionCancel()">Cancel</button>
230+ <button class="button--inline" data-ng-class="action.option.name === 'delete' ? 'button--destructive' : 'button--positive'" data-ng-click="actionGo('nodes')" data-ng-hide="hasActionsFailed('nodes')">
231+ <span data-ng-if="action.option.name === 'acquire'">Acquire {$ type_name $}</span>
232+ <span data-ng-if="action.option.name === 'deploy'">Deploy {$ type_name $}</span>
233+ <span data-ng-if="action.option.name === 'release'">Release {$ type_name $}</span>
234+ <span data-ng-if="action.option.name === 'set-zone'">Set zone for {$ type_name $}</span>
235+ <span data-ng-if="action.option.name === 'on'">Power on {$ type_name $}</span>
236+ <span data-ng-if="action.option.name === 'off'">Power off {$ type_name $}</span>
237+ <span data-ng-if="action.option.name === 'abort'">Abort action on {$ type_name $}</span>
238+ <span data-ng-if="action.option.name === 'rescue-mode'">Rescue {$ type_name $}</span>
239+ <span data-ng-if="action.option.name === 'exit-rescue-mode'">Exit rescue mode</span>
240+ <span data-ng-if="action.option.name === 'mark-broken'">Mark {$ type_name $}</span>
241+ <span data-ng-if="action.option.name === 'mark-fixed'">Mark {$ type_name $}</span>
242+ <span data-ng-if="action.option.name === 'override-failed-testing'">Override failed testing</span>
243+ <span data-ng-if="action.option.name === 'delete'">Delete {$ type_name $}</span>
244+ <span data-ng-if="action.option.name === 'import-images'">Import images</span>
245+ </button>
246+ </div>
247+ </form>
248+ </div>
249+ <div class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-hide="isActionError() || isDeployError() || isSSHKeyError() || hasActionPowerError(action.option.name) || action.option.name !== 'commission'" data-ng-if="hasCustomCommissioningScripts()">
250+ <form class="form form--stack">
251+ <fieldset class="form__fieldset eight-col u-margin--bottom-small">
252+ <div class="form__group">
253+ <label for="commissioning-scripts">Additional commissioning scripts</label>
254+ <span id="commissioning-scripts" data-maas-script-select="script" data-script-type="0" data-ng-model="commissioningSelection" class="tags--inline"></span>
255+ </div>
256+ </fieldset>
257+ </form>
258+ </div>
259+ <div class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-hide="isActionError() || isDeployError() || isSSHKeyError() || hasActionPowerError(action.option.name) || (action.option.name !== 'commission' && action.option.name !== 'test')">
260+ <form class="form form--stack">
261+ <fieldset class="form__fieldset eight-col u-margin--bottom-small">
262+ <div class="form__group">
263+ <label>Hardware tests</label>
264+ <span id="testing-scripts" data-maas-script-select="script" data-script-type="2" data-ng-model="testSelection" class="tags--inline"></span>
265+ </div>
266+ </fieldset>
267+ </form>
268+ </div>
269+ <div class="page-header__section twelve-col u-margin--bottom-none" data-ng-hide="isActionError() || isDeployError() || isSSHKeyError() || hasActionPowerError(action.option.name)" data-ng-if="action.option.name === 'commission' || action.option.name === 'test'">
270+ <form class="form form--inline">
271+ <div class="page-header__controls">
272+ <button class="button--base button--inline" data-ng-click="actionCancel()">Cancel</button>
273+ <button class="button--inline" data-ng-class="action.option.name === 'delete' ? 'button--destructive' : 'button--positive'" data-ng-click="actionGo('nodes')" data-ng-hide="hasActionsFailed('nodes')">
274+ <span data-ng-if="action.option.name === 'commission'">Commission {$ type_name $}</span>
275+ <span data-ng-if="action.option.name === 'test'">Test {$ type_name $}</span>
276+ </button>
277+ </div>
278+ </form>
279+ </div>
280+ <div class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-show="action.showing_confirmation && action.option.name === 'test'">
281+ <p class="page-header__message page-header__message--warning">
282+ Node is currently deployed. Are you sure you want to continue to test hardware?
283+ </p>
284+ <div class="page-header__controls">
285+ <button class="button--base button--inline" data-ng-click="actionCancel()">No</button>
286+ <button class="button--secondary button--inline" data-ng-click="actionGo()">Yes</button>
287+ </div>
288+ </div>
289+ <!-- XXX blake_r 2015-02-19 - Need to add e2e test. -->
290+ <div class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-show="isActionError()">
291+ <p class="page-header__message page-header__message--error">
292+ Node failed to be {$ action.option.sentence $}, because of the following error: {$ action.error $}
293+ </p>
294+ <div class="page-header__controls">
295+ <button class="button--base button--inline" data-ng-click="actionCancel()">Cancel</button>
296+ <button class="button--secondary button--inline" data-ng-click="actionGo()">Retry</button>
297+ </div>
298+ </div>
299+
300+ <!-- XXX blake_r 2015-02-19 - Need to add e2e test. -->
301+ <div class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-show="isDeployError()">
302+ <p class="page-header__message page-header__message--error">
303+ Node cannot be {$ action.option.sentence $}, because the required boot images have not been imported. To import boot images, visit the <a href="images/">images page</a>.
304+ </p>
305+ <div class="page-header__controls">
306+ <button class="button--base button--inline" data-ng-click="actionCancel()">Cancel</button>
307+ </div>
308+ </div>
309+
310+ <div class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-show="hasActionPowerError(action.option.name)">
311+ <p class="page-header__message page-header__message--error">
312+ Node cannot be {$ action.option.sentence $}, because power control software for the
313+ node is missing from its rack controller. To proceed, install the
314+ {$ getPowerErrors() $} on the rack controller.
315+ </p>
316+ <div class="page-header__controls">
317+ <button class="button--base button--inline" data-ng-click="actionCancel()">Cancel</button>
318+ </div>
319+ </div>
320+>>>>>>> src/maasserver/static/partials/node-details.html
321
322 <nav class="p-tabs">
323 <ul class="p-tabs__list" role="tablist" data-ng-class="{ 'u-hide': action.option }">
324diff --git a/src/maasserver/static/partials/nodes-list.html b/src/maasserver/static/partials/nodes-list.html
325index 8952e21..71f4b64 100644
326--- a/src/maasserver/static/partials/nodes-list.html
327+++ b/src/maasserver/static/partials/nodes-list.html
328@@ -1,3 +1,4 @@
329+<<<<<<< src/maasserver/static/partials/nodes-list.html
330 <header class="p-strip--light is-shallow is-bordered page-header" media-query="min-width: 769px">
331 <div class="row">
332 <div class="col-8">
333@@ -1047,3 +1048,1019 @@ sudo maas-rack register --url {$ tabs.controllers.registerUrl $} --secret {$ tab
334 on-check-all="toggleCheckAll('switches')" on-check="toggleChecked($switch_, 'switches')"></maas-switches-table>
335 </div>
336 </div>
337+=======
338+<header class="page-header u-margin--bottom-none" sticky media-query="min-width: 769px">
339+ <div class="wrapper--inner">
340+ <!-- XXX ricgard 2016-06-16 - Need to add e2e test. -->
341+ <h1 class="page-header__title">Nodes</h1>
342+
343+ <!-- XXX ricgard 2016-06-16 - Need to add e2e test. -->
344+ <p class="page-header__status" data-ng-show="loading"><span class="u-text--loading"><i class="icon icon--loading u-animation--spin"></i> Loading...</span></p>
345+
346+ <!-- XXX ricgard 2016-06-16 - Need to add e2e test. -->
347+ <div class="page-header__controls u-float--right ng-hide" data-ng-show="currentpage === 'nodes'">
348+ <div data-ng-hide="tabs.nodes.selectedItems.length">
349+ <div data-maas-cta="addHardwareOptions"
350+ data-ng-model="addHardwareOption"
351+ data-ng-change="addHardwareOptionChanged()" data-default-title="Add hardware">
352+ </div>
353+ </div>
354+ <div class="ng-hide" data-ng-show="tabs.nodes.selectedItems.length">
355+ <a class="u-display--inline u-margin--right" data-ng-click="showSelected('nodes')">{$ tabs.nodes.selectedItems.length $} Selected</a>
356+ <div data-maas-cta="tabs.nodes.takeActionOptions"
357+ data-ng-model="tabs.nodes.actionOption"
358+ data-ng-change="actionOptionSelected('nodes')">
359+ </div>
360+ </div>
361+ </div>
362+
363+ <!-- XXX ricgard 2016-06-16 - Need to add e2e test. -->
364+ <div class="page-header__controls u-float--rightng-hide" data-ng-show="currentpage === 'devices'">
365+ <div data-ng-hide="tabs.devices.selectedItems.length">
366+ <button class="button--secondary button--inline"
367+ data-ng-click="addDevice()"
368+ data-ng-hide="addDeviceScope.viewable">Add device</button>
369+ <button class="button--secondary button--inline ng-hide"
370+ data-ng-click="cancelAddDevice()"
371+ data-ng-show="addDeviceScope.viewable">Cancel add device</button>
372+ </div>
373+ <div data-ng-show="tabs.devices.selectedItems.length">
374+ <a class="u-display--inline u-margin--right" data-ng-click="showSelected('devices')">{$ tabs.devices.selectedItems.length $} Selected</a>
375+ <div data-maas-cta="tabs.devices.takeActionOptions"
376+ data-ng-model="tabs.devices.actionOption"
377+ data-ng-change="actionOptionSelected('devices')">
378+ </div>
379+ </div>
380+ </div>
381+
382+ <!-- XXX ricgard 2016-06-16 - Need to add e2e test. -->
383+ <div class="page-header__controls u-float--rightng-hide" data-ng-show="currentpage === 'controllers'">
384+ <div data-ng-if="!tabs.controllers.selectedItems.length">
385+ <button class="button--secondary button--inline"
386+ data-ng-click="tabs.controllers.addController = true"
387+ data-ng-hide="tabs.controllers.addController">Add rack controller</button>
388+ <button class="button--secondary button--inline ng-hide"
389+ data-ng-click="tabs.controllers.addController = false"
390+ data-ng-show="tabs.controllers.addController">Close add rack controller</button>
391+ </div>
392+ <div data-ng-show="tabs.controllers.selectedItems.length">
393+ <a class="u-display--inline u-margin--right" data-ng-click="showSelected('controllers')">{$ tabs.controllers.selectedItems.length $} Selected</a>
394+ <div data-maas-cta="tabs.controllers.takeActionOptions"
395+ data-ng-model="tabs.controllers.actionOption"
396+ data-ng-change="actionOptionSelected('controllers')">
397+ </div>
398+ </div>
399+ </div>
400+
401+ <div class="page-header__controls u-float--right ng-hide" data-ng-show="currentpage === 'switches'">
402+ <div class="ng-hide" data-ng-show="tabs.switches.selectedItems.length">
403+ <a class="u-display--inline u-margin--right" data-ng-click="showSelected('switches')">{$ tabs.switches.selectedItems.length $} Selected</a>
404+ <div data-maas-cta="tabs.switches.takeActionOptions"
405+ data-ng-model="tabs.switches.actionOption"
406+ data-ng-change="actionOptionSelected('switches')">
407+ </div>
408+ </div>
409+ </div>
410+
411+ <div data-ng-repeat="tab in ['nodes', 'switches']" data-ng-show="currentpage == tab">
412+ <!-- XXX ricgard 2016-06-16 - Need to add e2e test. -->
413+ <div class="page-header__dropdown" data-ng-class="{ 'is-open': tabs[tab].actionOption }">
414+ <!-- XXX ricgard 2016-06-16 - Need to add e2e test. -->
415+ <section class="page-header__section twelve-col u-margin--bottom-none" data-ng-hide="isActionError(tab) || hasActionsInProgress(tab)">
416+ <form class="form form--inline u-display--inline">
417+ <fieldset class="form__fieldset ng-hide" data-ng-show="tabs[tab].actionOption.name === 'commission' || tabs[tab].actionOption.name === 'test'">
418+ <div class="form__group">
419+ <input class="form__group-label" id="{$ tab $}-enableSSH" type="checkbox"
420+ data-ng-model="tabs[tab].commissionOptions.enableSSH">
421+ <label class="checkbox-label" for="{$ tab $}-enableSSH">Allow SSH access and prevent machine from powering off</label>
422+ </div>
423+ </fieldset>
424+ <fieldset class="form__fieldset ng-hide" data-ng-show="tabs[tab].actionOption.name === 'commission'">
425+ <div class="form__group">
426+ <input class="form__group-label" id="{$ tab $}-skipNetworking" type="checkbox"
427+ data-ng-model="tabs[tab].commissionOptions.skipNetworking">
428+ <label class="checkbox-label" for="{$ tab $}-skipNetworking">Retain network configuration</label>
429+ </div>
430+ <div class="form__group">
431+ <input class="form__group-label" id="{$ tab $}-skipStorage" type="checkbox"
432+ data-ng-model="tabs[tab].commissionOptions.skipStorage">
433+ <label class="checkbox-label" for="{$ tab $}-skipStorage">Retain storage configuration</label>
434+ </div>
435+ </fieldset>
436+ <fieldset class="form__fieldset ng-hide" data-ng-show="tabs[tab].actionOption.name === 'deploy'">
437+ <div class="form__group">
438+ <label for="image" class="form__group-label">Choose your image</label>
439+ <div class="form__group-input" data-maas-os-select="osinfo" data-ng-model="tabs[tab].osSelection"></div>
440+ </div>
441+ </fieldset>
442+ <fieldset class="form__fieldset" data-ng-if="tabs[tab].actionOption.name === 'release'">
443+ <div class="form__group">
444+ <div data-maas-release-options="tabs[tab].releaseOptions"></div>
445+ </div>
446+ </fieldset>
447+ <fieldset class="form__fieldset ng-hide" data-ng-show="tabs[tab].actionOption.name === 'set-zone'">
448+ <div class="form__group">
449+ <label for="{$ tab $}-zone3" class="form__group-label">Select Zone</label>
450+ <div class="form__group-input">
451+ <select name="zone" id="{$ tab $}-zone3"
452+ data-ng-model="tabs[tab].zoneSelection"
453+ data-ng-options="zone as zone.name for zone in zones">
454+ <option value="" disabled="disabled">Choose a zone</option>
455+ </select>
456+ </div>
457+ </div>
458+ </fieldset>
459+ </form>
460+ <div class="page-header__controls" data-ng-if="tabs[tab].actionOption.name !== 'commission' && tabs[tab].actionOption.name !== 'test'">
461+ <button class="button--base button--inline" data-ng-click="actionCancel(tab)">Cancel</button>
462+ <button class="button--inline" data-ng-class="tabs[tab].actionOption.name === 'delete' ? 'button--destructive' : 'button--positive'" data-ng-click="actionGo(tab)" data-ng-hide="hasActionsFailed(tab)">
463+ <span data-ng-if="tabs[tab].actionOption.name === 'acquire'">Acquire {$ tabs[tab].selectedItems.length $}
464+ <span data-ng-pluralize count="tabs[tab].selectedItems.length" when="{'one': 'machine', 'other': 'machines'}"></span>
465+ </span>
466+ <span data-ng-if="tabs[tab].actionOption.name === 'deploy'">Deploy {$ tabs[tab].selectedItems.length $}
467+ <span data-ng-pluralize count="tabs[tab].selectedItems.length" when="{'one': 'machine', 'other': 'machines'}"></span>
468+ </span>
469+ <span data-ng-if="tabs[tab].actionOption.name === 'release'">Release {$ tabs[tab].selectedItems.length $}
470+ <span data-ng-pluralize count="tabs[tab].selectedItems.length" when="{'one': 'machine', 'other': 'machines'}"></span>
471+ </span>
472+ <span data-ng-if="tabs[tab].actionOption.name === 'set-zone'">Set zone for {$ tabs[tab].selectedItems.length $}
473+ <span data-ng-pluralize count="tabs[tab].selectedItems.length" when="{'one': 'machine', 'other': 'machines'}"></span>
474+ </span>
475+ <span data-ng-if="tabs[tab].actionOption.name === 'on'">Power on {$ tabs[tab].selectedItems.length $}
476+ <span data-ng-pluralize count="tabs[tab].selectedItems.length" when="{'one': 'machine', 'other': 'machines'}"></span>
477+ </span>
478+ <span data-ng-if="tabs[tab].actionOption.name === 'off'">Power off {$ tabs[tab].selectedItems.length $}
479+ <span data-ng-pluralize count="tabs[tab].selectedItems.length" when="{'one': 'machine', 'other': 'machines'}"></span>
480+ </span>
481+ <span data-ng-if="tabs[tab].actionOption.name === 'abort'">Abort action for {$ tabs[tab].selectedItems.length $}
482+ <span data-ng-pluralize count="tabs[tab].selectedItems.length" when="{'one': 'machine', 'other': 'machines'}"></span>
483+ </span>
484+ <span data-ng-if="tabs[tab].actionOption.name === 'rescue-mode'">Set rescue mode for {$ tabs[tab].selectedItems.length $}
485+ <span data-ng-pluralize count="tabs[tab].selectedItems.length" when="{'one': 'machine', 'other': 'machines'}"></span>
486+ </span>
487+ <span data-ng-if="tabs[tab].actionOption.name === 'exit-rescue-mode'">Exit rescue mode for {$ tabs[tab].selectedItems.length $}
488+ <span data-ng-pluralize count="tabs[tab].selectedItems.length" when="{'one': 'machine', 'other': 'machines'}"></span>
489+ </span>
490+ <span data-ng-if="tabs[tab].actionOption.name === 'mark-broken'">Mark {$ tabs[tab].selectedItems.length $}
491+ <span data-ng-pluralize count="tabs[tab].selectedItems.length" when="{'one': 'machine', 'other': 'machines'}"></span> as broken
492+ </span>
493+ <span data-ng-if="tabs[tab].actionOption.name === 'mark-fixed'">Mark {$ tabs[tab].selectedItems.length $}
494+ <span data-ng-pluralize count="tabs[tab].selectedItems.length" when="{'one': 'machine', 'other': 'machines'}"></span> as fixed
495+ </span>
496+ <span data-ng-if="tabs[tab].actionOption.name === 'override-failed-testing'">Override failed testing on {$ tabs[tab].selectedItems.length $}
497+ <span data-ng-pluralize count="tabs[tab].selectedItems.length" when="{'one': 'machine', 'other': 'machines'}"></span>
498+ </span>
499+ <span data-ng-if="tabs[tab].actionOption.name === 'delete'">Delete {$ tabs[tab].selectedItems.length $}
500+ <span data-ng-pluralize count="tabs[tab].selectedItems.length" when="{'one': 'machine', 'other': 'machines'}"></span>
501+ </span>
502+ </button>
503+ <button class="button--secondary button--inline" data-ng-click="actionGo(tab)" data-ng-show="hasActionsFailed(tab)">Retry</button>
504+ </div>
505+ </section>
506+ <section class="page-header__section twelve-col u-margin--bottom-none" data-ng-hide="isActionError(tab) || hasActionsInProgress(tab)" data-ng-if="tabs[tab].actionOption.name === 'commission' && hasCustomCommissioningScripts()">
507+ <form class="form form--stack">
508+ <fieldset class="form__fieldset eight-col u-margin--bottom-small">
509+ <div class="form__group">
510+ <label for="{$ tab $}-commissiong-scripts">Additional commissioning Scripts</label>
511+ <span id="{$ tab $}-commissioning-scripts" data-maas-script-select="script" data-script-type="0" data-ng-model="tabs[tab].commissioningSelection" class="tags--inline"></span>
512+ </div>
513+ </fieldset>
514+ </form>
515+ </section>
516+ <section class="page-header__section twelve-col u-margin--bottom-none" data-ng-hide="isActionError(tab) || hasActionsInProgress(tab)" data-ng-if="tabs[tab].actionOption.name === 'commission' || tabs[tab].actionOption.name ==='test'">
517+ <form class="form form--stack">
518+ <fieldset class="form__fieldset eight-col u-margin--bottom-small">
519+ <div class="form__group">
520+ <label for="{$ tab $}-testing-scripts">Hardware Tests</label>
521+ <span id="{$ tab $}-testing-scripts" data-maas-script-select="script" data-script-type="2" data-ng-model="tabs[tab].testSelection" class="tags--inline"></span>
522+ </div>
523+ </fieldset>
524+ </form>
525+ </section>
526+ <section class="page-header__section twelve-col u-margin--bottom-none" data-ng-hide="isActionError(tab) || hasActionsInProgress(tab)" data-ng-if="tabs[tab].actionOption.name === 'commission' || tabs[tab].actionOption.name === 'test'">
527+ <div class="page-header__controls">
528+ <button class="button--base button--inline" data-ng-click="actionCancel(tab)">Cancel</button>
529+ <button class="button--inline" data-ng-class="tabs[tab].actionOption.name === 'delete' ? 'button--destructive' : 'button--positive'" data-ng-click="actionGo(tab)" data-ng-hide="hasActionsFailed(tab)">
530+ <span data-ng-if="tabs[tab].actionOption.name === 'commission'">Commission {$ tabs[tab].selectedItems.length $}
531+ <span data-ng-pluralize count="tabs[tab].selectedItems.length" when="{'one': 'machine', 'other': 'machines'}"></span>
532+ </span>
533+ <span data-ng-if="tabs[tab].actionOption.name === 'test'">Test {$ tabs[tab].selectedItems.length $}
534+ <span data-ng-pluralize count="tabs[tab].selectedItems.length" when="{'one': 'machine', 'other': 'machines'}"></span>
535+ </span>
536+ </button>
537+ <button class="button--secondary button--inline" data-ng-click="actionGo(tab)" data-ng-show="hasActionsFailed(tab)">Retry</button>
538+ </div>
539+ </section>
540+
541+ <!-- XXX ricgard 2016-06-16 - Need to add e2e test. -->
542+ <section class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-show="isActionError(tab)">
543+ <p data-ng-hide="isDeployError(tab) || isSSHKeyError(tab)"
544+ class="page-header__message page-header__message--error ng-hide">
545+ {$ tabs[tab].actionErrorCount $}
546+ <span data-ng-pluralize count="tabs[tab].selectedItems.length" when="{'one': 'node', 'other': 'nodes'}"></span>
547+ cannot be {$ tabs[tab].actionOption.sentence $}. To proceed, update your selection.
548+ </p>
549+ <p class="page-header__message page-header__message--error ng-hide" data-ng-show="isDeployError(tab)">
550+ {$ tabs[tab].selectedItems.length $}
551+ <span data-ng-pluralize count="tabs[tab].selectedItems.length" when="{'one': 'node', 'other': 'nodes'}"></span>
552+ cannot be {$ tabs[tab].actionOption.sentence $}, because the required boot images have not been imported. To import boot images, visit the <a href="#/images">images page</a>.
553+ </p>
554+ <p class="page-header__message page-header__message--error ng-hide" data-ng-show="isSSHKeyError(tab)">
555+ {$ tabs[tab].selectedItems.length $}
556+ <span data-ng-pluralize count="tabs[tab].selectedItems.length" when="{'one': 'node', 'other': 'nodes'}"></span>
557+ cannot be {$ tabs[tab].actionOption.sentence $}, because an SSH key has not been added to your account. To add an SSH key, visit <a href="account/prefs/">your account page</a>.
558+ </p>
559+ </section>
560+ <section class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-show="tabs[tab].actionProgress.showing_confirmation">
561+ <p class="page-header__message page-header__message--warning">
562+ {$ tabs[tab].actionProgress.affected_nodes $} of {$ tabs[tab].selectedItems.length $} are in a deployed state. Are you sure you want to continue to test hardware?
563+ </p>
564+ <div class="page-header__controls">
565+ <button class="button--base button--inline" data-ng-click="actionCancel(tab)">No</button>
566+ <button class="button--secondary button--inline" data-ng-click="actionGo(tab)">Yes</button>
567+ </div>
568+ </section>
569+ <!-- XXX ricgard 2016-06-16 - Need to add e2e test. -->
570+ <section class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-show="hasActionsInProgress(tab) || hasActionsFailed(tab)">
571+ <p class="page-header__message" data-ng-show="hasActionsInProgress(tab)">
572+ <i class="icon icon--loading u-animation--spin"></i>
573+ {$ tabs[tab].actionProgress.completed $} of {$ tabs[tab].actionProgress.total $}
574+ nodes are transitioning to {$ tabs[tab].actionOption.sentence $}.
575+ </p>
576+ <p class="page-header__message page-header__message--error"
577+ data-ng-repeat="(error, nodes) in tabs[tab].actionProgress.errors">
578+ The {$ tabs[tab].actionOption.title.toLowerCase() $} action for {$ nodes.length $}
579+ <span data-ng-pluralize count="nodes.length" when="{'one': 'node', 'other': 'nodes'}"></span>
580+ failed with error: {$ error $}
581+ </p>
582+ </section>
583+ </div>
584+
585+ </div>
586+
587+ <!-- XXX ricgard 2016-06-16 - Need to add e2e test. -->
588+ <div class="page-header__dropdown" data-ng-class="{ 'is-open': addHardwareScope.viewable }" data-ng-controller="AddHardwareController">
589+ <!-- XXX ricgard 2016-06-16 - Need to add e2e test. -->
590+ <section class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-show="showMachine()">
591+ <h3 class="page-header__dropdown-title">Add machine</h3>
592+ <form class="form form--stack">
593+ <fieldset class="form__fieldset six-col">
594+ <div class="form__group">
595+ <label for="machine-name" class="form__label two-col">Machine name</label>
596+ <div class="form__group-input three-col">
597+ <input type="text" id="machine-name" placeholder="Choose a machine name (optional)"
598+ data-ng-model="machine.name">
599+ </div>
600+ </div>
601+ <div class="form__group">
602+ <label for="domain" class="form__group-label two-col">Domain</label>
603+ <div class="form__group-input three-col">
604+ <select name="domain" id="domain"
605+ data-ng-model="machine.domain"
606+ data-ng-options="domain as domain.name for domain in domains">
607+ <option value="" disabled>Choose a domain</option>
608+ </select>
609+ </div>
610+ </div>
611+ <div class="form__group">
612+ <label for="architecture" class="form__group-label two-col">Architecture</label>
613+ <div class="form__group-input three-col">
614+ <select name="architecture" id="architecture"
615+ data-ng-model="machine.architecture"
616+ data-ng-options="arch for arch in architectures">
617+ <option value="" disabled>Choose an architecture</option>
618+ </select>
619+ </div>
620+ </div>
621+ <div class="form__group">
622+ <label for="min_hwe_kernel" class="form__group-label two-col">Minimum Kernel</label>
623+ <div class="form__group-input three-col">
624+ <select name="min_hwe_kernel" id="min_hwe_kernel"
625+ data-ng-model="machine.min_hwe_kernel"
626+ data-ng-options="hwe_kernel[0] as hwe_kernel[1] for hwe_kernel in hwe_kernels">
627+ <option value="">No minimum kernel</option>
628+ </select>
629+ </div>
630+ </div>
631+ <div class="form__group">
632+ <label for="zone4" class="form__group-label two-col">Zone</label>
633+ <select name="zone" id="zone4" class="three-col"
634+ data-ng-model="machine.zone"
635+ data-ng-options="zone as zone.name for zone in zones">
636+ </select>
637+ </div>
638+ <div class="form__group" data-ng-repeat="mac in machine.macs">
639+ <label for="mac-address" class="form__group-label two-col"><span data-ng-hide="mac !== machine.macs[0]">MAC Address</span>&nbsp;</label>
640+ <div class="form__group-input three-col">
641+ <input type="text" id="mac-address" placeholder="00:00:00:00:00:00"
642+ maxlength="17"
643+ data-ng-class="{ 'has-error': mac.error }"
644+ data-ng-model="mac.mac"
645+ data-ng-pattern="macAddressRegex"
646+ data-ng-change="validateMac(mac)"
647+ mac-address>
648+ <span class="form__group-remove" title="Delete MAC"
649+ data-ng-hide="mac === machine.macs[0]" data-ng-click="removeMac(mac)"></span>
650+ </div>
651+ </div>
652+ <div class="form__group">
653+ <div class="five-col">
654+ <button class="button--secondary button--inline u-float--right" data-ng-click="addMac()">+ Add MAC Address</a>
655+ </div>
656+ </div>
657+ </fieldset>
658+ <fieldset class="form__fieldset six-col last-col"
659+ data-maas-power-parameters="power_types"
660+ data-ng-model="machine.power">
661+ </fieldset>
662+ </form>
663+ </section>
664+ <section class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-show="showChassis()">
665+ <h3 class="page-header__dropdown-title">Add chassis</h3>
666+ <form action="post" class="form form--stack">
667+ <fieldset class="form__fieldset six-col"
668+ data-maas-power-parameters="chassisPowerTypes"
669+ data-ng-model="chassis.power">
670+ </fieldset>
671+ <fieldset class="form__fieldset six-col last-col">
672+ <div class="form__group">
673+ <label for="domain2" class="form__group-label two-col">Domain</label>
674+ <div class="form__group-input three-col">
675+ <select name="domain" id="domain2"
676+ data-ng-model="chassis.domain"
677+ data-ng-options="domain as domain.name for domain in domains">
678+ </select>
679+ </div>
680+ </div>
681+ </fieldset>
682+ </form>
683+ </section>
684+
685+ <!-- XXX ricgard 2016-06-16 - Need to add e2e test. -->
686+ <section class="page-header__section twelve-col u-margin--bottom-none" data-ng-show="showMachine()">
687+ <p class="page-header__message page-header__message--error ng-hide" data-ng-show="error">{$ error $}</p>
688+ <div class="page-header__controls">
689+ <button class="button--base button--inline" data-ng-click="cancel()">Cancel</button>
690+ <button class="button--secondary button--inline"
691+ data-ng-disabled="machineHasError()"
692+ data-ng-click="saveMachine(true)">Save and add another</button>
693+ <button class="button--positive button--inline"
694+ data-ng-disabled="machineHasError()"
695+ data-ng-click="saveMachine(false)">Save machine</button>
696+ </div>
697+ </section>
698+
699+ <!-- XXX ricgard 2016-06-16 - Need to add e2e test. -->
700+ <section class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-show="showChassis()">
701+ <p class="page-header__message page-header__message--error ng-hide" data-ng-show="error">{$ error $}</p>
702+ <div class="page-header__controls">
703+ <button class="button--base button--inline" data-ng-click="cancel()">Cancel</button>
704+ <button class="button--secondary button--inline"
705+ data-ng-disabled="chassisHasErrors()"
706+ data-ng-click="saveChassis(true)">Save and add another</button>
707+ <button class="button--positive button--inline"
708+ data-ng-disabled="chassisHasErrors()"
709+ data-ng-click="saveChassis(false)">Save chassis</button>
710+ </div>
711+ </section>
712+
713+ <!-- XXX ricgard 2016-06-16 - Need to add e2e test. -->
714+ <section class="page-header__section twelve-col u-margin--bottom-none" data-ng-hide="architectures.length">
715+ <p class="page-header__message page-header__message--error">
716+ Cannot add {$ mode $} until boot images have been imported. To fix, visit the <a href="#/images">images page</a>.
717+ </p>
718+ </section>
719+ </div>
720+
721+ <!-- XXX blake_r 2015-04-02 - Need to add e2e test. -->
722+ <div class="page-header__dropdown" data-ng-class="{ 'is-open': tabs.devices.actionOption }">
723+ <!-- XXX blake_r 2015-04-02 - Need to add e2e test. -->
724+ <div class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-hide="isActionError('devices') || hasActionsInProgress('devices')">
725+ <!-- XXX blake_r 2015-04-02 - Need to add e2e test. -->
726+ <form class="form form--inline u-display--inline">
727+ <fieldset class="form__fieldset ng-hide" data-ng-show="tabs.devices.actionOption.name === 'set-zone'">
728+ <div class="form__group">
729+ <label class="form__group-label" for="zone1">Set zone</label>
730+ <select name="zone" id="zone1"
731+ data-ng-model="tabs.devices.zoneSelection"
732+ data-ng-options="zone as zone.name for zone in zones">
733+ <option value="" disabled="disabled">Choose a zone</option>
734+ </select>
735+ </div>
736+ </fieldset>
737+ <div class="page-header__controls">
738+ <button class="button--base button--inline" data-ng-click="actionCancel('devices')">Cancel</button>
739+ <button class="button--positive button--inline" data-ng-click="actionGo('devices')">
740+ <span data-ng-if="tabs.devices.actionOption.name === 'set-zone'">Set zone for {$ tabs.devices.selectedItems.length $}
741+ <span data-ng-pluralize count="tabs.devices.selectedItems.length" when="{'one': 'device', 'other': 'devices'}"></span>
742+ </span>
743+ <span data-ng-if="tabs.devices.actionOption.name === 'delete'">Delete {$ tabs.devices.selectedItems.length $}
744+ <span data-ng-pluralize count="tabs.devices.selectedItems.length" when="{'one': 'device', 'other': 'devices'}"></span>
745+ </span>
746+ </button>
747+ </div>
748+ </form>
749+ </div>
750+
751+ <!-- XXX blake_r 2015-04-02 - Need to add e2e test. -->
752+ <div class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-show="isActionError('devices')">
753+ <p class="page-header__message page-header__message--error">
754+ {$ tabs.devices.actionErrorCount $}
755+ <span data-ng-pluralize count="tabs.devices.selectedItems.length" when="{'one': 'device', 'other': 'devices'}"></span>
756+ cannot be {$ tabs.devices.actionOption.sentence $}. To proceed, update your selection.
757+ </p>
758+ </div>
759+
760+ <!-- XXX blake_r 2015-05-07 - Need to add e2e test. -->
761+ <div class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-show="hasActionsInProgress('devices') || hasActionsFailed('devices')">
762+ <p class="page-header__message" data-ng-show="hasActionsInProgress('devices')">
763+ <i class="icon icon--loading u-animation--spin"></i>
764+ {$ tabs.devices.actionProgress.completed $} of {$ tabs.devices.actionProgress.total $}
765+ devices have been {$ tabs.devices.actionOption.sentence $}.
766+ </p>
767+ <p class="page-header__message page-header__message--error"
768+ data-ng-repeat="(error, devices) in tabs.devices.actionProgress.errors">
769+ The {$ tabs.devices.actionOption.title.toLowerCase() $} action for {$ devices.length $}
770+ <span data-ng-pluralize count="devices.length" when="{'one': 'device', 'other': 'devices'}"></span>
771+ failed with error: {$ error $}
772+ </p>
773+ </div>
774+ </div>
775+
776+ <!-- XXX ricgard 2016-06-16 - Need to add e2e test. -->
777+ <div class="page-header__dropdown" data-ng-class="{ 'is-open': addDeviceScope.viewable }" data-ng-controller="AddDeviceController">
778+ <section class="page-header__section twelve-col u-margin--bottom-none">
779+ <h3 class="page-header__dropdown-title">Add device</h3>
780+ <form class="form form--stack">
781+ <fieldset class="form__fieldset six-col">
782+ <div class="form__group">
783+ <label for="device-name" class="form__group-label two-col">Name</label>
784+ <div class="form__group-input three-col">
785+ <input type="text" id="device-name" placeholder="Name your device"
786+ data-ng-model="device.name"
787+ data-ng-class="{ 'has-error': nameHasError() }">
788+ </div>
789+
790+ </div>
791+ <div class="form__group">
792+ <label for="domain3" class="form__group-label two-col">Domain</label>
793+ <div class="form__group-input three-col">
794+ <select name="domain" id="domain3"
795+ data-ng-model="device.domain"
796+ data-ng-options="domain as domain.name for domain in domains">
797+ </select>
798+ </div>
799+ </div>
800+ </fieldset>
801+ </form>
802+ <table>
803+ <thead>
804+ <tr>
805+ <th class="table-col--20">MAC address</th>
806+ <th class="table-col--20">IP assignment</th>
807+ <th class="table-col--20">Subnet</th>
808+ <th class="table-col--35">IP address</th>
809+ <th class="table-col--5"></th>
810+ </tr>
811+ </thead>
812+ <tbody vs-repeat vs-scroll-parent="window">
813+ <tr data-ng-repeat="interface in device.interfaces">
814+ <td class="table-col--20" aria-label="MAC address">
815+ <input type="text" id="mac-address1" placeholder="00:00:00:00:00:00"
816+ data-ng-model="interface.mac"
817+ data-ng-class="{ 'has-error': macHasError(interface) }">
818+ </td>
819+ <td class="table-col--20" aria-label="IP assignment">
820+ <select name="ip-assignment" id="ip-assignment"
821+ data-ng-model="interface.ipAssignment"
822+ data-ng-options="assigment.title for assigment in ipAssignments">
823+ <option value="" disabled selected>Select IP assignment</option>
824+ </select>
825+ </td>
826+ <td class="table-col--20" aria-label="Subnet">
827+ <select name="subnet" id="subnet"
828+ data-ng-model="interface.subnetId"
829+ data-ng-options="subnet.id as subnet.name for subnet in subnets"
830+ data-ng-show="interface.ipAssignment.name === 'static'">
831+ <option value="" disabled selected>Select subnet</option>
832+ </select>
833+ </td>
834+ <td class="table-col--35" aria-label="IP address">
835+ <input type="text" id="ip-address" placeholder="000.000.000.000"
836+ data-ng-model="interface.ipAddress"
837+ data-ng-class="{ 'has-error': ipHasError(interface) }"
838+ data-ng-show="interface.ipAssignment.name === 'external'">
839+ <input type="text" id="ip-address"
840+ data-ng-model="interface.ipAddress"
841+ data-ng-class="{ 'has-error': ipHasError(interface) }"
842+ data-ng-show="interface.ipAssignment.name === 'static'">
843+ </td>
844+ <td class="table-col--5 u-u-align---right table--mobile-controls">
845+ <!-- space needed to set correct height -->
846+ <span data-ng-show="isPrimaryInterface(interface)">&nbsp;</span>
847+ <a title="Delete" class="icon icon--remove u-display--desktop"
848+ data-ng-click="deleteInterface(interface)"
849+ data-ng-hide="isPrimaryInterface(interface)">delete</a>
850+ <button class="button--secondary u-display--mobile"
851+ data-ng-click="deleteInterface(interface)"
852+ data-ng-hide="isPrimaryInterface(interface)">delete</a>
853+ </td>
854+ </tr>
855+ </tbody>
856+ </table>
857+ <button class="button--secondary button--inline" data-ng-click="addInterface()">+ Add another interface</button>
858+ </section>
859+ <section class="page-header__section twelve-col u-margin--bottom-none twelve-col u-margin--bottom-none">
860+ <form class="form form--inline">
861+ <p class="page-header__message page-header__message--error ng-hide" data-ng-show="error">{$ error $}</p>
862+ <div class="page-header__controls">
863+ <button class="button--base button--inline" data-ng-click="cancel()">Cancel</button>
864+ <button class="button--secondary button--inline"
865+ data-ng-class="{ disabled: deviceHasError() }"
866+ data-ng-click="save(true)">Save and add another</button>
867+ <button class="button--positive button--inline"
868+ data-ng-class="{ disabled: deviceHasError() }"
869+ data-ng-click="save(false)">Save device</button>
870+ </div>
871+ </form>
872+ </section>
873+ </div>
874+
875+ <!-- XXX blake_r 2015-04-02 - Need to add e2e test. -->
876+ <div class="page-header__dropdown" data-ng-class="{ 'is-open': tabs.controllers.actionOption }">
877+ <!-- XXX blake_r 2015-04-02 - Need to add e2e test. -->
878+ <div class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-hide="isActionError('controllers') || hasActionsInProgress('controllers')">
879+ <!-- XXX blake_r 2015-04-02 - Need to add e2e test. -->
880+ <form class="form form--inline u-display--inline">
881+ <fieldset class="form__fieldset ng-hide" data-ng-show="tabs.controllers.actionOption.name === 'set-zone'">
882+ <div class="form__group">
883+ <label for="zone2" class="form__group-label">Select Zone</label>
884+ <div class="form__group-input">
885+ <select name="zone" id="zone2"
886+ data-ng-model="tabs.controllers.zoneSelection"
887+ data-ng-options="zone as zone.name for zone in zones">
888+ <option value="" disabled="disabled">Choose a zone</option>
889+ </select>
890+ </div>
891+ </div>
892+ </fieldset>
893+ <fieldset class="form__fieldset ng-hide" data-ng-show="tabs.controllers.actionOption.name === 'test'">
894+ <div class="form__group">
895+ <input class="form__group-label" id="enable_SSH" type="checkbox"
896+ data-ng-model="tabs.controllers.commissionOptions.enableSSH">
897+ <label class="checkbox-label" for="enable_SSH">Allow SSH access and prevent machine from powering off</label>
898+ </div>
899+ </fieldset>
900+ </form>
901+ <div class="page-header__controls" data-ng-if="tabs.controllers.actionOption.name !== 'test'">
902+ <button class="button--base button--inline" data-ng-click="actionCancel('controllers')">Cancel</button>
903+ <button class="button--positive button--inline" data-ng-click="actionGo('controllers')">
904+ <span data-ng-if="tabs.controllers.actionOption.name === 'set-zone'">Set zone for {$ tabs.controllers.selectedItems.length $}
905+ <span data-ng-pluralize count="tabs.controllers.selectedItems.length" when="{'one': 'controller', 'other': 'controllers'}"></span>
906+ </span>
907+ <span data-ng-if="tabs.controllers.actionOption.name === 'on'">Power on {$ tabs.controllers.selectedItems.length $}
908+ <span data-ng-pluralize count="tabs.controllers.selectedItems.length" when="{'one': 'controller', 'other': 'controllers'}"></span>
909+ </span>
910+ <span data-ng-if="tabs.controllers.actionOption.name === 'off'">Power off {$ tabs.controllers.selectedItems.length $}
911+ <span data-ng-pluralize count="tabs.controllers.selectedItems.length" when="{'one': 'controller', 'other': 'controllers'}"></span>
912+ </span>
913+ <span data-ng-if="tabs.controllers.actionOption.name === 'delete'">Delete {$ tabs.controllers.selectedItems.length $}
914+ <span data-ng-pluralize count="tabs.controllers.selectedItems.length" when="{'one': 'controller', 'other': 'controllers'}"></span>
915+ </span>
916+ <span data-ng-if="tabs.controllers.actionOption.name === 'import-images'">Import images for {$ tabs.controllers.selectedItems.length $}
917+ <span data-ng-pluralize count="tabs.controllers.selectedItems.length" when="{'one': 'controller', 'other': 'controllers'}"></span>
918+ </span>
919+ </button>
920+ </div>
921+ </div>
922+ <div class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-hide="isActionError('controllers') || hasActionsInProgress('controllers')" data-ng-if="tabs.controllers.actionOption.name === 'test'">
923+ <form class="form form--stack">
924+ <fieldset class="form__fieldset eight-col u-margin--bottom-small">
925+ <div class="form__group">
926+ <label for="testing-scripts">Hardware Tests</label>
927+ <span id="testing-scripts" data-maas-script-select="script" data-script-type="2" data-ng-model="tabs.nodes.testSelection" class="tags--inline"></span>
928+ </div>
929+ </fieldset>
930+ </form>
931+ </div>
932+ <div class="page-header__section twelve-col u-margin--bottom-none" data-ng-hide="isActionError('controllers') || hasActionsInProgress('controllers')" data-ng-if="tabs.controllers.actionOption.name === 'test'">
933+ <div class="page-header__controls">
934+ <button class="button--base button--inline" data-ng-click="actionCancel('controllers')">Cancel</button>
935+ <button class="button--positive button--inline" data-ng-click="actionGo('controllers')">
936+ <span>Test {$ tabs.controllers.selectedItems.length $}
937+ <span data-ng-pluralize count="tabs.controllers.selectedItems.length" when="{'one': 'controller', 'other': 'controllers'}"></span>
938+ </span>
939+ </button>
940+ </div>
941+ </div>
942+
943+ <!-- XXX blake_r 2015-04-02 - Need to add e2e test. -->
944+ <div class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-show="isActionError('controllers')">
945+ <form class="form form--inline">
946+ <p class="page-header__message page-header__message--error">
947+ {$ tabs.controllers.actionErrorCount $}
948+ <span data-ng-pluralize count="tabs.controllers.selectedItems.length" when="{'one': 'controller', 'other': 'controllers'}"></span>
949+ cannot be {$ tabs.controllers.actionOption.sentence $}. To proceed, update your selection.
950+ </p>
951+ </form>
952+ </div>
953+
954+ <!-- XXX blake_r 2015-05-07 - Need to add e2e test. -->
955+ <div class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-show="hasActionsInProgress('controllers') || hasActionsFailed('controllers')">
956+ <form class="form form--inline">
957+ <p class="page-header__message" data-ng-show="hasActionsInProgress('controllers')">
958+ <i class="icon icon--loading u-animation--spin u-margin--right-small"></i>
959+ {$ tabs.controllers.actionProgress.completed $} of {$ tabs.controllers.actionProgress.total $}
960+ controllers have been {$ tabs.controllers.actionOption.sentence $}.
961+ </p>
962+ <p class="page-header__message page-header__message--error"
963+ data-ng-repeat="(error, controllers) in tabs.controllers.actionProgress.errors">
964+ The {$ tabs.controllers.actionOption.title.toLowerCase() $} action for {$ controllers.length $}
965+ <span data-ng-pluralize count="controllers.length" when="{'one': 'controller', 'other': 'controllers'}"></span>
966+ failed with error: {$ error $}
967+ </p>
968+ </form>
969+ </div>
970+ </div>
971+
972+ <div class="page-header__dropdown" data-ng-class="{ 'is-open': tabs.controllers.addController }">
973+ <!-- XXX blake_r 2015-05-07 - Need to add e2e test. -->
974+ <div class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-show="tabs.controllers.addController">
975+ <h3 class="page-header__dropdown-title">Add rack controller</h3>
976+ <pre class="u-margin--none">
977+ <code># To add a new rack controller, SSH into the rack controller.
978+# Install the maas-rack-controller package.
979+sudo apt install maas-rack-controller
980+# Register the rack controller with this MAAS. If the rack controller (and machines)
981+# don't have access to the URL, use a different IP address to allow connection.
982+sudo maas-rack register --url {$ tabs.controllers.registerUrl $} --secret {$ tabs.controllers.registerSecret $}</code>
983+ </pre>
984+ </div>
985+ <div class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-show="tabs.controllers.addController">
986+ <form class="form form--inline">
987+ <div class="page-header__controls">
988+ <button class="button--secondary button--inline"
989+ data-ng-click="tabs.controllers.addController = false">Close</button>
990+ </div>
991+ </form>
992+ </div>
993+ </div>
994+ </div>
995+</header>
996+<div sticky media-query="min-width: 769px" offset="89" class="page-navigation" data-ng-class="{ 'u-visibility--hidden': tabs.nodes.actionOption || addHardwareScope.viewable || tabs.devices.actionOption || addDeviceScope.viewable || tabs.controllers.actionOption || tabs.controllers.addController }">
997+ <div class="wrapper--inner">
998+ <nav class="page-navigation__links">
999+ <button class="page-navigation__link tooltip tooltip--right"
1000+ aria-label="A deployable node managed by MAAS."
1001+ data-ng-class="{ 'is-active': currentpage === 'nodes'}"
1002+ data-ng-click="toggleTab('nodes')">{$ nodes.length $} <ng-pluralize count="nodes.length" when="{'one': 'Machine', 'other': 'Machines'}"></ng-pluralize></button>
1003+ <button class="page-navigation__link tooltip"
1004+ aria-label="A node known to MAAS, but is not deployable."
1005+ data-ng-class="{ 'is-active': currentpage === 'devices'}"
1006+ data-ng-click="toggleTab('devices')">{$ devices.length $} <ng-pluralize count="devices.length" when="{'one': 'Device', 'other': 'Devices'}"></ng-pluralize></button>
1007+ <button class="page-navigation__link tooltip"
1008+ data-ng-if="isSuperUser()"
1009+ aria-label="A node that provides MAAS services."
1010+ data-ng-class="{ 'is-active': currentpage === 'controllers'}"
1011+ data-ng-click="toggleTab('controllers')">{$ controllers.length $} <ng-pluralize count="controllers.length" when="{'one': 'Controller', 'other': 'Controllers'}"></ng-pluralize></button>
1012+ <button class="page-navigation__link tooltip"
1013+ data-ng-if="showswitches"
1014+ aria-label="A node that is a network switch."
1015+ data-ng-class="{ 'is-active': currentpage === 'switches'}"
1016+ data-ng-click="toggleTab('switches')">{$ switches.length $} <ng-pluralize count="switches.length" when="{'one': 'Switch', 'other': 'Switches'}"></ng-pluralize></button>
1017+ </nav>
1018+ </div>
1019+</div>
1020+<div class="u-padding--top row">
1021+ <div class="wrapper--inner">
1022+ <maas-notifications></maas-notifications>
1023+ <aside class="three-col">
1024+ <div class="accordion maas-accordion ng-hide" data-ng-show="currentpage === 'nodes'"
1025+ data-ng-class="{ 'is-disabled': tabs.nodes.actionOption }">
1026+ <h3 class="accordion__title"
1027+ data-ng-class="{'is-active': isActive}"
1028+ data-ng-init="isActive = false"
1029+ data-ng-click="isActive = !isActive">Filter by</h3>
1030+ <div class="accordion__content">
1031+ <!-- XXX ricgard 2016-06-16 - Need to add e2e test. -->
1032+ <div class="ng-hide accordion__tab" data-ng-show="tabs.nodes.metadata.status.length">
1033+ <button class="accordion__tab-title maas-accordion-tab is-active">Status</button>
1034+ <div class="accordion__tab-content">
1035+ <ul class="accordion__tab-list">
1036+ <!-- XXX ricgard 2016-06-16 - Need to add e2e test. -->
1037+ <li class="accordion__tab-item" data-ng-repeat="status in tabs.nodes.metadata.status | orderBy:['name', '-count']" data-ng-class="{ 'is-active': isFilterActive('status', status.name, 'nodes') }">
1038+ <button class="accordion__tab-link" data-ng-click="toggleFilter('status', status.name, 'nodes')">{$ status.name $} ({$ status.count $})</button>
1039+ </li>
1040+ </ul>
1041+ </div>
1042+ </div>
1043+ <div class="ng-hide accordion__tab" data-ng-show="tabs.nodes.metadata.owner.length">
1044+ <button class="accordion__tab-title maas-accordion-tab">Owner</button>
1045+ <div class="accordion__tab-content">
1046+ <ul class="accordion__tab-list">
1047+ <li class="accordion__tab-item" data-ng-repeat="owner in tabs.nodes.metadata.owner | orderBy:['name', '-count']" data-ng-class="{ 'is-active': isFilterActive('owner', owner.name, 'nodes') }">
1048+ <button class="accordion__tab-link" data-ng-click="toggleFilter('owner', owner.name, 'nodes')">{$ owner.name $} ({$ owner.count $})</button>
1049+ </li>
1050+ </ul>
1051+ </div>
1052+ </div>
1053+ <div class="ng-hide accordion__tab" data-ng-show="tabs.nodes.metadata.architecture.length">
1054+ <button class="accordion__tab-title maas-accordion-tab">Architectures</button>
1055+ <div class="accordion__tab-content">
1056+ <ul class="accordion__tab-list">
1057+ <li class="accordion__tab-item" data-ng-repeat="architecture in tabs.nodes.metadata.architecture | orderBy:['name', '-count']" data-ng-class="{ 'is-active': isFilterActive('architecture', architecture.name, 'nodes') }">
1058+ <button class="accordion__tab-link" data-ng-click="toggleFilter('architecture', architecture.name, 'nodes')"><span data-maas-release-name="architecture.name"></span> ({$ architecture.count $})</button>
1059+ </li>
1060+ </ul>
1061+ </div>
1062+ </div>
1063+ <div class="ng-hide accordion__tab" data-ng-show="tabs.nodes.metadata.release.length">
1064+ <button class="accordion__tab-title maas-accordion-tab">OS/Release</button>
1065+ <div class="accordion__tab-content">
1066+ <ul class="accordion__tab-list">
1067+ <li class="accordion__tab-item" data-ng-repeat="release in tabs.nodes.metadata.release | orderBy:['name', '-count']" data-ng-class="{ 'is-active': isFilterActive('release', release.name, 'nodes') }">
1068+ <button class="accordion__tab-link" data-ng-click="toggleFilter('release', release.name, 'nodes')"><span data-maas-release-name="release.name"></span> ({$ release.count $})</button>
1069+ </li>
1070+ </ul>
1071+ </div>
1072+ </div>
1073+ <div class="ng-hide accordion__tab" data-ng-show="tabs.nodes.metadata.tags.length">
1074+ <button class="accordion__tab-title maas-accordion-tab">Tags</button>
1075+ <div class="accordion__tab-content">
1076+ <ul class="accordion__tab-list">
1077+ <li class="accordion__tab-item" data-ng-repeat="tag in tabs.nodes.metadata.tags | orderBy:['name', '-count']" data-ng-class="{ 'is-active': isFilterActive('tags', tag.name, 'nodes') }">
1078+ <button class="accordion__tab-link" data-ng-click="toggleFilter('tags', tag.name, 'nodes')">{$ tag.name $} ({$ tag.count $})</button>
1079+ </li>
1080+ </ul>
1081+ </div>
1082+ </div>
1083+ <div class="ng-hide accordion__tab" data-ng-show="tabs.nodes.metadata.storage_tags.length">
1084+ <button class="accordion__tab-title maas-accordion-tab">Storage Tags</button>
1085+ <div class="accordion__tab-content">
1086+ <ul class="accordion__tab-list">
1087+ <li class="accordion__tab-item" data-ng-repeat="tag in tabs.nodes.metadata.storage_tags | orderBy:['name', '-count']" data-ng-class="{ 'is-active': isFilterActive('storage_tags', tag.name, 'nodes') }">
1088+ <button class="accordion__tab-link" data-ng-click="toggleFilter('storage_tags', tag.name, 'nodes')">{$ tag.name $} ({$ tag.count $})</button>
1089+ </li>
1090+ </ul>
1091+ </div>
1092+ </div>
1093+ <div class="ng-hide accordion__tab" data-ng-show="tabs.nodes.metadata.subnets.length">
1094+ <button class="accordion__tab-title maas-accordion-tab">Subnets</button>
1095+ <div class="accordion__tab-content">
1096+ <ul class="accordion__tab-list">
1097+ <li class="accordion__tab-item" data-ng-repeat="subnet in tabs.nodes.metadata.subnets | orderBy:['name', '-count']" data-ng-class="{ 'is-active': isFilterActive('subnets', subnet.name, 'nodes') }">
1098+ <button class="accordion__tab-link" data-ng-click="toggleFilter('subnets', subnet.name, 'nodes')">{$ subnet.name $} ({$ subnet.count $})</button>
1099+ </li>
1100+ </ul>
1101+ </div>
1102+ </div>
1103+ <div class="ng-hide accordion__tab" data-ng-show="tabs.nodes.metadata.fabrics.length">
1104+ <button class="accordion__tab-title maas-accordion-tab">Fabrics</button>
1105+ <div class="accordion__tab-content">
1106+ <ul class="accordion__tab-list">
1107+ <li class="accordion__tab-item" data-ng-repeat="fabric in tabs.nodes.metadata.fabrics | orderBy:['name', '-count']" data-ng-class="{ 'is-active': isFilterActive('fabrics', fabric.name, 'nodes') }">
1108+ <button class="accordion__tab-link" data-ng-click="toggleFilter('fabrics', fabric.name, 'nodes')">{$ fabric.name $} ({$ fabric.count $})</button>
1109+ </li>
1110+ </ul>
1111+ </div>
1112+ </div>
1113+ <div class="ng-hide accordion__tab" data-ng-show="tabs.nodes.metadata.spaces.length">
1114+ <button class="accordion__tab-title maas-accordion-tab">Spaces</button>
1115+ <div class="accordion__tab-content">
1116+ <ul class="accordion__tab-list">
1117+ <li class="accordion__tab-item" data-ng-repeat="space in tabs.nodes.metadata.spaces | orderBy:['name', '-count']" data-ng-class="{ 'is-active': isFilterActive('spaces', space.name, 'nodes') }">
1118+ <button class="accordion__tab-link" data-ng-click="toggleFilter('spaces', space.name, 'nodes')">{$ space.name $} ({$ space.count $})</button>
1119+ </li>
1120+ </ul>
1121+ </div>
1122+ </div>
1123+ <div class="ng-hide accordion__tab" data-ng-show="tabs.nodes.metadata.zone.length">
1124+ <button class="accordion__tab-title maas-accordion-tab">Zones</button>
1125+ <div class="accordion__tab-content">
1126+ <ul class="accordion__tab-list">
1127+ <li class="accordion__tab-item" data-ng-repeat="zone in tabs.nodes.metadata.zone | orderBy:['name', '-count']" data-ng-class="{ 'is-active': isFilterActive('zone', zone.name, 'nodes') }">
1128+ <button class="accordion__tab-link" data-ng-click="toggleFilter('zone', zone.name, 'nodes')">{$ zone.name $} ({$ zone.count $})</button>
1129+ </li>
1130+ </ul>
1131+ </div>
1132+ </div>
1133+ </div>
1134+ </div>
1135+ <div class="accordion three-col maas-accordion ng-hide" data-ng-show="currentpage === 'devices'"
1136+ data-ng-class="{ 'is-disabled': tabs.devices.actionOption }">
1137+ <h3 class="accordion__title">Filter by</h3>
1138+ <!-- XXX rvba 2015-02-25 - Need to add e2e test. -->
1139+ <div class="ng-hide accordion__tab" data-ng-show="tabs.devices.metadata.owner.length">
1140+ <button class="accordion__tab-title maas-accordion-tab active">Owner</button>
1141+ <div class="accordion__tab-content">
1142+ <ul class="accordion__tab-list">
1143+ <li class="accordion__tab-item" data-ng-repeat="owner in tabs.devices.metadata.owner | orderBy:['name', '-count']" data-ng-class="{ 'is-active': isFilterActive('owner', owner.name, 'devices') }">
1144+ <button class="accordion__tab-link" data-ng-click="toggleFilter('owner', owner.name, 'devices')">{$ owner.name $} ({$ owner.count $})</button>
1145+ </li>
1146+ </ul>
1147+ </div>
1148+ </div>
1149+ <div class="ng-hide accordion__tab" data-ng-show="tabs.devices.metadata.tags.length">
1150+ <button class="accordion__tab-title maas-accordion-tab">Tags</button>
1151+ <div class="accordion__tab-content">
1152+ <ul class="accordion__tab-list">
1153+ <li class="accordion__tab-item" data-ng-repeat="tag in tabs.devices.metadata.tags | orderBy:['name', '-count']" data-ng-class="{ 'is-active': isFilterActive('tags', tag.name, 'devices') }">
1154+ <button class="accordion__tab-link" data-ng-click="toggleFilter('tags', tag.name, 'devices')">{$ tag.name $} ({$ tag.count $})</button>
1155+ </li>
1156+ </ul>
1157+ </div>
1158+ </div>
1159+ <div class="ng-hide accordion__tab" data-ng-show="tabs.devices.metadata.zone.length">
1160+ <button class="accordion__tab-title maas-accordion-tab">Zones</button>
1161+ <div class="accordion__tab-content">
1162+ <ul class="accordion__tab-list">
1163+ <li class="accordion__tab-item" data-ng-repeat="zone in tabs.devices.metadata.zone | orderBy:['name', '-count']" data-ng-class="{ 'is-active': isFilterActive('zone', zone.name, 'devices') }">
1164+ <button class="accordion__tab-link" data-ng-click="toggleFilter('zone', zone.name, 'devices')">{$ zone.name $} ({$ zone.count $})</button>
1165+ </li>
1166+ </ul>
1167+ </div>
1168+ </div>
1169+ </div>
1170+ </aside>
1171+ <!-- XXX ricgard 2016-06-16 - Need to add e2e test. -->
1172+ <div class="nine-col last-col ng-hide" data-ng-show="currentpage === 'nodes'">
1173+ <form>
1174+ <div id="search-bar" class="search nine-col">
1175+ <!-- XXX ricgard 2016-06-16 - Need to add e2e test. -->
1176+ <input
1177+ type="search" placeholder="Search nodes" class="search__input"
1178+ data-ng-model="tabs.nodes.search" data-ng-change="updateFilters('nodes')"
1179+ data-ng-class="{ error: !tabs.nodes.searchValid }"
1180+ data-ng-disabled="tabs.nodes.actionOption" />
1181+ <input type="reset" class="search__submit"
1182+ data-ng-class="{ 'search__submit--close': tabs.nodes.search.length > 0 }"
1183+ data-ng-click="clearSearch('nodes')" />
1184+ </div>
1185+ <maas-machines-table id="nodes-listing" search="tabs.nodes.search" ng-disabled="hasActionsInProgress('nodes')"
1186+ machine-has-error="!supportsAction($machine, 'nodes')" on-listing-change="onNodeListingChanged($machines, 'nodes')"
1187+ on-check-all="toggleCheckAll('nodes')" on-check="toggleChecked($machine, 'nodes')"></maas-machines-table>
1188+ </form>
1189+ </div>
1190+ <div class="nine-col last-col" class="ng-hide" data-ng-show="currentpage === 'devices'">
1191+ <div class="search nine-col">
1192+ <!-- XXX rvba 2015-02-25 - Need to add e2e test. -->
1193+ <input
1194+ type="search" placeholder="Search devices" class="search__input"
1195+ data-ng-model="tabs.devices.search" data-ng-change="updateFilters('devices')"
1196+ data-ng-class="{ error: !tabs.devices.searchValid }"
1197+ data-ng-disabled="tabs.devices.actionOption" />
1198+ <input type="reset" class="search__submit"
1199+ data-ng-class="{ 'search__submit--close': tabs.devices.search.length > 0 }"
1200+ data-ng-click="clearSearch('devices')" />
1201+ </div>
1202+ <table>
1203+ <thead>
1204+ <tr>
1205+ <th class="table-col--3">
1206+ <!-- XXX rvba 2015-02-25 - Need to add e2e test. -->
1207+ <input type="checkbox" class="checkbox" data-ng-click="toggleCheckAll('devices')" data-ng-checked="tabs.devices.allViewableChecked" id="check-all-devices" data-ng-disabled="hasActionsInProgress('devices')" />
1208+ <label for="check-all-devices" class="checkbox-label"></label>
1209+ </th>
1210+ <th class="table-col--24">
1211+ <!-- XXX rvba 2015-02-25 - Need to add e2e test. -->
1212+ <a href="" data-ng-click="sortTable('fqdn', 'devices')" data-ng-class="{'is-sorted': tabs.devices.predicate === 'fqdn', 'sort-asc': tabs.devices.reverse === false, 'sort-desc': tabs.devices.reverse === true}">
1213+ <span title="Fully Qualified Domain Name">FQDN</span>
1214+ </a>
1215+ </th>
1216+ <th class="table-col--25">
1217+ <a href="" data-ng-click="sortTable('primary_mac', 'devices')" data-ng-class="{'is-sorted': tabs.devices.predicate === 'primary_mac', 'sort-asc': tabs.devices.reverse === false, 'sort-desc': tabs.devices.reverse === true}">
1218+ <span title="Media Access Control addresses">MAC</span>
1219+ </a>
1220+ </th>
1221+ <th class="table-col--15">
1222+ <a href="" data-ng-click="sortTable('ip_assignment', 'devices')" data-ng-class="{'is-sorted': tabs.devices.predicate === 'ip_assignment', 'sort-asc': tabs.devices.reverse === false, 'sort-desc': tabs.devices.reverse === true}">IP Assignment</a>
1223+ </th>
1224+ <th class="table-col--20">
1225+ <a href="" data-ng-click="sortTable('ip_address', 'devices')" data-ng-class="{'is-sorted': tabs.devices.predicate === 'ip_address', 'sort-asc': tabs.devices.reverse === false, 'sort-desc': tabs.devices.reverse === true}">IP Address</a>
1226+ </th>
1227+ <th class="table-col--12">
1228+ <!-- XXX rvba 2015-02-25 - Need to add e2e test. -->
1229+ <a href="" data-ng-click="sortTable('owner', 'devices')" data-ng-class="{'is-sorted': tabs.devices.predicate === 'owner', 'sort-asc': tabs.devices.reverse === false, 'sort-desc': tabs.devices.reverse === true}">Owner</a>
1230+ </th>
1231+ </tr>
1232+ </thead>
1233+ <tbody vs-repeat vs-scroll-parent="window">
1234+ <!-- XXX rvba 2015-02-25 - Need to add e2e test. This really needs lots of tests. -->
1235+ <tr
1236+ data-ng-repeat="device in tabs.devices.filtered_items = (devices | nodesFilter:tabs.devices.search | orderBy:tabs.devices.predicate:tabs.devices.reverse) track by device.system_id"
1237+ data-ng-class="{ 'table--error': !supportsAction(device, 'devices'), selected: device.$selected }">
1238+ <td class="table-col--3" aria-label="Select device">
1239+ <input type="checkbox" class="checkbox" data-ng-click="toggleChecked(device, 'devices')" data-ng-checked="device.$selected" id="{$ device.fqdn $}" data-ng-disabled="hasActionsInProgress('devices')"/>
1240+ <label for="{$ device.fqdn $}" class="checkbox-label"></label>
1241+ </td>
1242+ <td class="table-col--24" aria-label="FQDN">
1243+ <a href="#/node/device/{$ device.system_id $}">{$ device.fqdn $}</a>
1244+ </td>
1245+ <td class="table-col--25" aria-label="MAC">
1246+ {$ device.primary_mac $}
1247+ <span class="extra-macs" data-ng-show="device.extra_macs.length">(+{$ device.extra_macs.length $})</span>
1248+ </td>
1249+ <td class="table-col--15" aria-label="IP Assignment">{$ getDeviceIPAssignment(device.ip_assignment) $}</td>
1250+ <td class="table-col--20" aria-label="IP Address">{$ device.ip_address $}</td>
1251+ <td class="table-col--12" aria-label="Owner">{$ device.owner $}</td>
1252+ </tr>
1253+ </tbody>
1254+ </table>
1255+ </div>
1256+ <div class="twelve-col last-col" class="ng-hide" data-ng-show="currentpage === 'controllers'">
1257+ <div class="search twelve-col">
1258+ <!-- XXX rvba 2015-02-25 - Need to add e2e test. -->
1259+ <input
1260+ type="search" placeholder="Search controllers" class="search__input"
1261+ data-ng-model="tabs.controllers.search" data-ng-change="updateFilters('controllers')"
1262+ data-ng-class="{ error: !tabs.controllers.searchValid }"
1263+ data-ng-disabled="tabs.controllers.actionOption" />
1264+ <input type="submit" class="search__submit"
1265+ data-ng-class="{ 'search__submit--close': tabs.controllers.search.length > 0 }"
1266+ data-ng-click="clearSearch('controllers')" />
1267+ </div>
1268+ <table>
1269+ <thead>
1270+ <tr>
1271+ <th class="table-col--2">
1272+ <!-- XXX rvba 2015-02-25 - Need to add e2e test. -->
1273+ <input type="checkbox" class="checkbox" data-ng-click="toggleCheckAll('controllers')" data-ng-checked="tabs.controllers.allViewableChecked" id="check-all-controllers" data-ng-disabled="hasActionsInProgress('controllers')" />
1274+ <label for="check-all-controllers" class="checkbox-label"></label>
1275+ </th>
1276+ <th class="table-col--19">
1277+ <a href="" data-ng-click="sortTable('fqdn', 'controllers')" data-ng-class="{'is-sorted': tabs.controllers.predicate === 'fqdn', 'sort-asc': tabs.controllers.reverse === false, 'sort-desc': tabs.controllers.reverse === true}">
1278+ <span title="Fully Qualified Domain Name">Name</span>
1279+ </a>
1280+ </th>
1281+ <th class="table-col--4 u-align--center">
1282+ <span title="Status">Status</span>
1283+ </th>
1284+ <th class="table-col--15">
1285+ <a href="" data-ng-click="sortTable('node_type_display', 'controllers')" data-ng-class="{'is-sorted': tabs.controllers.predicate === 'node_type_display', 'sort-asc': tabs.controllers.reverse === false, 'sort-desc': tabs.controllers.reverse === true}">
1286+ <span title="Controller Type">Type</span>
1287+ </a>
1288+ </th>
1289+ <th class="table-col--10">
1290+ <a href="" data-ng-click="sortTable('node_type_display', 'controllers')" data-ng-class="{'is-sorted': tabs.controllers.predicate === 'version', 'sort-asc': tabs.controllers.reverse === false, 'sort-desc': tabs.controllers.reverse === true}">
1291+ <span title="Version">Version</span>
1292+ </a>
1293+ </th>
1294+ <th class="table-col--15">
1295+ <a href="" data-ng-click="sortTable('updated', 'controllers')" data-ng-class="{'is-sorted': tabs.controllers.predicate === 'updated', 'sort-asc': tabs.controllers.reverse === false, 'sort-desc': tabs.controllers.reverse === true}">Last Image Sync</a>
1296+ </th>
1297+ <th class="table-col--15">
1298+ <a href="" data-ng-click="sortTable('updated', 'controllers')" data-ng-class="{'is-sorted': tabs.controllers.predicate === 'updated', 'sort-asc': tabs.controllers.reverse === false, 'sort-desc': tabs.controllers.reverse === true}">Images Status</a>
1299+ </th>
1300+ </tr>
1301+ </thead>
1302+ <tbody vs-repeat vs-scroll-parent="window">
1303+ <!-- XXX rvba 2015-02-25 - Need to add e2e test. This really needs lots of tests. -->
1304+ <tr
1305+ data-ng-repeat="controller in tabs.controllers.filtered_items = (controllers | nodesFilter:tabs.controllers.search | orderBy:tabs.controllers.predicate:tabs.controllers.reverse) track by controller.system_id"
1306+ data-ng-class="{ 'table--error': !supportsAction(controller, 'controllers'), selected: controller.$selected }">
1307+ <td class="table-col--2" aria-label="Select controller">
1308+ <input type="checkbox" class="checkbox" data-ng-click="toggleChecked(controller, 'controllers')" data-ng-checked="controller.$selected" id="{$ controller.fqdn $}" data-ng-disabled="hasActionsInProgress('controllers')"/>
1309+ <label for="{$ controller.fqdn $}" class="checkbox-label"></label>
1310+ </td>
1311+ <td class="table-col--19" aria-label="FQDN">
1312+ <a href="#/node/controller/{$ controller.system_id $}">{$ controller.fqdn $}</a>
1313+ </td>
1314+ <td class="table-col--4 u-align--center" data-maas-controller-status="controller" data-maas-services="services" aria-label="Status"></td>
1315+ <td class="table-col--15" aria-label="Type">{$ controller.node_type_display $}</td>
1316+ <td class="table-col--10" aria-label="Version">
1317+ <div data-ng-show="controller.version">
1318+ {$ controller.version__short $}
1319+ </div>
1320+ <div data-ng-show="!controller.version" class="u-text--subtle">
1321+ Unknown
1322+ <i class="icon icon--info u-margin--left-tiny tooltip"
1323+ aria-label="Less than 2.3.0"></i>
1324+ </div>
1325+ </td>
1326+ <td class="table-col--15" aria-label="Last image sync">{$ controller.last_image_sync || 'Never' $}</td>
1327+ <td class="table-col--15" aria-label="Image status">
1328+ <maas-controller-image-status system-id="controller.system_id"></maas-controller-image-status>
1329+ </td>
1330+ </tr>
1331+ </tbody>
1332+ </table>
1333+ </div>
1334+ <div class="twelve-col last-col" class="ng-hide" data-ng-show="currentpage === 'switches'">
1335+ <div class="search twelve-col">
1336+ <!-- XXX rvba 2015-02-25 - Need to add e2e test. -->
1337+ <input
1338+ type="search" placeholder="Search switches" class="search__input"
1339+ data-ng-model="tabs.switches.search" data-ng-change="updateFilters('switches')"
1340+ data-ng-class="{ error: !tabs.switches.searchValid }"
1341+ data-ng-disabled="tabs.switches.actionOption" />
1342+ <input type="submit" class="search__submit"
1343+ data-ng-class="{ 'search__submit--close': tabs.switches.search.length > 0 }"
1344+ data-ng-click="clearSearch('switches')" />
1345+ </div>
1346+ <maas-switches-table id="switches-listing" search="tabs.switches.search" ng-disabled="hasActionsInProgress('switches')"
1347+ switch-has-error="!supportsAction($switch_, 'switches')" on-listing-change="onNodeListingChanged($switches, 'switches')"
1348+ on-check-all="toggleCheckAll('switches')" on-check="toggleChecked($switch_, 'switches')"></maas-switches-table>
1349+ </div>
1350+ </div>
1351+</div>
1352+>>>>>>> src/maasserver/static/partials/nodes-list.html
1353diff --git a/src/maasserver/static/partials/script-results-list.html b/src/maasserver/static/partials/script-results-list.html
1354index bfadd34..60ad83a 100644
1355--- a/src/maasserver/static/partials/script-results-list.html
1356+++ b/src/maasserver/static/partials/script-results-list.html
1357@@ -4,6 +4,7 @@
1358 <p class="u-text--loading"><i class="p-icon--spinner u-animation--spin"></i>&nbsp;&nbsp;Loading...</p>
1359 </div>
1360 </div>
1361+<<<<<<< src/maasserver/static/partials/script-results-list.html
1362 <div class="row">
1363 <div data-ng-repeat="hardware_type in results">
1364 <div data-ng-if="resultsLoaded && (hardware_type.results | json) != '{}'">
1365@@ -47,12 +48,59 @@
1366 <button class="p-contextual-menu__link" aria-label="View previous {$ result.result_section $}" data-ng-if="!result.showing_history" data-ng-click="toggleMenu(); loadHistory(result)">View previous {$ result.result_section $}</button>
1367 <button class="p-contextual-menu__link" aria-label="Hide previous {$ result.result_section $}" data-ng-if="result.showing_history" data-ng-click="toggleMenu(); result.showing_results = false; result.showing_history = false">Hide previous {$ result.result_section $}</button>
1368 </div>
1369+=======
1370+ <div data-ng-repeat="hardware_type in results">
1371+ <div data-ng-if="resultsLoaded && (hardware_type.results | json) != '{}'">
1372+ <h2 data-ng-if="hardware_type.title !== 'null'">{$ hardware_type.title $}</h2>
1373+ <div data-ng-repeat="(title, results) in hardware_type.results">
1374+ <h3 data-ng-if="title !== 'null'">{$ title $}</h3>
1375+ <section class="table u-margin--bottom">
1376+ <header class="table__head">
1377+ <div class="table__row">
1378+ <div class="table__header table-col--2 u-padding--left-none"></div>
1379+ <div class="table__header table-col--24">Name</div>
1380+ <div class="table__header table-col--22">Tags</div>
1381+ <div class="table__header table-col--15">Runtime</div>
1382+ <div class="table__header table-col--20">Date</div>
1383+ <div class="table__header table-col--12">Result</div>
1384+ <div class="table__header table-col--5 u-align--right">Actions</div>
1385+ </div>
1386+ </header>
1387+ <main class="table__body">
1388+ <div data-ng-repeat="result in results">
1389+ <div class="table__row" data-ng-class="{'is-active': result.showing_results || result.showing_history}">
1390+ <div class="table__data table-col--2 u-padding--left-none" aria-label="Status">
1391+ <span data-maas-script-status="script-status" data-script-status="result.status"></span>
1392+ </div>
1393+ <div class="table__data table-col--24" data-ng-click="result.showing_results = !result.showing_results" aria-label="Name">
1394+ {$ result.name $}
1395+ <button class="icon u-margin--top-tiny u-float--right" data-ng-class="{'icon--open': !result.showing_results, 'icon--close': result.showing_results}" data-ng-if="!result.showing_history"></button>
1396+ </div>
1397+ <div class="table__data table-col--22" aria-label="Tags"><span data-ng-hide="result.showing_history">{$ result.tags $}</span></div>
1398+ <div class="table__data table-col--15" aria-label="Runtime"><span data-ng-hide="result.showing_history" data-maas-script-run-time="script-runtime" data-start-time="result.starttime" data-run-time="{{result.runtime}}" data-estimated-run-time="{{result.estimated_runtime}}" data-script-status="result.status"></span></div>
1399+ <div class="table__data table-col--20" aria-label="Date"><span data-ng-hide="result.showing_history">{$ result.updated $}</span></div>
1400+ <div class="table__data table-col--12" aria-label="Status">
1401+ <span data-ng-hide="result.showing_history">
1402+ <!-- 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)-->
1403+ {$ result.status_name $} <a data-ng-if="result.status === 2 || result.status === 3 || result.status === 4 || result.status === 6 || result.status === 8" href="#/node/{$ type_name $}/{$ node.system_id $}/{$ section.area $}/{$ result.id $}">View log</a>
1404+ </span>
1405+ </div>
1406+ <div class="table__data table-col--5 table--mobile-controls">
1407+ <div class="table__controls u-align--right" toggle-ctrl>
1408+ <button class="table__controls-toggle" data-ng-click="toggleMenu()">View actions</button>
1409+ <div class="table__controls-menu ng-hide" role="menu" data-ng-show="isToggled">
1410+ <button class="table__controls-action" aria-label="View metrics" data-ng-if="!result.showing_results" data-ng-click="toggleMenu(); result.showing_history = false; result.showing_results = true">View metrics</button>
1411+ <button class="table__controls-action" aria-label="Hide metrics" data-ng-if="result.showing_results" data-ng-click="toggleMenu(); result.showing_history = false; result.showing_results = false">Hide metrics</button>
1412+ <button class="table__controls-action" aria-label="View previous {$ result.result_section $}" data-ng-if="!result.showing_history" data-ng-click="toggleMenu(); result.showing_results = false; result.showing_history = true">View previous {$ result.result_section $}</button>
1413+ <button class="table__controls-action" aria-label="Hide previous {$ result.result_section $}" data-ng-if="result.showing_history" data-ng-click="toggleMenu(); result.showing_results = false; result.showing_history = false">Hide previous {$ result.result_section $}</button>
1414+>>>>>>> src/maasserver/static/partials/script-results-list.html
1415 </div>
1416 </td>
1417 <td class="p-table-expanding__panel col-12" aria-label="results" data-ng-if="result.showing_results && !result.showing_history">
1418 <div class="row">
1419 <div class="col-12" data-ng-if="result.results.length === 0">No metrics provided</div>
1420 </div>
1421+<<<<<<< src/maasserver/static/partials/script-results-list.html
1422 <div class="row" data-ng-if="result.results">
1423 <dl data-ng-repeat="item in result.results">
1424 <dt class="p-tooltip p-tooltip--top-center">
1425@@ -66,6 +114,34 @@
1426 <td class="p-table-expanding__panel col-12" aria-label="loading history" data-ng-if="result.loading_history">
1427 <div class="col-12">
1428 <p class="u-text--loading"><i class="p-icon--spinner u-animation--spin"></i>&nbsp;&nbsp;Loading...</p>
1429+=======
1430+ </div>
1431+ </div>
1432+
1433+ <div class="table__dropdown" aria-label="history" data-ng-if="result.showing_history">
1434+ <div class="table__row is-active">
1435+ <div class="table__data table-col--100">
1436+ <section class="table u-margin--bottom">
1437+ <main class="table__body">
1438+ <div class="table__row is-active u-border--none" data-ng-repeat="item in result.history_list">
1439+ <div class="table__data table-col--2 u-padding--left-none" aria-label="Status">
1440+ <span data-maas-script-status="script-status" data-script-status="item.status"></span>
1441+ </div>
1442+ <div class="table__data table-col--24" aria-label="Name">{$ result.name $}</div>
1443+ <div class="table__data table-col--24" aria-label="Tags">{$ result.tags $}</div>
1444+ <div class="table__data table-col--15" 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></div>
1445+ <div class="table__data table-col--20" aria-label="Date">{$ item.updated $}</div>
1446+ <div class="table__data table-col--10" aria-label="Status">
1447+ <!-- 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)-->
1448+ {$ item.status_name $} <a data-ng-if="item.status === 2 || item.status === 3 || item.status === 4 || item.status === 6 || item.status === 8" href="#/node/{$ type_name $}/{$ node.system_id $}/{$ section.area $}/{$ item.id $}">View log</a>
1449+ </div>
1450+ </div>
1451+ </main>
1452+ </section>
1453+ <p class="u-align--center u-margin--bottom">
1454+ <button class="button--secondary button--inline" data-ng-click="result.showing_history = false">Hide previous {$ result.result_section $}</button>
1455+ </p>
1456+>>>>>>> src/maasserver/static/partials/script-results-list.html
1457 </div>
1458 </div>
1459 <td class="p-table-expanding__panel col-12" aria-label="history" data-ng-if="result.showing_history">
1460diff --git a/src/maasserver/tests/test_bootsources.py b/src/maasserver/tests/test_bootsources.py
1461index a83c9c1..3629529 100644
1462--- a/src/maasserver/tests/test_bootsources.py
1463+++ b/src/maasserver/tests/test_bootsources.py
1464@@ -44,7 +44,10 @@ from maasserver.testing.testcase import (
1465 )
1466 from maasserver.tests.test_bootresources import SimplestreamsEnvFixture
1467 from maasserver.utils import get_maas_user_agent
1468+<<<<<<< src/maasserver/tests/test_bootsources.py
1469 from maastesting.djangotestcase import count_queries
1470+=======
1471+>>>>>>> src/maasserver/tests/test_bootsources.py
1472 from maastesting.matchers import MockCalledOnceWith
1473 from provisioningserver.config import DEFAULT_IMAGES_URL
1474 from provisioningserver.import_images import (
1475diff --git a/src/maasserver/triggers/tests/test_websocket_listener.py b/src/maasserver/triggers/tests/test_websocket_listener.py
1476index 6297f0e..5ba464b 100644
1477--- a/src/maasserver/triggers/tests/test_websocket_listener.py
1478+++ b/src/maasserver/triggers/tests/test_websocket_listener.py
1479@@ -1714,8 +1714,13 @@ class TestIPRangeListener(
1480 self.create_subnet, {'cidr': str(network)})
1481 params = {
1482 'subnet': subnet,
1483+<<<<<<< src/maasserver/triggers/tests/test_websocket_listener.py
1484 'start_ip': IPAddress(network.first + 2),
1485 'end_ip': IPAddress(network.last - 1)}
1486+=======
1487+ 'start_ip': str(IPAddress(network.first + 2)),
1488+ 'end_ip': str(IPAddress(network.last - 1))}
1489+>>>>>>> src/maasserver/triggers/tests/test_websocket_listener.py
1490 yield listener.startService()
1491 try:
1492 iprange = yield deferToDatabase(
1493diff --git a/src/provisioningserver/import_images/boot_resources.py b/src/provisioningserver/import_images/boot_resources.py
1494index f985df6..0a5b3c2 100644
1495--- a/src/provisioningserver/import_images/boot_resources.py
1496+++ b/src/provisioningserver/import_images/boot_resources.py
1497@@ -9,6 +9,7 @@ __all__ = [
1498 ]
1499
1500 from argparse import ArgumentParser
1501+import copy
1502 import errno
1503 from io import StringIO
1504 import os
1505@@ -41,6 +42,18 @@ from provisioningserver.utils.fs import (
1506 read_text_file,
1507 tempdir,
1508 )
1509+<<<<<<< src/provisioningserver/import_images/boot_resources.py
1510+=======
1511+from provisioningserver.utils.service_monitor import ServiceActionError
1512+from provisioningserver.utils.shell import (
1513+ call_and_check,
1514+ ExternalProcessError,
1515+)
1516+from provisioningserver.utils.snappy import (
1517+ get_snap_data_path,
1518+ running_in_snap,
1519+)
1520+>>>>>>> src/provisioningserver/import_images/boot_resources.py
1521 from twisted.internet.defer import inlineCallbacks
1522 from twisted.python.filepath import FilePath
1523
1524@@ -109,6 +122,54 @@ def write_snapshot_metadata(snapshot, meta_file_content):
1525 atomic_write(meta_file_content.encode("ascii"), meta_file, mode=0o644)
1526
1527
1528+<<<<<<< src/provisioningserver/import_images/boot_resources.py
1529+=======
1530+def write_targets_conf(snapshot):
1531+ """Write "maas.tgt" file."""
1532+ targets_conf = os.path.join(snapshot, 'maas.tgt')
1533+ targets_conf_content = compose_targets_conf(snapshot)
1534+ atomic_write(targets_conf_content, targets_conf, mode=0o644)
1535+
1536+
1537+def update_targets_conf(snapshot):
1538+ """Runs tgt-admin to update the new targets from "maas.tgt"."""
1539+ # Ensure that tgt is running before tgt-admin is used.
1540+ try:
1541+ service_monitor.ensureService("tgt").wait(30)
1542+ except ServiceActionError:
1543+ msg = "Unable to start tgt"
1544+ try_send_rack_event(EVENT_TYPES.RACK_IMPORT_WARNING, msg)
1545+ maaslog.warning(msg)
1546+ return
1547+
1548+ # Update the tgt config.
1549+ targets_conf = os.path.join(snapshot, 'maas.tgt')
1550+
1551+ # The targets_conf may not exist in the event the BootSource is broken
1552+ # and images havn't been imported yet. This fixes LP:1655721
1553+ if not os.path.exists(targets_conf):
1554+ return
1555+
1556+ try:
1557+ env = copy.deepcopy(os.environ)
1558+ # LP:1718706 - When TGT is run in a snap the socket is stored in the
1559+ # SNAP_DATA path. Define it before calling tgt-admin otherwise the
1560+ # standard path is used and the call will fail.
1561+ if running_in_snap():
1562+ env['TGT_IPC_SOCKET'] = os.path.join(
1563+ get_snap_data_path(), 'tgtd-socket')
1564+ call_and_check(sudo([
1565+ get_path('/usr/sbin/tgt-admin'),
1566+ '--conf', targets_conf,
1567+ '--update', 'ALL',
1568+ ]), env=env)
1569+ except ExternalProcessError as e:
1570+ msg = "Unable to update TGT config: %s" % e
1571+ try_send_rack_event(EVENT_TYPES.RACK_IMPORT_WARNING, msg)
1572+ maaslog.warning(msg)
1573+
1574+
1575+>>>>>>> src/provisioningserver/import_images/boot_resources.py
1576 def read_sources(sources_yaml):
1577 """Read boot resources config file.
1578
1579diff --git a/src/provisioningserver/import_images/tests/test_boot_resources.py b/src/provisioningserver/import_images/tests/test_boot_resources.py
1580index 28635e7..31fa955 100644
1581--- a/src/provisioningserver/import_images/tests/test_boot_resources.py
1582+++ b/src/provisioningserver/import_images/tests/test_boot_resources.py
1583@@ -43,7 +43,18 @@ from provisioningserver.testing.config import (
1584 BootSourcesFixture,
1585 ClusterConfigurationFixture,
1586 )
1587+<<<<<<< src/provisioningserver/import_images/tests/test_boot_resources.py
1588 from provisioningserver.utils.fs import write_text_file
1589+=======
1590+from provisioningserver.utils.fs import (
1591+ tempdir,
1592+ write_text_file,
1593+)
1594+from provisioningserver.utils.service_monitor import ServiceActionError
1595+from provisioningserver.utils.shell import ExternalProcessError
1596+from testtools.content import Content
1597+from testtools.content_type import UTF8_TEXT
1598+>>>>>>> src/provisioningserver/import_images/tests/test_boot_resources.py
1599 from testtools.matchers import (
1600 DirExists,
1601 FileExists,
1602@@ -477,6 +488,87 @@ class TestMain(MAASTestCase):
1603 boot_resources.NoConfigFile,
1604 boot_resources.main, self.make_args(sources="", sources_file=""))
1605
1606+<<<<<<< src/provisioningserver/import_images/tests/test_boot_resources.py
1607+=======
1608+ def test_update_targets_conf_ensures_tgt_service(self):
1609+ mock_ensureService = self.patch(
1610+ boot_resources.service_monitor, "ensureService")
1611+ self.patch(boot_resources, "call_and_check")
1612+ boot_resources.update_targets_conf(factory.make_name("snapshot"))
1613+ self.assertThat(mock_ensureService, MockCalledOnceWith("tgt"))
1614+
1615+ def test_update_targets_conf_logs_tgt_service_check_error(self):
1616+ # Regression test for LP:1735025
1617+ mock_ensureService = self.patch(
1618+ boot_resources.service_monitor, "ensureService")
1619+ mock_ensureService.side_effect = ServiceActionError()
1620+ mock_try_send_rack_event = self.patch(
1621+ boot_resources, 'try_send_rack_event')
1622+ mock_maaslog = self.patch(boot_resources.maaslog, 'warning')
1623+ boot_resources.update_targets_conf(factory.make_name("snapshot"))
1624+ self.assertThat(mock_try_send_rack_event, MockCalledOnce())
1625+ self.assertThat(mock_maaslog, MockCalledOnce())
1626+
1627+ def test_update_targets_conf_logs_error(self):
1628+ self.patch(boot_resources.service_monitor, "ensureService")
1629+ mock_try_send_rack_event = self.patch(
1630+ boot_resources, 'try_send_rack_event')
1631+ mock_maaslog = self.patch(boot_resources.maaslog, 'warning')
1632+ self.patch(boot_resources.os.path, 'exists').return_value = True
1633+ self.patch(boot_resources, 'call_and_check').side_effect = (
1634+ ExternalProcessError(
1635+ returncode=2, cmd=('tgt-admin',), output='error'))
1636+ snapshot = factory.make_name("snapshot")
1637+ boot_resources.update_targets_conf(snapshot)
1638+ self.assertThat(mock_try_send_rack_event, MockCalledOnce())
1639+ self.assertThat(mock_maaslog, MockCalledOnce())
1640+ self.assertThat(
1641+ boot_resources.call_and_check,
1642+ MockCalledOnceWith([
1643+ 'sudo', '-n', '/usr/sbin/tgt-admin',
1644+ '--conf', os.path.join(snapshot, 'maas.tgt'),
1645+ '--update', 'ALL'], env=os.environ))
1646+
1647+ def test_update_targets_only_runs_when_conf_exists(self):
1648+ # Regression test for LP:1655721
1649+ temp_dir = self.useFixture(TempDirectory()).path
1650+ self.useFixture(ClusterConfigurationFixture(tftp_root=temp_dir))
1651+ mock_ensureService = self.patch(
1652+ boot_resources.service_monitor, "ensureService")
1653+ mock_call_and_check = self.patch(boot_resources, "call_and_check")
1654+ mock_path_exists = self.patch(boot_resources.os.path, 'exists')
1655+ mock_path_exists.return_value = False
1656+ boot_resources.update_targets_conf(temp_dir)
1657+ self.assertThat(mock_ensureService, MockCalledOnceWith("tgt"))
1658+ self.assertThat(
1659+ mock_path_exists,
1660+ MockCalledOnceWith(os.path.join(temp_dir, 'maas.tgt')))
1661+ self.assertThat(mock_call_and_check, MockNotCalled())
1662+
1663+ def test_update_targets_conf_sets_env_var_in_snap(self):
1664+ # Regression test for LP:1718706
1665+ self.patch(boot_resources.service_monitor, "ensureService")
1666+ self.patch(boot_resources.os.path, 'exists').return_value = True
1667+ self.patch(boot_resources, 'call_and_check')
1668+ self.patch(boot_resources, 'running_in_snap').return_value = True
1669+ snap_data_path = factory.make_name('snap_data_path')
1670+ self.patch(boot_resources, 'get_snap_data_path').return_value = (
1671+ snap_data_path)
1672+ snapshot = factory.make_name("snapshot")
1673+ boot_resources.update_targets_conf(snapshot)
1674+ self.assertThat(
1675+ boot_resources.call_and_check,
1676+ MockCalledOnceWith([
1677+ 'sudo', '-n', '/usr/sbin/tgt-admin',
1678+ '--conf', os.path.join(snapshot, 'maas.tgt'),
1679+ '--update', 'ALL'
1680+ ], env={
1681+ 'TGT_IPC_SOCKET': os.path.join(
1682+ snap_data_path, 'tgtd-socket'),
1683+ **os.environ,
1684+ }))
1685+
1686+>>>>>>> src/provisioningserver/import_images/tests/test_boot_resources.py
1687
1688 class TestMetaContains(MAASTestCase):
1689 """Tests for the `meta_contains` function."""
1690diff --git a/src/provisioningserver/import_images/tests/test_download_resources.py b/src/provisioningserver/import_images/tests/test_download_resources.py
1691index 9f40e29..81bc3c1 100644
1692--- a/src/provisioningserver/import_images/tests/test_download_resources.py
1693+++ b/src/provisioningserver/import_images/tests/test_download_resources.py
1694@@ -359,6 +359,41 @@ class TestRepoWriter(MAASTestCase):
1695 label=product['label'], subarches={'ga-16.04', 'generic'},
1696 bootloader_type=None))
1697
1698+<<<<<<< src/provisioningserver/import_images/tests/test_download_resources.py
1699+ def test_inserts_no_generic_link_for_generic_non_ga_kflavor(self):
1700+ # Regression test for LP:1749246
1701+=======
1702+ def test_inserts_generic_link_for_generic_ga_kflavor(self):
1703+>>>>>>> src/provisioningserver/import_images/tests/test_download_resources.py
1704+ product_mapping = ProductMapping()
1705+ product = self.make_product(subarch='hwe-16.04', kflavor='generic')
1706+ product_mapping.add(product, 'hwe-16.04')
1707+ repo_writer = download_resources.RepoWriter(
1708+ None, None, product_mapping)
1709+ self.patch(
1710+ download_resources, 'products_exdata').return_value = product
1711+ # Prevent MAAS from trying to actually write the file.
1712+ mock_insert_file = self.patch(download_resources, 'insert_file')
1713+ mock_link_resources = self.patch(download_resources, 'link_resources')
1714+ # We only need to provide the product as the other fields are only used
1715+ # when writing the actual files to disk.
1716+ repo_writer.insert_item(product, None, None, None, None)
1717+ # None is used for the store and the content source as we're not
1718+ # writing anything to disk.
1719+ self.assertThat(
1720+ mock_insert_file,
1721+ MockCalledOnceWith(
1722+ None, os.path.basename(product['path']), product['sha256'],
1723+ {'sha256': product['sha256']}, product['size'], None))
1724+ # links are mocked out by the mock_insert_file above.
1725+ self.assertThat(
1726+ mock_link_resources,
1727+ MockCalledOnceWith(
1728+ snapshot_path=None, links=mock.ANY, osystem=product['os'],
1729+ arch=product['arch'], release=product['release'],
1730+ label=product['label'], subarches={'hwe-16.04'},
1731+ bootloader_type=None))
1732+
1733 def test_inserts_no_generic_link_for_generic_non_ga_kflavor(self):
1734 # Regression test for LP:1749246
1735 product_mapping = ProductMapping()
1736diff --git a/src/provisioningserver/utils/tests/test_network.py b/src/provisioningserver/utils/tests/test_network.py
1737index bda51e1..3b3c6b0 100644
1738--- a/src/provisioningserver/utils/tests/test_network.py
1739+++ b/src/provisioningserver/utils/tests/test_network.py
1740@@ -83,7 +83,10 @@ from provisioningserver.utils.network import (
1741 resolves_to_loopback_address,
1742 reverseResolve,
1743 )
1744+<<<<<<< src/provisioningserver/utils/tests/test_network.py
1745 from provisioningserver.utils.shell import get_env_with_locale
1746+=======
1747+>>>>>>> src/provisioningserver/utils/tests/test_network.py
1748 from testtools import ExpectedException
1749 from testtools.matchers import (
1750 Contains,
1751diff --git a/utilities/release-build b/utilities/release-build
1752index 3c0a5af..49f32cc 100755
1753--- a/utilities/release-build
1754+++ b/utilities/release-build
1755@@ -15,7 +15,7 @@ usage () {
1756 exit 1
1757 }
1758
1759-DEFAULT_DISTROS="$(ubuntu-distro-info --supported | grep -v trusty)"
1760+DEFAULT_DISTROS="$(ubuntu-distro-info --supported | grep -v trusty | grep -v bionic)"
1761 PACKAGE_DISTROS=""
1762 KEEP_CHANGELOG=0
1763
1764@@ -61,7 +61,7 @@ dch -a "" --release-heuristic log --nomultimaint
1765 PKG=$(head -n1 debian/changelog | awk '{print $1}')
1766 MAJOR_VER=$(head -n 1 debian/changelog | sed 's/^.*(//' | sed 's/).*//' | sed 's/-.*//')
1767 REV_COUNT=$(git rev-list --count $COMMIT)
1768-REV_SHORT=$(git rev-parse --short $COMMIT)
1769+REV_SHORT=$(git rev-parse --short=7 $COMMIT)
1770 FULL_VER="$MAJOR_VER-$REV_COUNT-g$REV_SHORT"
1771 TARBALL="maas_$FULL_VER.orig.tar.gz"
1772
1773@@ -89,3 +89,12 @@ if echo "$PACKAGE_DISTROS" | grep -q "bionic"; then
1774 sed -i "s/) UNRELEASED;/~18.04.1) bionic;/i" debian/changelog
1775 debuild -S -sa
1776 fi
1777+<<<<<<< utilities/release-build
1778+=======
1779+
1780+if echo "$PACKAGE_DISTROS" | grep -q "artful"; then
1781+ cp $ROOTDIR/debian/changelog debian/changelog
1782+ sed -i "s/) UNRELEASED;/~17.10.1) artful;/" debian/changelog
1783+ debuild -S
1784+fi
1785+>>>>>>> utilities/release-build

Subscribers

People subscribed via source and target branches

to all changes: