Merge lp:~rbanffy/maas/1508059-node-if-config--on-ready-ui into lp:~maas-committers/maas/trunk
- 1508059-node-if-config--on-ready-ui
- Merge into trunk
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 |
Related bugs: |
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".
Description of the change
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.
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.
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
* 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_
* 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!
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://
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.
Christian Reis (kiko) wrote : | # |
What about assigned/allocated?
Blake Rouse (blake-rouse) wrote : | # |
Uhm... I described why...
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.
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_
> * 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
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.
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_
> > * 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
Ricardo Bánffy (rbanffy) wrote : | # |
One JS style question below
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.
Preview Diff
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> |
Other htan the comment update, looks good.