Merge lp:~rbanffy/maas/1508059-node-if-config--on-ready-ui into lp:~maas-committers/maas/trunk

Proposed by Ricardo Bánffy
Status: Merged
Approved by: Ricardo Bánffy
Approved revision: no longer in the source branch.
Merged at revision: 4413
Proposed branch: lp:~rbanffy/maas/1508059-node-if-config--on-ready-ui
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 996 lines (+415/-257)
5 files modified
src/maasserver/api/interfaces.py (+1/-1)
src/maasserver/api/tests/test_interfaces.py (+254/-234)
src/maasserver/static/js/angular/controllers/node_details_networking.js (+46/-13)
src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js (+99/-3)
src/maasserver/static/partials/node-details.html (+15/-6)
To merge this branch: bzr merge lp:~rbanffy/maas/1508059-node-if-config--on-ready-ui
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Andres Rodriguez (community) Needs Fixing
LaMont Jones (community) Approve
Review via email: mp+275440@code.launchpad.net

Commit message

Disables the UI for changing networking info when the node is on any state other than "Ready" or "Broken". Changes the API to also allow changes when the node is "Broken".

To post a comment you must log in.
Revision history for this message
LaMont Jones (lamont) wrote :

Other htan the comment update, looks good.

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

So, I think here's what we need:

1. Users can't update network config when machine is 'Deploying'
2. Users can't update network config when machine is 'Commissioning'
3. Users can't update network config when machine is 'Deployed'.

Other than that, users can edit network config.

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

No it should only be when ready. Please read the attached bug for more information on why. I can explain in more detail later. Dealing with some other things at the moment.

I have not looked at this branch yet. But i will test it out soon.

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

Below is a breakdown on all the states an why only allowing Ready is the winner.

* NEW
   - Has not been commissioned so networking should not be changed.
* COMMISSIONING
   - Networking is going to be reset once finished.
* DEPLOYED
   - Already deployed changing anything would have no affect.
* BROKEN
   - Not even a usable node so manipulating the networking has no point. Must
     be placed it Ready state before it can be used.
* DEPLOYING
   - Already deploying, it is too late.
* ALLOCATED
   - Now this one is debatable, but I believe it needs to be consistent with
     storage. To make it consistent it should be disallowed.
        Reasoning: Storage can be modified in this state but only fstype and
                   mount_point. All other operations are disabled. This is
                   because once the node is released they will return to the
                   previous value before the acquire action. Networking should
                   disallow this state because if you make a change while it is
                   acquired they will be perminent, and will not reset upon
                   release. I think it would be confusing to the user to
                   provide two different outcomes from the allocated state.
* RELEASING
   - Well it is releasing so it will go to ready no action should be allowed.
* DISK_ERASING
   - Similar to releasing same reasoning applies.

* FAILED_COMMISSIONING
* FAILED_DEPLOYMENT
* FAILED_RELEASING
* FAILED_DISK_ERASING
   - All failed states require action from the user so they should be disabled
     as well.

* READY
   - Winner: Should be allowed!

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

Now I have actually looked at the code. I think it still needs some improvements and there are no unit tests for this change that you have already implemented.

Take a look at: http://paste.ubuntu.com/12898049/

I have made the changes to disallow editing even for non-admins and it shows a nice information message to an administrator when the node is not ready.

I would use that to improve this branch, but you also need to add unit tests for all the methods that I have added in the controller in that paste.

review: Needs Fixing
Revision history for this message
Christian Reis (kiko) wrote :

What about assigned/allocated?

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

Uhm... I described why...

Revision history for this message
Ricardo Bánffy (rbanffy) wrote :

Thanks Andres, Blake and Kiko for your comments. I refrained from adding any new UI elements to the initial implementation (which actually made things vanish from the interface, which is something I sometimes consider confusing and, therefore, I went with just disabling the affected elements) without having someone from design chiming in, but the message is coherent with the rest of the UI.

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

Hi Blake,

> * NEW
> - Has not been commissioned so networking should not be changed.
>

agree

> * COMMISSIONING
> - Networking is going to be reset once finished.
>

agree

* DEPLOYED
> - Already deployed changing anything would have no affect.
>
agree

* BROKEN
> - Not even a usable node so manipulating the networking has no point.
> Must
> be placed it Ready state before it can be used.
>

Disagree. The user should be able to change his network config. In fact, he
might have marked it broken to change things in MAAS, like adding more NICs
or god knows, so he should be able to make changes here. Once he marks the
machine fixed, it will be ready an usable.

> * DEPLOYING
> - Already deploying, it is too late.
>

Agreed.

> * ALLOCATED
> - Now this one is debatable, but I believe it needs to be consistent
> with
> storage. To make it consistent it should be disallowed.
> Reasoning: Storage can be modified in this state but only fstype
> and
> mount_point. All other operations are disabled. This is
> because once the node is released they will return to
> the
> previous value before the acquire action. Networking
> should
> disallow this state because if you make a change while
> it is
> acquired they will be perminent, and will not reset upon
> release. I think it would be confusing to the user to
> provide two different outcomes from the allocated state.
>

Disagree. Landscape guys allocate machines to them for them to do
operations in the machine. It is a way for them to reserve machines so
nobody else uses them. The networking is not like storage. The way
networking can change is more flexible than storage.

> * RELEASING
> - Well it is releasing so it will go to ready no action should be
> allowed.
>

Agree.

> * DISK_ERASING
> - Similar to releasing same reasoning applies.
>

Agree.

>
> * FAILED_COMMISSIONING
> * FAILED_DEPLOYMENT
> * FAILED_RELEASING
> * FAILED_DISK_ERASING
> - All failed states require action from the user so they should be
> disabled
> as well.
>

Agree.

>
> * READY
> - Winner: Should be allowed!
>

Agree.

So the states where we can change networking are: READY, BROKEN, ALLOCATED.

@Ricardo, please address that!

--
Andres Rodriguez (RoAkSoAx)
Ubuntu Server Developer
MSc. Telecom & Networking
Systems Engineer

Revision history for this message
Ricardo Bánffy (rbanffy) wrote :

BTW, this is what I get when I run a subset of the tests and push it to review before the tests finish running... It was a very educational experience.

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

> Hi Blake,
>
>
>
> > * NEW
> > - Has not been commissioned so networking should not be changed.
> >
>
> agree
>
> > * COMMISSIONING
> > - Networking is going to be reset once finished.
> >
>
> agree
>
> * DEPLOYED
> > - Already deployed changing anything would have no affect.
> >
> agree
>
> * BROKEN
> > - Not even a usable node so manipulating the networking has no point.
> > Must
> > be placed it Ready state before it can be used.
> >
>
> Disagree. The user should be able to change his network config. In fact, he
> might have marked it broken to change things in MAAS, like adding more NICs
> or god knows, so he should be able to make changes here. Once he marks the
> machine fixed, it will be ready an usable.

I think it is weird to allow them to change the networking configuration in this state, but don't really care if its allowed.

>
>
> > * DEPLOYING
> > - Already deploying, it is too late.
> >
>
> Agreed.
>
>
> > * ALLOCATED
> > - Now this one is debatable, but I believe it needs to be consistent
> > with
> > storage. To make it consistent it should be disallowed.
> > Reasoning: Storage can be modified in this state but only fstype
> > and
> > mount_point. All other operations are disabled. This is
> > because once the node is released they will return to
> > the
> > previous value before the acquire action. Networking
> > should
> > disallow this state because if you make a change while
> > it is
> > acquired they will be perminent, and will not reset upon
> > release. I think it would be confusing to the user to
> > provide two different outcomes from the allocated state.
> >
>
> Disagree. Landscape guys allocate machines to them for them to do
> operations in the machine. It is a way for them to reserve machines so
> nobody else uses them. The networking is not like storage. The way
> networking can change is more flexible than storage.

Yeah I said this was debatable. My vision was just to not confuse the user as storage configuration will be reset, unlike networking.

>
>
> > * RELEASING
> > - Well it is releasing so it will go to ready no action should be
> > allowed.
> >
>
> Agree.
>
>
> > * DISK_ERASING
> > - Similar to releasing same reasoning applies.
> >
>
> Agree.
>
> >
> > * FAILED_COMMISSIONING
> > * FAILED_DEPLOYMENT
> > * FAILED_RELEASING
> > * FAILED_DISK_ERASING
> > - All failed states require action from the user so they should be
> > disabled
> > as well.
> >
>
> Agree.
>
> >
> > * READY
> > - Winner: Should be allowed!
> >
>
> Agree.
>
> So the states where we can change networking are: READY, BROKEN, ALLOCATED.
>
> @Ricardo, please address that!

I am good with that.

>
>
>
> --
> Andres Rodriguez (RoAkSoAx)
> Ubuntu Server Developer
> MSc. Telecom & Networking
> Systems Engineer

Revision history for this message
Ricardo Bánffy (rbanffy) wrote :

One JS style question below

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

Looks good. Just got one issue with the jQuery in the JS. Please use my suggestion which is just plain JS.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/interfaces.py'
2--- src/maasserver/api/interfaces.py 2015-10-21 21:32:35 +0000
3+++ src/maasserver/api/interfaces.py 2015-10-26 18:43:18 +0000
4@@ -68,7 +68,7 @@
5
6 def raise_error_for_invalid_state_on_allocated_operations(
7 node, user, operation):
8- if node.status != NODE_STATUS.READY:
9+ if node.status not in (NODE_STATUS.READY, NODE_STATUS.BROKEN):
10 raise NodeStateViolation(
11 "Cannot %s interface because the node is not Ready." % operation)
12
13
14=== modified file 'src/maasserver/api/tests/test_interfaces.py'
15--- src/maasserver/api/tests/test_interfaces.py 2015-10-21 21:32:35 +0000
16+++ src/maasserver/api/tests/test_interfaces.py 2015-10-26 18:43:18 +0000
17@@ -98,66 +98,70 @@
18
19 def test_create_physical(self):
20 self.become_admin()
21- node = factory.make_Node(status=NODE_STATUS.READY)
22- mac = factory.make_mac_address()
23- name = factory.make_name("eth")
24- vlan = factory.make_VLAN()
25- tags = [
26- factory.make_name("tag")
27- for _ in range(3)
28- ]
29- uri = get_node_interfaces_uri(node)
30- response = self.client.post(uri, {
31- "op": "create_physical",
32- "mac_address": mac,
33- "name": name,
34- "vlan": vlan.id,
35- "tags": ",".join(tags),
36- })
37+ for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN):
38+ node = factory.make_Node(status=status)
39+ mac = factory.make_mac_address()
40+ name = factory.make_name("eth")
41+ vlan = factory.make_VLAN()
42+ tags = [
43+ factory.make_name("tag")
44+ for _ in range(3)
45+ ]
46+ uri = get_node_interfaces_uri(node)
47+ response = self.client.post(uri, {
48+ "op": "create_physical",
49+ "mac_address": mac,
50+ "name": name,
51+ "vlan": vlan.id,
52+ "tags": ",".join(tags),
53+ })
54
55- self.assertEqual(httplib.OK, response.status_code, response.content)
56- self.assertThat(json.loads(response.content), ContainsDict({
57- "mac_address": Equals(mac),
58- "name": Equals(name),
59- "vlan": ContainsDict({
60- "id": Equals(vlan.id),
61- }),
62- "type": Equals("physical"),
63- "tags": Equals(tags),
64- "enabled": Equals(True),
65- }))
66+ self.assertEqual(
67+ httplib.OK, response.status_code, response.content)
68+ self.assertThat(json.loads(response.content), ContainsDict({
69+ "mac_address": Equals(mac),
70+ "name": Equals(name),
71+ "vlan": ContainsDict({
72+ "id": Equals(vlan.id),
73+ }),
74+ "type": Equals("physical"),
75+ "tags": Equals(tags),
76+ "enabled": Equals(True),
77+ }))
78
79 def test_create_physical_disabled(self):
80 self.become_admin()
81- node = factory.make_Node(status=NODE_STATUS.READY)
82- mac = factory.make_mac_address()
83- name = factory.make_name("eth")
84- vlan = factory.make_VLAN()
85- tags = [
86- factory.make_name("tag")
87- for _ in range(3)
88- ]
89- uri = get_node_interfaces_uri(node)
90- response = self.client.post(uri, {
91- "op": "create_physical",
92- "mac_address": mac,
93- "name": name,
94- "vlan": vlan.id,
95- "tags": ",".join(tags),
96- "enabled": False,
97- })
98+ for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN):
99+ node = factory.make_Node(status=status)
100+ mac = factory.make_mac_address()
101+ name = factory.make_name("eth")
102+ vlan = factory.make_VLAN()
103+ tags = [
104+ factory.make_name("tag")
105+ for _ in range(3)
106+ ]
107+ uri = get_node_interfaces_uri(node)
108+ response = self.client.post(uri, {
109+ "op": "create_physical",
110+ "mac_address": mac,
111+ "name": name,
112+ "vlan": vlan.id,
113+ "tags": ",".join(tags),
114+ "enabled": False,
115+ })
116
117- self.assertEqual(httplib.OK, response.status_code, response.content)
118- self.assertThat(json.loads(response.content), ContainsDict({
119- "mac_address": Equals(mac),
120- "name": Equals(name),
121- "vlan": ContainsDict({
122- "id": Equals(vlan.id),
123- }),
124- "type": Equals("physical"),
125- "tags": Equals(tags),
126- "enabled": Equals(False),
127- }))
128+ self.assertEqual(
129+ httplib.OK, response.status_code, response.content)
130+ self.assertThat(json.loads(response.content), ContainsDict({
131+ "mac_address": Equals(mac),
132+ "name": Equals(name),
133+ "vlan": ContainsDict({
134+ "id": Equals(vlan.id),
135+ }),
136+ "type": Equals("physical"),
137+ "tags": Equals(tags),
138+ "enabled": Equals(False),
139+ }))
140
141 def test_create_physical_requires_admin(self):
142 node = factory.make_Node()
143@@ -174,7 +178,7 @@
144 self.assertEqual(
145 httplib.FORBIDDEN, response.status_code, response.content)
146
147- def test_create_physical_409_when_not_ready(self):
148+ def test_create_physical_409_when_not_ready_or_broken(self):
149 self.become_admin()
150 for status in (
151 NODE_STATUS.NEW,
152@@ -186,7 +190,6 @@
153 NODE_STATUS.DEPLOYING,
154 NODE_STATUS.DEPLOYED,
155 NODE_STATUS.RETIRED,
156- NODE_STATUS.BROKEN,
157 NODE_STATUS.FAILED_DEPLOYMENT,
158 NODE_STATUS.RELEASING,
159 NODE_STATUS.FAILED_RELEASING,
160@@ -246,42 +249,44 @@
161
162 def test_create_bond(self):
163 self.become_admin()
164- node = factory.make_Node(status=NODE_STATUS.READY)
165- vlan = factory.make_VLAN()
166- parent_1_iface = factory.make_Interface(
167- INTERFACE_TYPE.PHYSICAL, vlan=vlan, node=node)
168- parent_2_iface = factory.make_Interface(
169- INTERFACE_TYPE.PHYSICAL, vlan=vlan, node=node)
170- name = factory.make_name("bond")
171- tags = [
172- factory.make_name("tag")
173- for _ in range(3)
174- ]
175- uri = get_node_interfaces_uri(node)
176- response = self.client.post(uri, {
177- "op": "create_bond",
178- "mac_address": "%s" % parent_1_iface.mac_address,
179- "name": name,
180- "vlan": vlan.id,
181- "parents": [parent_1_iface.id, parent_2_iface.id],
182- "tags": ",".join(tags),
183- })
184+ for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN):
185+ node = factory.make_Node(status=status)
186+ vlan = factory.make_VLAN()
187+ parent_1_iface = factory.make_Interface(
188+ INTERFACE_TYPE.PHYSICAL, vlan=vlan, node=node)
189+ parent_2_iface = factory.make_Interface(
190+ INTERFACE_TYPE.PHYSICAL, vlan=vlan, node=node)
191+ name = factory.make_name("bond")
192+ tags = [
193+ factory.make_name("tag")
194+ for _ in range(3)
195+ ]
196+ uri = get_node_interfaces_uri(node)
197+ response = self.client.post(uri, {
198+ "op": "create_bond",
199+ "mac_address": "%s" % parent_1_iface.mac_address,
200+ "name": name,
201+ "vlan": vlan.id,
202+ "parents": [parent_1_iface.id, parent_2_iface.id],
203+ "tags": ",".join(tags),
204+ })
205
206- self.assertEqual(httplib.OK, response.status_code, response.content)
207- parsed_interface = json.loads(response.content)
208- self.assertThat(parsed_interface, ContainsDict({
209- "mac_address": Equals("%s" % parent_1_iface.mac_address),
210- "name": Equals(name),
211- "vlan": ContainsDict({
212- "id": Equals(vlan.id),
213- }),
214- "type": Equals("bond"),
215- "tags": Equals(tags),
216- }))
217- self.assertItemsEqual([
218- parent_1_iface.name,
219- parent_2_iface.name,
220- ], parsed_interface['parents'])
221+ self.assertEqual(
222+ httplib.OK, response.status_code, response.content)
223+ parsed_interface = json.loads(response.content)
224+ self.assertThat(parsed_interface, ContainsDict({
225+ "mac_address": Equals("%s" % parent_1_iface.mac_address),
226+ "name": Equals(name),
227+ "vlan": ContainsDict({
228+ "id": Equals(vlan.id),
229+ }),
230+ "type": Equals("bond"),
231+ "tags": Equals(tags),
232+ }))
233+ self.assertItemsEqual([
234+ parent_1_iface.name,
235+ parent_2_iface.name,
236+ ], parsed_interface['parents'])
237
238 def test_create_bond_requires_admin(self):
239 node = factory.make_Node()
240@@ -302,7 +307,7 @@
241 self.assertEqual(
242 httplib.FORBIDDEN, response.status_code, response.content)
243
244- def test_create_bond_409_when_not_ready(self):
245+ def test_create_bond_409_when_not_ready_or_broken(self):
246 self.become_admin()
247 for status in (
248 NODE_STATUS.NEW,
249@@ -314,7 +319,6 @@
250 NODE_STATUS.DEPLOYING,
251 NODE_STATUS.DEPLOYED,
252 NODE_STATUS.RETIRED,
253- NODE_STATUS.BROKEN,
254 NODE_STATUS.FAILED_DEPLOYMENT,
255 NODE_STATUS.RELEASING,
256 NODE_STATUS.FAILED_RELEASING,
257@@ -524,46 +528,55 @@
258
259 def test_update_physical_interface(self):
260 self.become_admin()
261- node = factory.make_Node(status=NODE_STATUS.READY)
262- interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
263- new_name = factory.make_name("name")
264- new_vlan = factory.make_VLAN()
265- uri = get_node_interface_uri(interface)
266- response = self.client.put(uri, {
267- "name": new_name,
268- "vlan": new_vlan.id,
269- })
270- self.assertEqual(httplib.OK, response.status_code, response.content)
271- parsed_interface = json.loads(response.content)
272- self.assertEquals(new_name, parsed_interface["name"])
273- self.assertEquals(new_vlan.id, parsed_interface["vlan"]["id"])
274+ for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN):
275+ node = factory.make_Node(status=status)
276+ interface = factory.make_Interface(
277+ INTERFACE_TYPE.PHYSICAL, node=node)
278+ new_name = factory.make_name("name")
279+ new_vlan = factory.make_VLAN()
280+ uri = get_node_interface_uri(interface)
281+ response = self.client.put(uri, {
282+ "name": new_name,
283+ "vlan": new_vlan.id,
284+ })
285+ self.assertEqual(
286+ httplib.OK, response.status_code, response.content)
287+ parsed_interface = json.loads(response.content)
288+ self.assertEquals(new_name, parsed_interface["name"])
289+ self.assertEquals(new_vlan.id, parsed_interface["vlan"]["id"])
290
291 def test_update_bond_interface(self):
292 self.become_admin()
293- node = factory.make_Node(status=NODE_STATUS.READY)
294- bond, [nic_0, nic_1], [vlan_10, vlan_11] = make_complex_interface(node)
295- uri = get_node_interface_uri(bond)
296- response = self.client.put(uri, {
297- "parents": [nic_0.id],
298- })
299- self.assertEqual(httplib.OK, response.status_code, response.content)
300- parsed_interface = json.loads(response.content)
301- self.assertEquals([nic_0.name], parsed_interface["parents"])
302+ for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN):
303+ node = factory.make_Node(status=status)
304+ bond, [nic_0, nic_1], [vlan_10, vlan_11] = make_complex_interface(
305+ node)
306+ uri = get_node_interface_uri(bond)
307+ response = self.client.put(uri, {
308+ "parents": [nic_0.id],
309+ })
310+ self.assertEqual(
311+ httplib.OK, response.status_code, response.content)
312+ parsed_interface = json.loads(response.content)
313+ self.assertEquals([nic_0.name], parsed_interface["parents"])
314
315 def test_update_vlan_interface(self):
316 self.become_admin()
317- node = factory.make_Node(status=NODE_STATUS.READY)
318- bond, [nic_0, nic_1], [vlan_10, vlan_11] = make_complex_interface(node)
319- physical_interface = factory.make_Interface(
320- INTERFACE_TYPE.PHYSICAL, node=node)
321- uri = get_node_interface_uri(vlan_10)
322- response = self.client.put(uri, {
323- "parent": physical_interface.id,
324- })
325- self.assertEqual(httplib.OK, response.status_code, response.content)
326- parsed_interface = json.loads(response.content)
327- self.assertEquals(
328- [physical_interface.name], parsed_interface["parents"])
329+ for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN):
330+ node = factory.make_Node(status=status)
331+ bond, [nic_0, nic_1], [vlan_10, vlan_11] = make_complex_interface(
332+ node)
333+ physical_interface = factory.make_Interface(
334+ INTERFACE_TYPE.PHYSICAL, node=node)
335+ uri = get_node_interface_uri(vlan_10)
336+ response = self.client.put(uri, {
337+ "parent": physical_interface.id,
338+ })
339+ self.assertEqual(
340+ httplib.OK, response.status_code, response.content)
341+ parsed_interface = json.loads(response.content)
342+ self.assertEquals(
343+ [physical_interface.name], parsed_interface["parents"])
344
345 def test_update_requires_admin(self):
346 node = factory.make_Node()
347@@ -576,7 +589,7 @@
348 self.assertEqual(
349 httplib.FORBIDDEN, response.status_code, response.content)
350
351- def test_read_409_when_not_ready(self):
352+ def test_read_409_when_not_ready_or_broken(self):
353 self.become_admin()
354 for status in (
355 NODE_STATUS.NEW,
356@@ -588,7 +601,6 @@
357 NODE_STATUS.DEPLOYING,
358 NODE_STATUS.DEPLOYED,
359 NODE_STATUS.RETIRED,
360- NODE_STATUS.BROKEN,
361 NODE_STATUS.FAILED_DEPLOYMENT,
362 NODE_STATUS.RELEASING,
363 NODE_STATUS.FAILED_RELEASING,
364@@ -608,13 +620,14 @@
365
366 def test_delete_deletes_interface(self):
367 self.become_admin()
368- node = factory.make_Node(interface=True, status=NODE_STATUS.READY)
369- interface = node.get_boot_interface()
370- uri = get_node_interface_uri(interface)
371- response = self.client.delete(uri)
372- self.assertEqual(
373- httplib.NO_CONTENT, response.status_code, response.content)
374- self.assertIsNone(reload_object(interface))
375+ for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN):
376+ node = factory.make_Node(interface=True, status=status)
377+ interface = node.get_boot_interface()
378+ uri = get_node_interface_uri(interface)
379+ response = self.client.delete(uri)
380+ self.assertEqual(
381+ httplib.NO_CONTENT, response.status_code, response.content)
382+ self.assertIsNone(reload_object(interface))
383
384 def test_delete_403_when_not_admin(self):
385 node = factory.make_Node(interface=True)
386@@ -634,7 +647,7 @@
387 self.assertEqual(
388 httplib.NOT_FOUND, response.status_code, response.content)
389
390- def test_delete_409_when_not_ready(self):
391+ def test_delete_409_when_not_ready_or_broken(self):
392 self.become_admin()
393 for status in (
394 NODE_STATUS.NEW,
395@@ -646,7 +659,6 @@
396 NODE_STATUS.DEPLOYING,
397 NODE_STATUS.DEPLOYED,
398 NODE_STATUS.RETIRED,
399- NODE_STATUS.BROKEN,
400 NODE_STATUS.FAILED_DEPLOYMENT,
401 NODE_STATUS.RELEASING,
402 NODE_STATUS.FAILED_RELEASING,
403@@ -665,33 +677,36 @@
404 # This just tests that the form is saved and the updated interface
405 # is returned.
406 self.become_admin()
407- node = factory.make_Node(interface=True, status=NODE_STATUS.READY)
408- interface = node.get_boot_interface()
409- uri = get_node_interface_uri(interface)
410- response = self.client.post(uri, {
411- "op": "link_subnet",
412- "mode": INTERFACE_LINK_TYPE.DHCP,
413- })
414- self.assertEqual(httplib.OK, response.status_code, response.content)
415- parsed_response = json.loads(response.content)
416- self.assertThat(
417- parsed_response["links"][0], ContainsDict({
418- "mode": Equals(INTERFACE_LINK_TYPE.DHCP),
419- }))
420+ for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN):
421+ node = factory.make_Node(interface=True, status=status)
422+ interface = node.get_boot_interface()
423+ uri = get_node_interface_uri(interface)
424+ response = self.client.post(uri, {
425+ "op": "link_subnet",
426+ "mode": INTERFACE_LINK_TYPE.DHCP,
427+ })
428+ self.assertEqual(
429+ httplib.OK, response.status_code, response.content)
430+ parsed_response = json.loads(response.content)
431+ self.assertThat(
432+ parsed_response["links"][0], ContainsDict({
433+ "mode": Equals(INTERFACE_LINK_TYPE.DHCP),
434+ }))
435
436 def test_link_subnet_raises_error(self):
437 self.become_admin()
438- node = factory.make_Node(interface=True, status=NODE_STATUS.READY)
439- interface = node.get_boot_interface()
440- uri = get_node_interface_uri(interface)
441- response = self.client.post(uri, {
442- "op": "link_subnet",
443- })
444- self.assertEqual(
445- httplib.BAD_REQUEST, response.status_code, response.content)
446- self.assertEquals({
447- "mode": ["This field is required."]
448- }, json.loads(response.content))
449+ for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN):
450+ node = factory.make_Node(interface=True, status=status)
451+ interface = node.get_boot_interface()
452+ uri = get_node_interface_uri(interface)
453+ response = self.client.post(uri, {
454+ "op": "link_subnet",
455+ })
456+ self.assertEqual(
457+ httplib.BAD_REQUEST, response.status_code, response.content)
458+ self.assertEquals({
459+ "mode": ["This field is required."]
460+ }, json.loads(response.content))
461
462 def test_link_subnet_requries_admin(self):
463 node = factory.make_Node(interface=True)
464@@ -703,7 +718,7 @@
465 self.assertEqual(
466 httplib.FORBIDDEN, response.status_code, response.content)
467
468- def test_link_subnet_409_when_not_ready(self):
469+ def test_link_subnet_409_when_not_ready_or_broken(self):
470 self.become_admin()
471 for status in (
472 NODE_STATUS.NEW,
473@@ -715,7 +730,6 @@
474 NODE_STATUS.DEPLOYING,
475 NODE_STATUS.DEPLOYED,
476 NODE_STATUS.RETIRED,
477- NODE_STATUS.BROKEN,
478 NODE_STATUS.FAILED_DEPLOYMENT,
479 NODE_STATUS.RELEASING,
480 NODE_STATUS.FAILED_RELEASING,
481@@ -737,33 +751,36 @@
482 # This just tests that the form is saved and the updated interface
483 # is returned.
484 self.become_admin()
485- node = factory.make_Node(interface=True, status=NODE_STATUS.READY)
486- interface = node.get_boot_interface()
487- subnet = factory.make_Subnet()
488- dhcp_ip = factory.make_StaticIPAddress(
489- alloc_type=IPADDRESS_TYPE.DHCP, ip="",
490- subnet=subnet, interface=interface)
491- uri = get_node_interface_uri(interface)
492- response = self.client.post(uri, {
493- "op": "unlink_subnet",
494- "id": dhcp_ip.id,
495- })
496- self.assertEqual(httplib.OK, response.status_code, response.content)
497- self.assertIsNone(reload_object(dhcp_ip))
498+ for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN):
499+ node = factory.make_Node(interface=True, status=status)
500+ interface = node.get_boot_interface()
501+ subnet = factory.make_Subnet()
502+ dhcp_ip = factory.make_StaticIPAddress(
503+ alloc_type=IPADDRESS_TYPE.DHCP, ip="",
504+ subnet=subnet, interface=interface)
505+ uri = get_node_interface_uri(interface)
506+ response = self.client.post(uri, {
507+ "op": "unlink_subnet",
508+ "id": dhcp_ip.id,
509+ })
510+ self.assertEqual(
511+ httplib.OK, response.status_code, response.content)
512+ self.assertIsNone(reload_object(dhcp_ip))
513
514 def test_unlink_subnet_raises_error(self):
515 self.become_admin()
516- node = factory.make_Node(interface=True, status=NODE_STATUS.READY)
517- interface = node.get_boot_interface()
518- uri = get_node_interface_uri(interface)
519- response = self.client.post(uri, {
520- "op": "unlink_subnet",
521- })
522- self.assertEqual(
523- httplib.BAD_REQUEST, response.status_code, response.content)
524- self.assertEquals({
525- "id": ["This field is required."]
526- }, json.loads(response.content))
527+ for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN):
528+ node = factory.make_Node(interface=True, status=status)
529+ interface = node.get_boot_interface()
530+ uri = get_node_interface_uri(interface)
531+ response = self.client.post(uri, {
532+ "op": "unlink_subnet",
533+ })
534+ self.assertEqual(
535+ httplib.BAD_REQUEST, response.status_code, response.content)
536+ self.assertEquals({
537+ "id": ["This field is required."]
538+ }, json.loads(response.content))
539
540 def test_unlink_subnet_requries_admin(self):
541 node = factory.make_Node(interface=True)
542@@ -775,7 +792,7 @@
543 self.assertEqual(
544 httplib.FORBIDDEN, response.status_code, response.content)
545
546- def test_unlink_subnet_409_when_not_ready(self):
547+ def test_unlink_subnet_409_when_not_ready_or_broken(self):
548 self.become_admin()
549 for status in (
550 NODE_STATUS.NEW,
551@@ -787,7 +804,6 @@
552 NODE_STATUS.DEPLOYING,
553 NODE_STATUS.DEPLOYED,
554 NODE_STATUS.RETIRED,
555- NODE_STATUS.BROKEN,
556 NODE_STATUS.FAILED_DEPLOYMENT,
557 NODE_STATUS.RELEASING,
558 NODE_STATUS.FAILED_RELEASING,
559@@ -807,55 +823,60 @@
560 # The form that is used is fully tested in test_forms_interface_link.
561 # This just tests that the form is saved and the node link is created.
562 self.become_admin()
563- node = factory.make_Node(interface=True, status=NODE_STATUS.READY)
564- interface = node.get_boot_interface()
565- network = factory.make_ipv4_network()
566- subnet = factory.make_Subnet(
567- cidr=unicode(network.cidr), vlan=interface.vlan)
568- link_ip = factory.make_StaticIPAddress(
569- alloc_type=IPADDRESS_TYPE.AUTO, ip="",
570- subnet=subnet, interface=interface)
571- uri = get_node_interface_uri(interface)
572- response = self.client.post(uri, {
573- "op": "set_default_gateway",
574- "link_id": link_ip.id
575- })
576- self.assertEqual(httplib.OK, response.status_code, response.content)
577- self.assertEqual(link_ip, reload_object(node).gateway_link_ipv4)
578+ for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN):
579+ node = factory.make_Node(interface=True, status=status)
580+ interface = node.get_boot_interface()
581+ network = factory.make_ipv4_network()
582+ subnet = factory.make_Subnet(
583+ cidr=unicode(network.cidr), vlan=interface.vlan)
584+ link_ip = factory.make_StaticIPAddress(
585+ alloc_type=IPADDRESS_TYPE.AUTO, ip="",
586+ subnet=subnet, interface=interface)
587+ uri = get_node_interface_uri(interface)
588+ response = self.client.post(uri, {
589+ "op": "set_default_gateway",
590+ "link_id": link_ip.id
591+ })
592+ self.assertEqual(
593+ httplib.OK, response.status_code, response.content)
594+ self.assertEqual(link_ip, reload_object(node).gateway_link_ipv4)
595
596 def test_set_default_gateway_sets_gateway_link_ipv6_on_node(self):
597 # The form that is used is fully tested in test_forms_interface_link.
598 # This just tests that the form is saved and the node link is created.
599 self.become_admin()
600- node = factory.make_Node(interface=True, status=NODE_STATUS.READY)
601- interface = node.get_boot_interface()
602- network = factory.make_ipv6_network()
603- subnet = factory.make_Subnet(
604- cidr=unicode(network.cidr), vlan=interface.vlan)
605- link_ip = factory.make_StaticIPAddress(
606- alloc_type=IPADDRESS_TYPE.AUTO, ip="",
607- subnet=subnet, interface=interface)
608- uri = get_node_interface_uri(interface)
609- response = self.client.post(uri, {
610- "op": "set_default_gateway",
611- "link_id": link_ip.id
612- })
613- self.assertEqual(httplib.OK, response.status_code, response.content)
614- self.assertEqual(link_ip, reload_object(node).gateway_link_ipv6)
615+ for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN):
616+ node = factory.make_Node(interface=True, status=status)
617+ interface = node.get_boot_interface()
618+ network = factory.make_ipv6_network()
619+ subnet = factory.make_Subnet(
620+ cidr=unicode(network.cidr), vlan=interface.vlan)
621+ link_ip = factory.make_StaticIPAddress(
622+ alloc_type=IPADDRESS_TYPE.AUTO, ip="",
623+ subnet=subnet, interface=interface)
624+ uri = get_node_interface_uri(interface)
625+ response = self.client.post(uri, {
626+ "op": "set_default_gateway",
627+ "link_id": link_ip.id
628+ })
629+ self.assertEqual(
630+ httplib.OK, response.status_code, response.content)
631+ self.assertEqual(link_ip, reload_object(node).gateway_link_ipv6)
632
633 def test_set_default_gateway_raises_error(self):
634 self.become_admin()
635- node = factory.make_Node(interface=True, status=NODE_STATUS.READY)
636- interface = node.get_boot_interface()
637- uri = get_node_interface_uri(interface)
638- response = self.client.post(uri, {
639- "op": "set_default_gateway",
640- })
641- self.assertEqual(
642- httplib.BAD_REQUEST, response.status_code, response.content)
643- self.assertEquals({
644- "__all__": ["This interface has no usable gateways."]
645- }, json.loads(response.content))
646+ for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN):
647+ node = factory.make_Node(interface=True, status=status)
648+ interface = node.get_boot_interface()
649+ uri = get_node_interface_uri(interface)
650+ response = self.client.post(uri, {
651+ "op": "set_default_gateway",
652+ })
653+ self.assertEqual(
654+ httplib.BAD_REQUEST, response.status_code, response.content)
655+ self.assertEquals({
656+ "__all__": ["This interface has no usable gateways."]
657+ }, json.loads(response.content))
658
659 def test_set_default_gateway_requries_admin(self):
660 node = factory.make_Node(interface=True)
661@@ -867,7 +888,7 @@
662 self.assertEqual(
663 httplib.FORBIDDEN, response.status_code, response.content)
664
665- def test_set_default_gateway_409_when_not_ready(self):
666+ def test_set_default_gateway_409_when_not_ready_or_broken(self):
667 self.become_admin()
668 for status in (
669 NODE_STATUS.NEW,
670@@ -879,7 +900,6 @@
671 NODE_STATUS.DEPLOYING,
672 NODE_STATUS.DEPLOYED,
673 NODE_STATUS.RETIRED,
674- NODE_STATUS.BROKEN,
675 NODE_STATUS.FAILED_DEPLOYMENT,
676 NODE_STATUS.RELEASING,
677 NODE_STATUS.FAILED_RELEASING,
678
679=== modified file 'src/maasserver/static/js/angular/controllers/node_details_networking.js'
680--- src/maasserver/static/js/angular/controllers/node_details_networking.js 2015-10-15 19:36:22 +0000
681+++ src/maasserver/static/js/angular/controllers/node_details_networking.js 2015-10-26 18:43:18 +0000
682@@ -83,11 +83,11 @@
683
684 angular.module('MAAS').controller('NodeNetworkingController', [
685 '$scope', '$filter', 'FabricsManager', 'VLANsManager', 'SubnetsManager',
686- 'NodesManager', 'GeneralManager', 'ManagerHelperService',
687+ 'NodesManager', 'GeneralManager', 'UsersManager', 'ManagerHelperService',
688 'ValidationService',
689 function(
690 $scope, $filter, FabricsManager, VLANsManager, SubnetsManager,
691- NodesManager, GeneralManager, ManagerHelperService,
692+ NodesManager, GeneralManager, UsersManager, ManagerHelperService,
693 ValidationService) {
694
695 // Different interface types.
696@@ -122,7 +122,7 @@
697 var SELECTION_MODE = {
698 NONE: null,
699 SINGLE: "single",
700- MUTLI: "multi",
701+ MULTI: "multi",
702 DELETE: "delete",
703 ADD: "add",
704 CREATE_BOND: "create-bond"
705@@ -408,6 +408,33 @@
706 updateLoaded();
707 };
708
709+ // Return true if user is a super user/
710+ $scope.isSuperUser = function() {
711+ var authUser = UsersManager.getAuthUser();
712+ if(!angular.isObject(authUser)) {
713+ return false;
714+ }
715+ return authUser.is_superuser;
716+ };
717+
718+ // Return true if the networking information cannot be edited.
719+ // (it can't be changed when the node is in any state other
720+ // than Ready or Broken and the user is not a superuser)
721+ $scope.isAllNetworkingDisabled = function() {
722+ if (!$scope.isSuperUser()) {
723+ // If the user is not a superuser, disable the networking panel.
724+ return true;
725+ } else if (angular.isObject($scope.node) &&
726+ ["Ready", "Broken"].indexOf($scope.node.status) === -1) {
727+ // If the node is not ready or broken, disable networking panel.
728+ return true;
729+ } else {
730+ // User must be a superuser and the node must be
731+ // either ready or broken. Enable it.
732+ return false;
733+ }
734+ };
735+
736 // Get the text for the type of the interface.
737 $scope.getInterfaceTypeText = function(nic) {
738 var text = INTERFACE_TYPE_TEXTS[nic.type];
739@@ -682,7 +709,7 @@
740
741 if($scope.selectedInterfaces.length > 1) {
742 if($scope.selectedMode !== SELECTION_MODE.BOND) {
743- $scope.selectedMode = SELECTION_MODE.MUTLI;
744+ $scope.selectedMode = SELECTION_MODE.MULTI;
745 }
746 } else if($scope.selectedInterfaces.length === 1) {
747 $scope.selectedMode = SELECTION_MODE.SINGLE;
748@@ -786,7 +813,7 @@
749 $scope.newInterface = {};
750 $scope.newBondInterface = {};
751 if($scope.selectedMode === SELECTION_MODE.CREATE_BOND) {
752- $scope.selectedMode = SELECTION_MODE.MUTLI;
753+ $scope.selectedMode = SELECTION_MODE.MULTI;
754 } else {
755 $scope.selectedMode = SELECTION_MODE.SINGLE;
756 }
757@@ -935,20 +962,25 @@
758 }
759 };
760
761- // Return true if this interface should be disabled in the list. Only
762+ // Return true if the networking information cannot be edited
763+ // or if this interface should be disabled in the list. Only
764 // returns true when in create bond mode.
765 $scope.isDisabled = function() {
766- return (
767- $scope.selectedMode !== SELECTION_MODE.NONE &&
768- $scope.selectedMode !== SELECTION_MODE.SINGLE &&
769- $scope.selectedMode !== SELECTION_MODE.MUTLI);
770+ if ($scope.isAllNetworkingDisabled()) {
771+ return true;
772+ } else {
773+ return (
774+ $scope.selectedMode !== SELECTION_MODE.NONE &&
775+ $scope.selectedMode !== SELECTION_MODE.SINGLE &&
776+ $scope.selectedMode !== SELECTION_MODE.MULTI);
777+ }
778 };
779
780 // Return true when a bond can be created based on the current
781 // selection. Only can be done if no aliases are selected and all
782 // selected interfaces are on the same VLAN.
783 $scope.canCreateBond = function() {
784- if($scope.selectedMode !== SELECTION_MODE.MUTLI) {
785+ if($scope.selectedMode !== SELECTION_MODE.MULTI) {
786 return false;
787 }
788 var interfaces = getSelectedInterfaces();
789@@ -974,7 +1006,7 @@
790
791 // Show the create bond view.
792 $scope.showCreateBond = function() {
793- if($scope.selectedMode === SELECTION_MODE.MUTLI &&
794+ if($scope.selectedMode === SELECTION_MODE.MULTI &&
795 $scope.canCreateBond()) {
796 $scope.selectedMode = SELECTION_MODE.CREATE_BOND;
797
798@@ -1069,7 +1101,8 @@
799 ManagerHelperService.loadManagers([
800 FabricsManager,
801 VLANsManager,
802- SubnetsManager
803+ SubnetsManager,
804+ UsersManager
805 ]).then(function() {
806 $scope.managersHaveLoaded = true;
807 updateLoaded();
808
809=== modified file 'src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js'
810--- src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js 2015-10-23 01:40:32 +0000
811+++ src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js 2015-10-26 18:43:18 +0000
812@@ -182,18 +182,18 @@
813 }));
814
815 // Load the required dependencies for the NodeNetworkingController.
816- var FabricsManager, VLANsManager, SubnetsManager, NodesManager;
817- var GeneralManager, ManagerHelperService;
818+ var FabricsManager, VLANsManager, SubnetsManager, UsersManager;
819+ var NodesManager, GeneralManager, ManagerHelperService;
820 beforeEach(inject(function($injector) {
821 FabricsManager = $injector.get("FabricsManager");
822 VLANsManager = $injector.get("VLANsManager");
823 SubnetsManager = $injector.get("SubnetsManager");
824 NodesManager = $injector.get("NodesManager");
825 GeneralManager = $injector.get("GeneralManager");
826+ UsersManager = $injector.get("UsersManager");
827 ManagerHelperService = $injector.get("ManagerHelperService");
828 }));
829
830- // Create the node on the parent.
831 var node;
832 beforeEach(function() {
833 node = {
834@@ -2626,6 +2626,9 @@
835
836 it("returns false when in none, single, or multi mode", function() {
837 var controller = makeController();
838+ $scope.isSuperUser = function() { return true; };
839+ // Node needs to be Ready or Broken for the mode to be considered.
840+ $scope.node = {status: "Ready"};
841 $scope.selectedMode = null;
842 expect($scope.isDisabled()).toBe(false);
843 $scope.selectedMode = "single";
844@@ -2636,6 +2639,9 @@
845
846 it("returns true when in delete, add, or create modes", function() {
847 var controller = makeController();
848+ $scope.isSuperUser = function() { return true; };
849+ // Node needs to be Ready or Broken for the mode to be considered.
850+ $scope.node = {status: "Ready"};
851 $scope.selectedMode = "create-bond";
852 expect($scope.isDisabled()).toBe(true);
853 $scope.selectedMode = "add";
854@@ -2643,6 +2649,96 @@
855 $scope.selectedMode = "delete";
856 expect($scope.isDisabled()).toBe(true);
857 });
858+
859+ it("returns true when the node state is not 'Ready' or 'Broken'",
860+ function() {
861+ var controller = makeController();
862+ $scope.isSuperUser = function() { return true; };
863+ $scope.node = {status: "Ready"};
864+ expect($scope.isDisabled()).toBe(false);
865+ $scope.node = {status: "Broken"};
866+ expect($scope.isDisabled()).toBe(false);
867+ ["New",
868+ "Commissioning",
869+ "Failed commissioning",
870+ "Missing",
871+ "Reserved",
872+ "Allocated",
873+ "Deploying",
874+ "Deployed",
875+ "Retired",
876+ "Failed deployment",
877+ "Releasing",
878+ "Releasing failed",
879+ "Disk erasing",
880+ "Failed disk erasing"].forEach(function (s) {
881+ $scope.node = {state: s};
882+ expect($scope.isDisabled()).toBe(true);
883+ });
884+ });
885+
886+ it("returns true if the user is not a superuser", function() {
887+ var controller = makeController();
888+ $scope.isSuperUser = function() { return false; };
889+ $scope.node = {status: "Ready"};
890+ expect($scope.isDisabled()).toBe(true);
891+ $scope.node = {status: "Broken"};
892+ expect($scope.isDisabled()).toBe(true);
893+ });
894+ });
895+
896+ describe("isSuperUser", function() {
897+ it("returns true if the user is a superuser", function() {
898+ var controller = makeController();
899+ spyOn(UsersManager, "getAuthUser").and.returnValue(
900+ { is_superuser: true });
901+ expect($scope.isSuperUser()).toBe(true);
902+ });
903+
904+ it("returns false if the user is not a superuser", function() {
905+ var controller = makeController();
906+ spyOn(UsersManager, "getAuthUser").and.returnValue(
907+ { is_superuser: false });
908+ expect($scope.isSuperUser()).toBe(false);
909+ });
910+ });
911+
912+ describe("isAllNetworkingDisabled", function() {
913+ it("returns true if the user is not a superuser and the node is ready",
914+ function() {
915+ var controller = makeController();
916+ spyOn(UsersManager, "getAuthUser").and.returnValue(
917+ { is_superuser: false });
918+ expect($scope.isSuperUser()).toBe(false);
919+ expect($scope.isAllNetworkingDisabled()).toBe(true);
920+ });
921+
922+ it("return false if the node is Ready and we are a superuser",
923+ function() {
924+ var controller = makeController();
925+ spyOn(UsersManager, "getAuthUser").and.returnValue(
926+ { is_superuser: true });
927+ $scope.node.status = "Ready";
928+ expect($scope.isAllNetworkingDisabled()).toBe(false);
929+ });
930+
931+ it("return false if the node is broken and we are a superuser",
932+ function() {
933+ var controller = makeController();
934+ spyOn(UsersManager, "getAuthUser").and.returnValue(
935+ { is_superuser: true });
936+ $scope.node.status = "Broken";
937+ expect($scope.isAllNetworkingDisabled()).toBe(false);
938+ });
939+
940+ it("return true if the node is deploying and we are a superuser",
941+ function() {
942+ var controller = makeController();
943+ spyOn(UsersManager, "getAuthUser").and.returnValue(
944+ { is_superuser: true });
945+ $scope.node.status = "Deploying";
946+ expect($scope.isAllNetworkingDisabled()).toBe(true);
947+ });
948 });
949
950 describe("canCreateBond", function() {
951
952=== modified file 'src/maasserver/static/partials/node-details.html'
953--- src/maasserver/static/partials/node-details.html 2015-10-26 09:48:55 +0000
954+++ src/maasserver/static/partials/node-details.html 2015-10-26 18:43:18 +0000
955@@ -316,6 +316,13 @@
956 <form class="ng-hide" data-ng-show="loaded">
957 <div class="twelve-col">
958 <h2 class="title">Network</h2>
959+ <div class="twelve-col error ng-hide" data-ng-show="isAllNetworkingDisabled() && isSuperUser()">
960+ <ul class="flash-messages">
961+ <li class="flash-messages__item info">
962+ Network configuration cannot be modified unless the node is Ready or Broken.
963+ </li>
964+ </ul>
965+ </div>
966 <div class="table margin-bottom">
967 <header class="table__head">
968 <div class="table__row">
969@@ -344,11 +351,13 @@
970 data-ng-class="{ active: isInterfaceSelected(interface), disabled: isDisabled(), selected: !isOnlyInterfaceSelected(interface) }"
971 data-ng-repeat="interface in interfaces | removeBondParents:newBondInterface">
972 <div class="table__data table__column--3">
973- <input class="checkbox" type="checkbox" id="{$ getUniqueKey(interface) $}"
974- data-ng-checked="isInterfaceSelected(interface)"
975- data-ng-click="toggleInterfaceSelect(interface)"
976- data-ng-disabled="isDisabled()" />
977- <label class="checkbox-label" for="{$ getUniqueKey(interface) $}"></label>
978+ <span data-ng-hide="isAllNetworkingDisabled()">
979+ <input class="checkbox" type="checkbox" id="{$ getUniqueKey(interface) $}"
980+ data-ng-checked="isInterfaceSelected(interface)"
981+ data-ng-click="toggleInterfaceSelect(interface)"
982+ data-ng-disabled="isDisabled()">
983+ <label class="checkbox-label" for="{$ getUniqueKey(interface) $}"></label>
984+ </span>
985 </div>
986 <div class="table__data table__column--15" data-ng-show="column == 'name'">
987 <span class="ng-hide" data-ng-show="interface.type == 'alias' || interface.type == 'vlan'">{$ interface.name $}</span>
988@@ -595,7 +604,7 @@
989 <a class="link-cta-ubuntu secondary margin-right"
990 data-ng-class="{ disabled: !canCreateBond() }"
991 data-ng-click="showCreateBond()"
992- data-ng-hide="isShowingCreateBond()">Create Bond</a>
993+ data-ng-hide="isAllNetworkingDisabled() || isShowingCreateBond()">Create Bond</a>
994 </div>
995 </form>
996 </div>