Merge lp:~blake-rouse/maas/node-networking-link into lp:~maas-committers/maas/trunk

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: no longer in the source branch.
Merged at revision: 4337
Proposed branch: lp:~blake-rouse/maas/node-networking-link
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 2142 lines (+1529/-130)
10 files modified
src/maasserver/models/interface.py (+134/-10)
src/maasserver/models/tests/test_interface.py (+503/-1)
src/maasserver/static/js/angular/controllers/node_details_networking.js (+175/-24)
src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js (+530/-58)
src/maasserver/static/js/angular/factories/nodes.js (+14/-2)
src/maasserver/static/js/angular/factories/tests/test_nodes.js (+45/-1)
src/maasserver/static/partials/node-details.html (+21/-22)
src/maasserver/websockets/handlers/node.py (+33/-3)
src/maasserver/websockets/handlers/tests/test_node.py (+54/-5)
src/maastesting/factory.py (+20/-4)
To merge this branch: bzr merge lp:~blake-rouse/maas/node-networking-link
Reviewer Review Type Date Requested Status
Andres Rodriguez (community) Approve
Review via email: mp+273002@code.launchpad.net

Commit message

Add the ability to change the link mode of an interface or alias in the UI. Provide the backend work to allow making this changed without changing the link_id. Order the links for each interface by ID so the order never changes. Clean up the unmountFilesystem websocket handler name and fix the naming of subnets and vlans in the dropdowns.

Description of the change

This is a rather large change just because the backend could not handle the interaction that the UI wanted. Also includes some small fixes and HTML and JS.

I have fully tested this from packaging and it fully works.

To post a comment you must log in.
Revision history for this message
Andres Rodriguez (andreserl) wrote :

lgtm! I have not done an indepth review but looks good so far!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/models/interface.py'
2--- src/maasserver/models/interface.py 2015-09-24 16:22:12 +0000
3+++ src/maasserver/models/interface.py 2015-10-01 14:33:47 +0000
4@@ -601,7 +601,8 @@
5 return ip_address
6
7 def _link_subnet_static(
8- self, subnet, ip_address=None, alloc_type=None, user=None):
9+ self, subnet, ip_address=None, alloc_type=None, user=None,
10+ swap_static_ip=None):
11 """Link interface to subnet using STATIC."""
12 valid_alloc_types = [
13 IPADDRESS_TYPE.STICKY,
14@@ -617,12 +618,8 @@
15 user = None
16
17 ngi = None
18- has_allocations = False
19 if subnet is not None:
20 ngi = subnet.get_managed_cluster_interface()
21- if ngi is not None:
22- has_allocations = self._has_static_allocation_on_cluster(
23- ngi.nodegroup, get_subnet_family(subnet))
24
25 if ip_address:
26 ip_address = IPAddress(ip_address)
27@@ -664,10 +661,20 @@
28 None, None, alloc_type=alloc_type, subnet=subnet, user=user)
29 self.ip_addresses.add(static_ip)
30
31+ # Swap the ID's that way it keeps the same ID as the swap object.
32+ if swap_static_ip is not None:
33+ static_ip.id, swap_static_ip.id = swap_static_ip.id, static_ip.id
34+ swap_static_ip.delete()
35+ static_ip.save()
36+
37 # Need to update the hostmaps on the cluster, if this subnet
38 # has a managed interface.
39- if ngi is not None and not has_allocations:
40- self._update_host_maps(ngi.nodegroup, static_ip)
41+ if ngi is not None:
42+ allocated_ip = (
43+ self._get_first_static_allocation_for_cluster(
44+ ngi.nodegroup, get_subnet_family(subnet)))
45+ if allocated_ip is None or allocated_ip.id == static_ip.id:
46+ self._update_host_maps(ngi.nodegroup, static_ip)
47
48 # Was successful at creating the STATIC link. Remove the DHCP and
49 # LINK_UP link if it exists.
50@@ -756,7 +763,8 @@
51 subnet = None
52 self.link_subnet(INTERFACE_LINK_TYPE.LINK_UP, subnet)
53
54- def _unlink_static_ip(self, static_ip, update_cluster=True):
55+ def _unlink_static_ip(
56+ self, static_ip, update_cluster=True, swap_alloc_type=None):
57 """Unlink the STATIC IP address from the interface."""
58 registered_on_cluster = False
59 ngi = None
60@@ -771,14 +779,23 @@
61 # Need to remove the hostmap on the cluster before it can
62 # be deleted.
63 self._remove_host_maps(ngi.nodegroup, static_ip)
64- static_ip.delete()
65+
66+ # If the allocation type is only changing then we don't need to delete
67+ # the IP address it needs to be updated.
68+ ip_version = IPAddress(static_ip.ip).version
69+ if swap_alloc_type is not None:
70+ static_ip.alloc_type = swap_alloc_type
71+ static_ip.ip = None
72+ static_ip.save()
73+ else:
74+ static_ip.delete()
75
76 # If this IP address was registered on the cluster and now has been
77 # deleted we need to register the next assigned IP address to the
78 # cluster hostmap.
79 if registered_on_cluster and ngi is not None and update_cluster:
80 new_hostmap_ip = self._get_first_static_allocation_for_cluster(
81- ngi.nodegroup, IPAddress(static_ip.ip).version)
82+ ngi.nodegroup, ip_version)
83 if new_hostmap_ip is not None:
84 self._update_host_maps(ngi.nodegroup, new_hostmap_ip)
85
86@@ -787,6 +804,7 @@
87 self._update_dns_zones([ngi.nodegroup])
88 else:
89 self._update_dns_zones()
90+ return static_ip
91
92 def unlink_ip_address(
93 self, ip_address, update_cluster=True, clearing_config=False):
94@@ -815,6 +833,112 @@
95 ip_address = self.ip_addresses.get(id=link_id)
96 self.unlink_ip_address(ip_address)
97
98+ def _swap_subnet(self, static_ip, subnet, ip_address=None):
99+ """Swap the subnet for the `static_ip`."""
100+ # Check that requested `ip_address` is available.
101+ if ip_address is not None:
102+ already_used = get_one(
103+ StaticIPAddress.objects.filter(ip=ip_address))
104+ if already_used is not None:
105+ raise StaticIPAddressUnavailable(
106+ "IP address is already in use.")
107+
108+ # Remove the hostmap on the new subnet.
109+ new_subnet_ngi = subnet.get_managed_cluster_interface()
110+ if new_subnet_ngi is not None:
111+ static_ip_on_new_subnet = (
112+ self._get_first_static_allocation_for_cluster(
113+ new_subnet_ngi.nodegroup, get_subnet_family(subnet)))
114+ if (static_ip_on_new_subnet is not None and
115+ static_ip_on_new_subnet.id > static_ip.id):
116+ # The updated static_id should be registered over the other
117+ # IP address registered on the new subnet.
118+ self._remove_host_maps(
119+ new_subnet_ngi.nodegroup, static_ip_on_new_subnet)
120+
121+ # If the subnets are different then remove the hostmap from the old
122+ # subnet as well.
123+ if static_ip.subnet is not None and static_ip.subnet != subnet:
124+ old_subnet_ngi = static_ip.subnet.get_managed_cluster_interface()
125+ registered_on_cluster = False
126+ if old_subnet_ngi is not None:
127+ registered_on_cluster = (
128+ self._is_first_static_allocation_on_cluster(
129+ static_ip, old_subnet_ngi.nodegroup))
130+ if registered_on_cluster:
131+ self._remove_host_maps(old_subnet_ngi.nodegroup, static_ip)
132+
133+ # Clear the subnet before checking which is the next hostmap.
134+ static_ip.subnet = None
135+ static_ip.save()
136+
137+ # Register the new STATIC IP address for the old subnet.
138+ if registered_on_cluster and old_subnet_ngi is not None:
139+ new_hostmap_ip = self._get_first_static_allocation_for_cluster(
140+ old_subnet_ngi.nodegroup, IPAddress(static_ip.ip).version)
141+ if new_hostmap_ip is not None:
142+ self._update_host_maps(
143+ old_subnet_ngi.nodegroup, new_hostmap_ip)
144+
145+ # Update the DNS configuration for the old subnet if needed.
146+ if old_subnet_ngi is not None:
147+ self._update_dns_zones([old_subnet_ngi.nodegroup])
148+
149+ # If the IP addresses are on the same subnet but the IP's are
150+ # different then we need to remove the hostmap.
151+ if (static_ip.subnet == subnet and
152+ new_subnet_ngi is not None and
153+ static_ip.ip != ip_address):
154+ self._remove_host_maps(
155+ new_subnet_ngi.nodegroup, static_ip)
156+
157+ # Link to the new subnet, which will also update the hostmap.
158+ return self._link_subnet_static(
159+ subnet, ip_address=ip_address, swap_static_ip=static_ip)
160+
161+ def update_ip_address(
162+ self, static_ip, mode, subnet, ip_address=None):
163+ """Update an already existing link on interface to be the new data."""
164+ if mode == INTERFACE_LINK_TYPE.AUTO:
165+ new_alloc_type = IPADDRESS_TYPE.AUTO
166+ elif mode == INTERFACE_LINK_TYPE.DHCP:
167+ new_alloc_type = IPADDRESS_TYPE.DHCP
168+ elif mode in [INTERFACE_LINK_TYPE.LINK_UP, INTERFACE_LINK_TYPE.STATIC]:
169+ new_alloc_type = IPADDRESS_TYPE.STICKY
170+
171+ current_mode = static_ip.get_interface_link_type()
172+ if current_mode == INTERFACE_LINK_TYPE.STATIC:
173+ if mode == INTERFACE_LINK_TYPE.STATIC:
174+ if (static_ip.subnet == subnet and (
175+ ip_address is None or static_ip.ip == ip_address)):
176+ # Same subnet and IP address nothing to do.
177+ return static_ip
178+ # Update the subent and IP address for the static assignment.
179+ return self._swap_subnet(
180+ static_ip, subnet, ip_address=ip_address)
181+ else:
182+ # Not staying in the same mode so we can just remove the
183+ # static IP and change its alloc_type from STICKY.
184+ static_ip = self._unlink_static_ip(
185+ static_ip, swap_alloc_type=new_alloc_type)
186+ elif mode == INTERFACE_LINK_TYPE.STATIC:
187+ # Linking to the subnet statically were the original was not a
188+ # static link. Swap the objects so the object keeps the same ID.
189+ return self._link_subnet_static(
190+ subnet, ip_address=ip_address,
191+ swap_static_ip=static_ip)
192+ static_ip.alloc_type = new_alloc_type
193+ static_ip.ip = None
194+ static_ip.subnet = subnet
195+ static_ip.save()
196+ return static_ip
197+
198+ def update_link_by_id(self, link_id, mode, subnet, ip_address=None):
199+ """Update the `IPAddress` link on interface by its ID."""
200+ static_ip = self.ip_addresses.get(id=link_id)
201+ return self.update_ip_address(
202+ static_ip, mode, subnet, ip_address=ip_address)
203+
204 def clear_all_links(self, clearing_config=False):
205 """Remove all the `IPAddress` link on the interface."""
206 for ip_address in self.ip_addresses.exclude(
207
208=== modified file 'src/maasserver/models/tests/test_interface.py'
209--- src/maasserver/models/tests/test_interface.py 2015-09-24 16:22:12 +0000
210+++ src/maasserver/models/tests/test_interface.py 2015-10-01 14:33:47 +0000
211@@ -29,7 +29,10 @@
212 NODEGROUP_STATUS,
213 NODEGROUPINTERFACE_MANAGEMENT,
214 )
215-from maasserver.exceptions import StaticIPAddressOutOfRange
216+from maasserver.exceptions import (
217+ StaticIPAddressOutOfRange,
218+ StaticIPAddressUnavailable,
219+)
220 from maasserver.models import (
221 Fabric,
222 interface as interface_module,
223@@ -67,6 +70,7 @@
224 IPNetwork,
225 IPRange,
226 )
227+from testtools import ExpectedException
228 from testtools.matchers import (
229 Equals,
230 MatchesDict,
231@@ -1318,6 +1322,504 @@
232 alloc_type=IPADDRESS_TYPE.STICKY, ip=None).first())
233
234
235+class TestUpdateIPAddress(MAASServerTestCase):
236+ """Tests for `Interface.update_ip_address`."""
237+
238+ def test__switch_dhcp_to_auto(self):
239+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
240+ subnet = factory.make_Subnet(vlan=interface.vlan)
241+ static_ip = factory.make_StaticIPAddress(
242+ alloc_type=IPADDRESS_TYPE.DHCP, ip="",
243+ subnet=subnet, interface=interface)
244+ static_id = static_ip.id
245+ new_subnet = factory.make_Subnet(vlan=interface.vlan)
246+ static_ip = interface.update_ip_address(
247+ static_ip, INTERFACE_LINK_TYPE.AUTO, new_subnet)
248+ self.assertEquals(static_id, static_ip.id)
249+ self.assertEquals(IPADDRESS_TYPE.AUTO, static_ip.alloc_type)
250+ self.assertEquals(new_subnet, static_ip.subnet)
251+ self.assertIsNone(static_ip.ip)
252+
253+ def test__switch_dhcp_to_link_up(self):
254+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
255+ subnet = factory.make_Subnet(vlan=interface.vlan)
256+ static_ip = factory.make_StaticIPAddress(
257+ alloc_type=IPADDRESS_TYPE.DHCP, ip="",
258+ subnet=subnet, interface=interface)
259+ static_id = static_ip.id
260+ new_subnet = factory.make_Subnet(vlan=interface.vlan)
261+ static_ip = interface.update_ip_address(
262+ static_ip, INTERFACE_LINK_TYPE.LINK_UP, new_subnet)
263+ self.assertEquals(static_id, static_ip.id)
264+ self.assertEquals(IPADDRESS_TYPE.STICKY, static_ip.alloc_type)
265+ self.assertEquals(new_subnet, static_ip.subnet)
266+ self.assertIsNone(static_ip.ip)
267+
268+ def test__switch_dhcp_to_static(self):
269+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
270+ nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
271+ network_v4 = factory.make_ipv4_network(slash=24)
272+ subnet = factory.make_Subnet(
273+ vlan=interface.vlan, cidr=unicode(network_v4.cidr))
274+ factory.make_NodeGroupInterface(
275+ nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP,
276+ subnet=subnet)
277+ static_ip = factory.make_StaticIPAddress(
278+ alloc_type=IPADDRESS_TYPE.DHCP, ip="",
279+ subnet=subnet, interface=interface)
280+ static_id = static_ip.id
281+ network_v6 = factory.make_ipv6_network(slash=24)
282+ new_subnet = factory.make_Subnet(
283+ vlan=interface.vlan, cidr=unicode(network_v6.cidr))
284+ factory.make_NodeGroupInterface(
285+ nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP,
286+ subnet=new_subnet)
287+ mock_update_host_maps = self.patch_autospec(
288+ interface, "_update_host_maps")
289+ mock_update_dns_zones = self.patch_autospec(
290+ interface, "_update_dns_zones")
291+ static_ip = interface.update_ip_address(
292+ static_ip, INTERFACE_LINK_TYPE.STATIC, new_subnet)
293+ self.assertEquals(static_id, static_ip.id)
294+ self.assertEquals(IPADDRESS_TYPE.STICKY, static_ip.alloc_type)
295+ self.assertEquals(new_subnet, static_ip.subnet)
296+ self.assertIsNotNone(static_ip.ip)
297+ self.assertThat(
298+ mock_update_host_maps, MockCalledOnceWith(nodegroup, static_ip))
299+ self.assertThat(
300+ mock_update_dns_zones, MockCalledOnceWith([nodegroup]))
301+
302+ def test__switch_auto_to_dhcp(self):
303+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
304+ subnet = factory.make_Subnet(vlan=interface.vlan)
305+ static_ip = factory.make_StaticIPAddress(
306+ alloc_type=IPADDRESS_TYPE.AUTO, ip="",
307+ subnet=subnet, interface=interface)
308+ static_id = static_ip.id
309+ new_subnet = factory.make_Subnet(vlan=interface.vlan)
310+ static_ip = interface.update_ip_address(
311+ static_ip, INTERFACE_LINK_TYPE.DHCP, new_subnet)
312+ self.assertEquals(static_id, static_ip.id)
313+ self.assertEquals(IPADDRESS_TYPE.DHCP, static_ip.alloc_type)
314+ self.assertEquals(new_subnet, static_ip.subnet)
315+ self.assertIsNone(static_ip.ip)
316+
317+ def test__switch_auto_to_link_up(self):
318+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
319+ subnet = factory.make_Subnet(vlan=interface.vlan)
320+ static_ip = factory.make_StaticIPAddress(
321+ alloc_type=IPADDRESS_TYPE.AUTO, ip="",
322+ subnet=subnet, interface=interface)
323+ static_id = static_ip.id
324+ new_subnet = factory.make_Subnet(vlan=interface.vlan)
325+ static_ip = interface.update_ip_address(
326+ static_ip, INTERFACE_LINK_TYPE.LINK_UP, new_subnet)
327+ self.assertEquals(static_id, static_ip.id)
328+ self.assertEquals(IPADDRESS_TYPE.STICKY, static_ip.alloc_type)
329+ self.assertEquals(new_subnet, static_ip.subnet)
330+ self.assertIsNone(static_ip.ip)
331+
332+ def test__switch_auto_to_static(self):
333+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
334+ nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
335+ network_v4 = factory.make_ipv4_network(slash=24)
336+ subnet = factory.make_Subnet(
337+ vlan=interface.vlan, cidr=unicode(network_v4.cidr))
338+ factory.make_NodeGroupInterface(
339+ nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP,
340+ subnet=subnet)
341+ static_ip = factory.make_StaticIPAddress(
342+ alloc_type=IPADDRESS_TYPE.AUTO, ip="",
343+ subnet=subnet, interface=interface)
344+ static_id = static_ip.id
345+ network_v6 = factory.make_ipv6_network(slash=24)
346+ new_subnet = factory.make_Subnet(
347+ vlan=interface.vlan, cidr=unicode(network_v6.cidr))
348+ factory.make_NodeGroupInterface(
349+ nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP,
350+ subnet=new_subnet)
351+ mock_update_host_maps = self.patch_autospec(
352+ interface, "_update_host_maps")
353+ mock_update_dns_zones = self.patch_autospec(
354+ interface, "_update_dns_zones")
355+ static_ip = interface.update_ip_address(
356+ static_ip, INTERFACE_LINK_TYPE.STATIC, new_subnet)
357+ self.assertEquals(static_id, static_ip.id)
358+ self.assertEquals(IPADDRESS_TYPE.STICKY, static_ip.alloc_type)
359+ self.assertEquals(new_subnet, static_ip.subnet)
360+ self.assertIsNotNone(static_ip.ip)
361+ self.assertThat(
362+ mock_update_host_maps, MockCalledOnceWith(nodegroup, static_ip))
363+ self.assertThat(
364+ mock_update_dns_zones, MockCalledOnceWith([nodegroup]))
365+
366+ def test__switch_link_up_to_auto(self):
367+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
368+ subnet = factory.make_Subnet(vlan=interface.vlan)
369+ static_ip = factory.make_StaticIPAddress(
370+ alloc_type=IPADDRESS_TYPE.STICKY, ip="",
371+ subnet=subnet, interface=interface)
372+ static_id = static_ip.id
373+ new_subnet = factory.make_Subnet(vlan=interface.vlan)
374+ static_ip = interface.update_ip_address(
375+ static_ip, INTERFACE_LINK_TYPE.AUTO, new_subnet)
376+ self.assertEquals(static_id, static_ip.id)
377+ self.assertEquals(IPADDRESS_TYPE.AUTO, static_ip.alloc_type)
378+ self.assertEquals(new_subnet, static_ip.subnet)
379+ self.assertIsNone(static_ip.ip)
380+
381+ def test__switch_link_up_to_dhcp(self):
382+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
383+ subnet = factory.make_Subnet(vlan=interface.vlan)
384+ static_ip = factory.make_StaticIPAddress(
385+ alloc_type=IPADDRESS_TYPE.STICKY, ip="",
386+ subnet=subnet, interface=interface)
387+ static_id = static_ip.id
388+ new_subnet = factory.make_Subnet(vlan=interface.vlan)
389+ static_ip = interface.update_ip_address(
390+ static_ip, INTERFACE_LINK_TYPE.DHCP, new_subnet)
391+ self.assertEquals(static_id, static_ip.id)
392+ self.assertEquals(IPADDRESS_TYPE.DHCP, static_ip.alloc_type)
393+ self.assertEquals(new_subnet, static_ip.subnet)
394+ self.assertIsNone(static_ip.ip)
395+
396+ def test__switch_link_up_to_static(self):
397+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
398+ nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
399+ network_v4 = factory.make_ipv4_network(slash=24)
400+ subnet = factory.make_Subnet(
401+ vlan=interface.vlan, cidr=unicode(network_v4.cidr))
402+ factory.make_NodeGroupInterface(
403+ nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP,
404+ subnet=subnet)
405+ static_ip = factory.make_StaticIPAddress(
406+ alloc_type=IPADDRESS_TYPE.STICKY, ip="",
407+ subnet=subnet, interface=interface)
408+ static_id = static_ip.id
409+ network_v6 = factory.make_ipv6_network(slash=24)
410+ new_subnet = factory.make_Subnet(
411+ vlan=interface.vlan, cidr=unicode(network_v6.cidr))
412+ factory.make_NodeGroupInterface(
413+ nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP,
414+ subnet=new_subnet)
415+ mock_update_host_maps = self.patch_autospec(
416+ interface, "_update_host_maps")
417+ mock_update_dns_zones = self.patch_autospec(
418+ interface, "_update_dns_zones")
419+ static_ip = interface.update_ip_address(
420+ static_ip, INTERFACE_LINK_TYPE.STATIC, new_subnet)
421+ self.assertEquals(static_id, static_ip.id)
422+ self.assertEquals(IPADDRESS_TYPE.STICKY, static_ip.alloc_type)
423+ self.assertEquals(new_subnet, static_ip.subnet)
424+ self.assertIsNotNone(static_ip.ip)
425+ self.assertThat(
426+ mock_update_host_maps, MockCalledOnceWith(nodegroup, static_ip))
427+ self.assertThat(
428+ mock_update_dns_zones, MockCalledOnceWith([nodegroup]))
429+
430+ def test__switch_static_to_dhcp(self):
431+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
432+ nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
433+ subnet = factory.make_Subnet(vlan=interface.vlan)
434+ ngi = factory.make_NodeGroupInterface(
435+ nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP,
436+ subnet=subnet)
437+ static_ip = factory.make_StaticIPAddress(
438+ alloc_type=IPADDRESS_TYPE.STICKY,
439+ ip=factory.pick_ip_in_static_range(ngi),
440+ subnet=subnet, interface=interface)
441+ static_id = static_ip.id
442+ new_subnet = factory.make_Subnet(vlan=interface.vlan)
443+ mock_remove_host_maps = self.patch_autospec(
444+ interface, "_remove_host_maps")
445+ mock_update_dns_zones = self.patch_autospec(
446+ interface, "_update_dns_zones")
447+ static_ip = interface.update_ip_address(
448+ static_ip, INTERFACE_LINK_TYPE.DHCP, new_subnet)
449+ self.assertEquals(static_id, static_ip.id)
450+ self.assertEquals(IPADDRESS_TYPE.DHCP, static_ip.alloc_type)
451+ self.assertEquals(new_subnet, static_ip.subnet)
452+ self.assertIsNone(static_ip.ip)
453+ self.assertThat(
454+ mock_remove_host_maps, MockCalledOnceWith(nodegroup, static_ip))
455+ self.assertThat(
456+ mock_update_dns_zones, MockCalledOnceWith([nodegroup]))
457+
458+ def test__switch_static_to_auto(self):
459+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
460+ nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
461+ subnet = factory.make_Subnet(vlan=interface.vlan)
462+ ngi = factory.make_NodeGroupInterface(
463+ nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP,
464+ subnet=subnet)
465+ static_ip = factory.make_StaticIPAddress(
466+ alloc_type=IPADDRESS_TYPE.STICKY,
467+ ip=factory.pick_ip_in_static_range(ngi),
468+ subnet=subnet, interface=interface)
469+ static_id = static_ip.id
470+ new_subnet = factory.make_Subnet(vlan=interface.vlan)
471+ mock_remove_host_maps = self.patch_autospec(
472+ interface, "_remove_host_maps")
473+ mock_update_dns_zones = self.patch_autospec(
474+ interface, "_update_dns_zones")
475+ static_ip = interface.update_ip_address(
476+ static_ip, INTERFACE_LINK_TYPE.AUTO, new_subnet)
477+ self.assertEquals(static_id, static_ip.id)
478+ self.assertEquals(IPADDRESS_TYPE.AUTO, static_ip.alloc_type)
479+ self.assertEquals(new_subnet, static_ip.subnet)
480+ self.assertIsNone(static_ip.ip)
481+ self.assertThat(
482+ mock_remove_host_maps, MockCalledOnceWith(nodegroup, static_ip))
483+ self.assertThat(
484+ mock_update_dns_zones, MockCalledOnceWith([nodegroup]))
485+
486+ def test__switch_static_to_link_up(self):
487+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
488+ nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
489+ subnet = factory.make_Subnet(vlan=interface.vlan)
490+ ngi = factory.make_NodeGroupInterface(
491+ nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP,
492+ subnet=subnet)
493+ static_ip = factory.make_StaticIPAddress(
494+ alloc_type=IPADDRESS_TYPE.STICKY,
495+ ip=factory.pick_ip_in_static_range(ngi),
496+ subnet=subnet, interface=interface)
497+ static_id = static_ip.id
498+ new_subnet = factory.make_Subnet(vlan=interface.vlan)
499+ mock_remove_host_maps = self.patch_autospec(
500+ interface, "_remove_host_maps")
501+ mock_update_dns_zones = self.patch_autospec(
502+ interface, "_update_dns_zones")
503+ static_ip = interface.update_ip_address(
504+ static_ip, INTERFACE_LINK_TYPE.LINK_UP, new_subnet)
505+ self.assertEquals(static_id, static_ip.id)
506+ self.assertEquals(IPADDRESS_TYPE.STICKY, static_ip.alloc_type)
507+ self.assertEquals(new_subnet, static_ip.subnet)
508+ self.assertIsNone(static_ip.ip)
509+ self.assertThat(
510+ mock_remove_host_maps, MockCalledOnceWith(nodegroup, static_ip))
511+ self.assertThat(
512+ mock_update_dns_zones, MockCalledOnceWith([nodegroup]))
513+
514+ def test__switch_static_to_same_subnet_does_nothing(self):
515+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
516+ nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
517+ subnet = factory.make_Subnet(vlan=interface.vlan)
518+ ngi = factory.make_NodeGroupInterface(
519+ nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP,
520+ subnet=subnet)
521+ static_ip = factory.make_StaticIPAddress(
522+ alloc_type=IPADDRESS_TYPE.STICKY,
523+ ip=factory.pick_ip_in_static_range(ngi),
524+ subnet=subnet, interface=interface)
525+ static_id = static_ip.id
526+ static_ip_address = static_ip.ip
527+ mock_remove_host_maps = self.patch_autospec(
528+ interface, "_remove_host_maps")
529+ mock_update_host_maps = self.patch_autospec(
530+ interface, "_update_host_maps")
531+ mock_update_dns_zones = self.patch_autospec(
532+ interface, "_update_dns_zones")
533+ static_ip = interface.update_ip_address(
534+ static_ip, INTERFACE_LINK_TYPE.STATIC, subnet)
535+ self.assertEquals(static_id, static_ip.id)
536+ self.assertEquals(IPADDRESS_TYPE.STICKY, static_ip.alloc_type)
537+ self.assertEquals(subnet, static_ip.subnet)
538+ self.assertEquals(static_ip_address, static_ip.ip)
539+ self.assertThat(mock_remove_host_maps, MockNotCalled())
540+ self.assertThat(mock_update_host_maps, MockNotCalled())
541+ self.assertThat(mock_update_dns_zones, MockNotCalled())
542+
543+ def test__switch_static_to_already_used_ip_address(self):
544+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
545+ nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
546+ subnet = factory.make_Subnet(vlan=interface.vlan)
547+ ngi = factory.make_NodeGroupInterface(
548+ nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP,
549+ subnet=subnet)
550+ static_ip = factory.make_StaticIPAddress(
551+ alloc_type=IPADDRESS_TYPE.STICKY,
552+ ip=factory.pick_ip_in_static_range(ngi),
553+ subnet=subnet, interface=interface)
554+ other_interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
555+ used_ip_address = factory.pick_ip_in_static_range(
556+ ngi, but_not=[static_ip.ip])
557+ factory.make_StaticIPAddress(
558+ alloc_type=IPADDRESS_TYPE.STICKY,
559+ ip=used_ip_address,
560+ subnet=subnet, interface=other_interface)
561+ with ExpectedException(StaticIPAddressUnavailable):
562+ interface.update_ip_address(
563+ static_ip, INTERFACE_LINK_TYPE.STATIC, subnet,
564+ ip_address=used_ip_address)
565+
566+ def test__switch_static_to_same_subnet_with_different_ip(self):
567+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
568+ nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
569+ network = factory.make_ipv4_network(slash=24)
570+ subnet = factory.make_Subnet(
571+ vlan=interface.vlan, cidr=unicode(network.cidr))
572+ ngi = factory.make_NodeGroupInterface(
573+ nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP,
574+ subnet=subnet)
575+ static_ip = factory.make_StaticIPAddress(
576+ alloc_type=IPADDRESS_TYPE.STICKY,
577+ ip=factory.pick_ip_in_static_range(ngi),
578+ subnet=subnet, interface=interface)
579+ static_id = static_ip.id
580+ static_ip_address = static_ip.ip
581+ new_ip_address = factory.pick_ip_in_static_range(
582+ ngi, but_not=[static_ip_address])
583+ mock_remove_host_maps = self.patch_autospec(
584+ interface, "_remove_host_maps")
585+ mock_update_host_maps = self.patch_autospec(
586+ interface, "_update_host_maps")
587+ mock_update_dns_zones = self.patch_autospec(
588+ interface, "_update_dns_zones")
589+ new_static_ip = interface.update_ip_address(
590+ static_ip, INTERFACE_LINK_TYPE.STATIC, subnet,
591+ ip_address=new_ip_address)
592+ self.assertEquals(static_id, new_static_ip.id)
593+ self.assertEquals(IPADDRESS_TYPE.STICKY, new_static_ip.alloc_type)
594+ self.assertEquals(subnet, new_static_ip.subnet)
595+ self.assertEquals(new_ip_address, new_static_ip.ip)
596+ # The remove actual loads the IP address from the database so it
597+ # is not the same object. We just need to check that the IP's match.
598+ self.assertEquals(nodegroup, mock_remove_host_maps.call_args[0][0])
599+ self.assertEquals(
600+ static_ip_address, mock_remove_host_maps.call_args[0][1].ip)
601+ self.assertThat(
602+ mock_update_host_maps,
603+ MockCalledOnceWith(nodegroup, new_static_ip))
604+ self.assertThat(
605+ mock_update_dns_zones, MockCalledOnceWith([nodegroup]))
606+
607+ def test__switch_static_to_another_subnet(self):
608+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
609+ nodegroup_v4 = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
610+ network_v4 = factory.make_ipv4_network(slash=24)
611+ subnet = factory.make_Subnet(
612+ vlan=interface.vlan, cidr=unicode(network_v4.cidr))
613+ ngi = factory.make_NodeGroupInterface(
614+ nodegroup_v4, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP,
615+ subnet=subnet)
616+ static_ip = factory.make_StaticIPAddress(
617+ alloc_type=IPADDRESS_TYPE.STICKY,
618+ ip=factory.pick_ip_in_static_range(ngi),
619+ subnet=subnet, interface=interface)
620+ other_static_ip = factory.make_StaticIPAddress(
621+ alloc_type=IPADDRESS_TYPE.STICKY,
622+ ip=factory.pick_ip_in_static_range(ngi, but_not=[static_ip.id]),
623+ subnet=subnet, interface=interface)
624+ static_id = static_ip.id
625+ nodegroup_v6 = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
626+ network_v6 = factory.make_ipv6_network(slash=24)
627+ new_subnet = factory.make_Subnet(
628+ vlan=interface.vlan, cidr=unicode(network_v6.cidr))
629+ new_ngi = factory.make_NodeGroupInterface(
630+ nodegroup_v6, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP,
631+ subnet=new_subnet)
632+ new_subnet_static_ip = factory.make_StaticIPAddress(
633+ alloc_type=IPADDRESS_TYPE.STICKY,
634+ ip=factory.pick_ip_in_static_range(new_ngi),
635+ subnet=new_subnet, interface=interface)
636+ mock_remove_host_maps = self.patch_autospec(
637+ interface, "_remove_host_maps")
638+ mock_update_host_maps = self.patch_autospec(
639+ interface, "_update_host_maps")
640+ mock_update_dns_zones = self.patch_autospec(
641+ interface, "_update_dns_zones")
642+ new_static_ip = interface.update_ip_address(
643+ static_ip, INTERFACE_LINK_TYPE.STATIC, new_subnet)
644+ self.assertEquals(static_id, new_static_ip.id)
645+ self.assertEquals(IPADDRESS_TYPE.STICKY, new_static_ip.alloc_type)
646+ self.assertEquals(new_subnet, new_static_ip.subnet)
647+ self.assertIsNotNone(new_static_ip.ip)
648+ self.assertThat(
649+ mock_remove_host_maps,
650+ MockCallsMatch(
651+ call(nodegroup_v6, new_subnet_static_ip),
652+ call(nodegroup_v4, static_ip),
653+ ))
654+ self.assertThat(
655+ mock_update_host_maps,
656+ MockCallsMatch(
657+ call(nodegroup_v4, other_static_ip),
658+ call(nodegroup_v6, new_static_ip),
659+ ))
660+ self.assertThat(
661+ mock_update_dns_zones,
662+ MockCallsMatch(
663+ call([nodegroup_v4]),
664+ call([nodegroup_v6]),
665+ ))
666+
667+ def test__switch_static_to_another_subnet_with_ip_address(self):
668+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
669+ nodegroup_v4 = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
670+ network_v4 = factory.make_ipv4_network(slash=24)
671+ subnet = factory.make_Subnet(
672+ vlan=interface.vlan, cidr=unicode(network_v4.cidr))
673+ ngi = factory.make_NodeGroupInterface(
674+ nodegroup_v4, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP,
675+ subnet=subnet)
676+ static_ip = factory.make_StaticIPAddress(
677+ alloc_type=IPADDRESS_TYPE.STICKY,
678+ ip=factory.pick_ip_in_static_range(ngi),
679+ subnet=subnet, interface=interface)
680+ static_id = static_ip.id
681+ nodegroup_v6 = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
682+ network_v6 = factory.make_ipv6_network(slash=24)
683+ new_subnet = factory.make_Subnet(
684+ vlan=interface.vlan, cidr=unicode(network_v6.cidr))
685+ new_ngi = factory.make_NodeGroupInterface(
686+ nodegroup_v6, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP,
687+ subnet=new_subnet)
688+ new_ip_address = factory.pick_ip_in_static_range(new_ngi)
689+ mock_remove_host_maps = self.patch_autospec(
690+ interface, "_remove_host_maps")
691+ mock_update_host_maps = self.patch_autospec(
692+ interface, "_update_host_maps")
693+ mock_update_dns_zones = self.patch_autospec(
694+ interface, "_update_dns_zones")
695+ new_static_ip = interface.update_ip_address(
696+ static_ip, INTERFACE_LINK_TYPE.STATIC, new_subnet,
697+ ip_address=new_ip_address)
698+ self.assertEquals(static_id, new_static_ip.id)
699+ self.assertEquals(IPADDRESS_TYPE.STICKY, new_static_ip.alloc_type)
700+ self.assertEquals(new_subnet, new_static_ip.subnet)
701+ self.assertEquals(new_ip_address, new_static_ip.ip)
702+ self.assertThat(
703+ mock_remove_host_maps,
704+ MockCalledOnceWith(nodegroup_v4, static_ip))
705+ self.assertThat(
706+ mock_update_host_maps,
707+ MockCalledOnceWith(nodegroup_v6, new_static_ip))
708+ self.assertThat(
709+ mock_update_dns_zones,
710+ MockCallsMatch(
711+ call([nodegroup_v4]),
712+ call([nodegroup_v6]),
713+ ))
714+
715+
716+class TestUpdateLinkById(MAASServerTestCase):
717+ """Tests for `Interface.update_link_by_id`."""
718+
719+ def test__calls_update_ip_address_with_ip_address(self):
720+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
721+ subnet = factory.make_Subnet(vlan=interface.vlan)
722+ static_ip = factory.make_StaticIPAddress(
723+ alloc_type=IPADDRESS_TYPE.DHCP, ip="",
724+ subnet=subnet, interface=interface)
725+ mock_update_ip_address = self.patch_autospec(
726+ interface, "update_ip_address")
727+ interface.update_link_by_id(
728+ static_ip.id, INTERFACE_LINK_TYPE.AUTO, subnet)
729+ self.expectThat(mock_update_ip_address, MockCalledOnceWith(
730+ static_ip, INTERFACE_LINK_TYPE.AUTO, subnet, ip_address=None))
731+
732+
733 class TestClaimAutoIPs(MAASServerTestCase):
734 """Tests for `Interface.claim_auto_ips`."""
735
736
737=== modified file 'src/maasserver/static/js/angular/controllers/node_details_networking.js'
738--- src/maasserver/static/js/angular/controllers/node_details_networking.js 2015-09-29 15:19:25 +0000
739+++ src/maasserver/static/js/angular/controllers/node_details_networking.js 2015-10-01 14:33:47 +0000
740@@ -6,10 +6,10 @@
741
742 angular.module('MAAS').controller('NodeNetworkingController', [
743 '$scope', 'FabricsManager', 'VLANsManager', 'SubnetsManager',
744- 'NodesManager', 'ManagerHelperService',
745+ 'NodesManager', 'ManagerHelperService', 'ValidationService',
746 function(
747 $scope, FabricsManager, VLANsManager, SubnetsManager, NodesManager,
748- ManagerHelperService) {
749+ ManagerHelperService, ValidationService) {
750
751 // Different interface types.
752 var INTERFACE_TYPE = {
753@@ -36,7 +36,7 @@
754 "auto": "Auto assign",
755 "static": "Static assign",
756 "dhcp": "DHCP",
757- "link_up": "Unconfigured"
758+ "link_up": "No IP"
759 };
760
761 // Set the initial values for this scope.
762@@ -46,6 +46,7 @@
763 $scope.column = 'name';
764 $scope.fabrics = FabricsManager.getItems();
765 $scope.vlans = VLANsManager.getItems();
766+ $scope.subnets = SubnetsManager.getItems();
767 $scope.interfaces = [];
768 $scope.interfaceLinksMap = {};
769 $scope.originalInterfaces = {};
770@@ -119,7 +120,7 @@
771 // disabled or has no links (which means the interface
772 // is in LINK_UP mode).
773 nic.link_id = -1;
774- nic.subnet_id = null;
775+ nic.subnet = null;
776 nic.mode = LINK_MODE.LINK_UP;
777 nic.ip_address = "";
778 interfaces.push(nic);
779@@ -128,9 +129,13 @@
780 angular.forEach(nic.links, function(link) {
781 var nic_copy = angular.copy(nic);
782 nic_copy.link_id = link.id;
783- nic_copy.subnet_id = link.subnet_id;
784+ nic_copy.subnet = SubnetsManager.getItemFromList(
785+ link.subnet_id);
786 nic_copy.mode = link.mode;
787 nic_copy.ip_address = link.ip_address;
788+ if(angular.isUndefined(nic_copy.ip_address)) {
789+ nic_copy.ip_address = "";
790+ }
791 // We don't want to deep copy the VLAN and fabric
792 // object so we set those back to the original.
793 nic_copy.vlan = nic.vlan;
794@@ -176,6 +181,23 @@
795 }
796 }
797
798+ // Return the original link object for the given interface.
799+ function mapNICToOriginalLink(nic) {
800+ var originalInteface = $scope.originalInterfaces[nic.id];
801+ if(angular.isObject(originalInteface)) {
802+ var i, link = null;
803+ for(i = 0; i < originalInteface.links.length; i++) {
804+ link = originalInteface.links[i];
805+ if(link.id === nic.link_id) {
806+ break;
807+ }
808+ }
809+ return link;
810+ } else {
811+ return null;
812+ }
813+ }
814+
815 // Called by $parent when the node has been loaded.
816 $scope.nodeLoaded = function() {
817 $scope.$watch("node.interfaces", updateInterfaces);
818@@ -203,22 +225,25 @@
819 }
820 };
821
822- // Get the subnet for the interface.
823- $scope.getSubnet = function(nic) {
824- return SubnetsManager.getItemFromList(nic.subnet_id);
825- };
826-
827- // Get the name of the subnet for this interface.
828- $scope.getSubnetName = function(nic) {
829- if(angular.isNumber(nic.subnet_id)) {
830- var subnet = $scope.getSubnet(nic);
831- if(angular.isObject(subnet)) {
832- return subnet.name;
833- } else {
834- return "Unknown";
835- }
836+ // Get the text to display in the VLAN dropdown.
837+ $scope.getVLANText = function(vlan) {
838+ if(angular.isString(vlan.name) && vlan.name.length > 0) {
839+ return vlan.vid + " (" + vlan.name + ")";
840 } else {
841+ return vlan.vid;
842+ }
843+ };
844+
845+ // Get the text to display in the subnet dropdown.
846+ $scope.getSubnetText = function(subnet) {
847+ if(!angular.isObject(subnet)) {
848 return "Unconfigured";
849+ } else if(angular.isString(subnet.name) &&
850+ subnet.name.length > 0 &&
851+ subnet.cidr !== subnet.name) {
852+ return subnet.cidr + " (" + subnet.name + ")";
853+ } else {
854+ return subnet.cidr;
855 }
856 };
857
858@@ -240,10 +265,10 @@
859 // Save the following interface on the node. This will only save if
860 // the interface has changed.
861 $scope.saveInterface = function(nic) {
862- // If the name or vlan has changed then we need to update
863- // the interface.
864 var originalInteface = $scope.originalInterfaces[nic.id];
865- if(originalInteface.name !== nic.name ||
866+ if($scope.isInterfaceNameInvalid(nic)) {
867+ nic.name = originalInteface.name;
868+ } else if(originalInteface.name !== nic.name ||
869 originalInteface.vlan_id !== nic.vlan.id) {
870 var params = {
871 "name": nic.name,
872@@ -255,6 +280,10 @@
873 // we need to expose this as a better message to the
874 // user.
875 console.log(error);
876+
877+ // Update the interfaces so it is back to the way it
878+ // was before the user changed it.
879+ updateInterfaces();
880 });
881 }
882 };
883@@ -268,10 +297,16 @@
884 // if it has changed.
885 $scope.clearFocusInterface = function(nic) {
886 if(angular.isUndefined(nic)) {
887- $scope.saveInterface($scope.focusInterface);
888+ if($scope.focusInterface.type !== INTERFACE_TYPE.ALIAS) {
889+ $scope.saveInterface($scope.focusInterface);
890+ }
891+ $scope.saveInterfaceIPAddress($scope.focusInterface);
892 $scope.focusInterface = null;
893 } else if($scope.focusInterface === nic) {
894- $scope.saveInterface($scope.focusInterface);
895+ if($scope.focusInterface.type !== INTERFACE_TYPE.ALIAS) {
896+ $scope.saveInterface($scope.focusInterface);
897+ }
898+ $scope.saveInterfaceIPAddress($scope.focusInterface);
899 $scope.focusInterface = null;
900 }
901 };
902@@ -300,6 +335,122 @@
903 $scope.saveInterface(nic);
904 };
905
906+ // Return True if the link mode select should be disabled.
907+ $scope.isLinkModeDisabled = function(nic) {
908+ // This is only disabled when a subnet has not been selected.
909+ return !angular.isObject(nic.subnet);
910+ };
911+
912+ // Get the available link modes for an interface.
913+ $scope.getLinkModes = function(nic) {
914+ modes = [];
915+ if(!angular.isObject(nic.subnet)) {
916+ // No subnet is configure so the only allowed mode
917+ // is 'link_up'.
918+ modes.push({
919+ "mode": LINK_MODE.LINK_UP,
920+ "text": LINK_MODE_TEXTS[LINK_MODE.LINK_UP]
921+ });
922+ } else {
923+ angular.forEach(LINK_MODE_TEXTS, function(text, mode) {
924+ // Don't add LINK_UP or DHCP if more than one link exists.
925+ if(nic.links.length > 1 && (
926+ mode === LINK_MODE.LINK_UP ||
927+ mode === LINK_MODE.DHCP)) {
928+ return;
929+ }
930+ modes.push({
931+ "mode": mode,
932+ "text": text
933+ });
934+ });
935+ }
936+ return modes;
937+ };
938+
939+ // Called when the link mode for this interface and link has been
940+ // changed.
941+ $scope.saveInterfaceLink = function(nic) {
942+ var params = {
943+ "mode": nic.mode
944+ };
945+ if(angular.isObject(nic.subnet)) {
946+ params.subnet = nic.subnet.id;
947+ }
948+ if(nic.link_id >= 0) {
949+ params.link_id = nic.link_id;
950+ }
951+ if(nic.mode === LINK_MODE.STATIC && nic.ip_address.length > 0) {
952+ params.ip_address = nic.ip_address;
953+ }
954+ NodesManager.linkSubnet($scope.node, nic.id, params).then(
955+ null, function(error) {
956+ // XXX blake_r: Just log the error in the console, but
957+ // we need to expose this as a better message to the
958+ // user.
959+ console.log(error);
960+
961+ // Update the interfaces so it is back to the way it
962+ // was before the user changed it.
963+ updateInterfaces();
964+ });
965+ };
966+
967+ // Called when the user changes the subnet.
968+ $scope.subnetChanged = function(nic) {
969+ if(!angular.isObject(nic.subnet)) {
970+ // Set to 'Unconfigured' so the link mode should be set to
971+ // 'link_up'.
972+ nic.mode = LINK_MODE.LINK_UP;
973+ }
974+ // Clear the IP address so a new one on the subnet is assigned.
975+ nic.ip_address = "";
976+ $scope.saveInterfaceLink(nic);
977+ };
978+
979+ // Return True when the IP address input field should be shown.
980+ $scope.shouldShowIPAddress = function(nic) {
981+ if(nic.mode === LINK_MODE.STATIC) {
982+ // Check that the original has an IP address if it doesn't then
983+ // it should not be shown as the IP address still has not been
984+ // loaded over the websocket. If the subnets have been switched
985+ // then the IP address has been clear, don't show the IP
986+ // address until the original subnet and nic subnet match.
987+ var originalLink = mapNICToOriginalLink(nic);
988+ return (
989+ angular.isObject(originalLink) &&
990+ angular.isString(originalLink.ip_address) &&
991+ originalLink.ip_address.length > 0 &&
992+ angular.isObject(nic.subnet) &&
993+ nic.subnet.id === originalLink.subnet_id);
994+ } else if(angular.isString(nic.ip_address) &&
995+ nic.ip_address.length > 0) {
996+ return true;
997+ } else {
998+ return false;
999+ }
1000+ };
1001+
1002+ // Return True if the interface IP address that the user typed is
1003+ // invalid.
1004+ $scope.isIPAddressInvalid = function(nic) {
1005+ return (nic.ip_address.length === 0 ||
1006+ !ValidationService.validateIP(nic.ip_address) ||
1007+ !ValidationService.validateIPInNetwork(
1008+ nic.ip_address, nic.subnet.cidr));
1009+ };
1010+
1011+ // Save the interface IP address.
1012+ $scope.saveInterfaceIPAddress = function(nic) {
1013+ var originalLink = mapNICToOriginalLink(nic);
1014+ var prevIPAddress = originalLink.ip_address;
1015+ if($scope.isIPAddressInvalid(nic)) {
1016+ nic.ip_address = prevIPAddress;
1017+ } else if(nic.ip_address !== prevIPAddress) {
1018+ $scope.saveInterfaceLink(nic);
1019+ }
1020+ };
1021+
1022 // Load all the required managers.
1023 ManagerHelperService.loadManagers([
1024 FabricsManager,
1025
1026=== modified file 'src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js'
1027--- src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js 2015-09-29 15:19:25 +0000
1028+++ src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js 2015-10-01 14:33:47 +0000
1029@@ -65,6 +65,9 @@
1030 expect($scope.nodeHasLoaded).toBe(false);
1031 expect($scope.managersHaveLoaded).toBe(false);
1032 expect($scope.column).toBe('name');
1033+ expect($scope.fabrics).toBe(FabricsManager.getItems());
1034+ expect($scope.vlans).toBe(VLANsManager.getItems());
1035+ expect($scope.subnets).toBe(SubnetsManager.getItems());
1036 expect($scope.interfaces).toEqual([]);
1037 expect($scope.interfaceLinksMap).toEqual({});
1038 expect($scope.originalInterfaces).toEqual({});
1039@@ -212,7 +215,7 @@
1040 members: [parent1, parent2],
1041 vlan: null,
1042 link_id: -1,
1043- subnet_id: null,
1044+ subnet: null,
1045 mode: "link_up",
1046 ip_address: ""
1047 }]);
1048@@ -297,13 +300,15 @@
1049 links: [],
1050 vlan: null,
1051 link_id: -1,
1052- subnet_id: null,
1053+ subnet: null,
1054 mode: "link_up",
1055 ip_address: ""
1056 }]);
1057 });
1058
1059 it("duplicates links as alias interfaces", function() {
1060+ var subnet0 = { id: 0 }, subnet1 = { id: 1 }, subnet2 = { id: 2 };
1061+ SubnetsManager._items = [subnet0, subnet1, subnet2];
1062 var links = [
1063 {
1064 id: 0,
1065@@ -345,7 +350,7 @@
1066 vlan: null,
1067 fabric: undefined,
1068 link_id: 0,
1069- subnet_id: 0,
1070+ subnet: subnet0,
1071 mode: "dhcp",
1072 ip_address: ""
1073 },
1074@@ -359,7 +364,7 @@
1075 vlan: null,
1076 fabric: undefined,
1077 link_id: 1,
1078- subnet_id: 1,
1079+ subnet: subnet1,
1080 mode: "auto",
1081 ip_address: ""
1082 },
1083@@ -373,7 +378,7 @@
1084 vlan: null,
1085 fabric: undefined,
1086 link_id: 2,
1087- subnet_id: 2,
1088+ subnet: subnet2,
1089 mode: "static",
1090 ip_address: "192.168.122.10"
1091 }
1092@@ -470,7 +475,7 @@
1093 "auto": "Auto assign",
1094 "static": "Static assign",
1095 "dhcp": "DHCP",
1096- "link_up": "Unconfigured",
1097+ "link_up": "No IP",
1098 "missing_type": "missing_type"
1099 };
1100
1101@@ -485,64 +490,63 @@
1102 });
1103 });
1104
1105- describe("getSubnet", function() {
1106+ describe("getVLANText", function() {
1107
1108- it("returns item from SubnetsManager", function() {
1109+ it("returns just vid", function() {
1110 var controller = makeController();
1111- var subnet_id = makeInteger(0, 100);
1112- var subnet = {
1113- id: subnet_id
1114- };
1115- SubnetsManager._items = [subnet];
1116-
1117- var nic = {
1118- subnet_id: subnet_id
1119- };
1120- expect($scope.getSubnet(nic)).toBe(subnet);
1121+ var vlan = {
1122+ vid: 5
1123+ };
1124+ expect($scope.getVLANText(vlan)).toBe(5);
1125 });
1126
1127- it("returns null for missing subnet", function() {
1128+ it("returns vid + name", function() {
1129 var controller = makeController();
1130- var subnet_id = makeInteger(0, 100);
1131- var nic = {
1132- subnet_id: subnet_id
1133+ var name = makeName("vlan");
1134+ var vlan = {
1135+ vid: 5,
1136+ name: name
1137 };
1138- expect($scope.getSubnet(nic)).toBeNull();
1139+ expect($scope.getVLANText(vlan)).toBe("5 (" + name + ")");
1140 });
1141 });
1142
1143- describe("getSubnetName", function() {
1144-
1145- it("returns name from item in SubnetsManager", function() {
1146- var controller = makeController();
1147- var subnet_id = makeInteger(0, 100);
1148- var subnet_name = makeName("subnet");
1149- var subnet = {
1150- id: subnet_id,
1151- name: subnet_name
1152- };
1153- SubnetsManager._items = [subnet];
1154-
1155- var nic = {
1156- subnet_id: subnet_id
1157- };
1158- expect($scope.getSubnetName(nic)).toBe(subnet_name);
1159- });
1160-
1161- it("returns 'Unknown' if item not in SubnetsManager", function() {
1162- var controller = makeController();
1163- var nic = {
1164- subnet_id: makeInteger(0, 100)
1165- };
1166- expect($scope.getSubnetName(nic)).toBe("Unknown");
1167- });
1168-
1169- it("returns 'Unconfigured' if no subnet_id", function() {
1170- var controller = makeController();
1171- var nic = {
1172- subnet_id: null
1173- };
1174- expect($scope.getSubnetName(nic)).toBe("Unconfigured");
1175+ describe("getSubnetText", function() {
1176+
1177+ it("returns 'Unconfigured' for null", function() {
1178+ var controller = makeController();
1179+ expect($scope.getSubnetText(null)).toBe("Unconfigured");
1180+ });
1181+
1182+ it("returns just cidr if no name", function() {
1183+ var controller = makeController();
1184+ var cidr = makeName("cidr");
1185+ var subnet = {
1186+ cidr: cidr
1187+ };
1188+ expect($scope.getSubnetText(subnet)).toBe(cidr);
1189+ });
1190+
1191+ it("returns just cidr if name same as cidr", function() {
1192+ var controller = makeController();
1193+ var cidr = makeName("cidr");
1194+ var subnet = {
1195+ cidr: cidr,
1196+ name: cidr
1197+ };
1198+ expect($scope.getSubnetText(subnet)).toBe(cidr);
1199+ });
1200+
1201+ it("returns cidr + name", function() {
1202+ var controller = makeController();
1203+ var cidr = makeName("cidr");
1204+ var name = makeName("name");
1205+ var subnet = {
1206+ cidr: cidr,
1207+ name: name
1208+ };
1209+ expect($scope.getSubnetText(subnet)).toBe(
1210+ cidr + " (" + name + ")");
1211 });
1212 });
1213
1214@@ -615,6 +619,31 @@
1215 expect(NodesManager.updateInterface).not.toHaveBeenCalled();
1216 });
1217
1218+ it("resets name if its invalid and doesn't call update", function() {
1219+ var controller = makeController();
1220+ var id = makeInteger(0, 100);
1221+ var name = makeName("nic");
1222+ var vlan = { id: makeInteger(0, 100) };
1223+ var original_nic = {
1224+ id: id,
1225+ name: name,
1226+ vlan_id: vlan.id
1227+ };
1228+ var nic = {
1229+ id: id,
1230+ name: "",
1231+ vlan: vlan
1232+ };
1233+ $scope.originalInterfaces[id] = original_nic;
1234+ $scope.interfaces = [nic];
1235+
1236+ spyOn(NodesManager, "updateInterface").and.returnValue(
1237+ $q.defer().promise);
1238+ $scope.saveInterface(nic);
1239+ expect(nic.name).toBe(name);
1240+ expect(NodesManager.updateInterface).not.toHaveBeenCalled();
1241+ });
1242+
1243 it("calls NodesManager.updateInterface if name changed", function() {
1244 var controller = makeController();
1245 var id = makeInteger(0, 100);
1246@@ -686,32 +715,72 @@
1247
1248 it("clears focusInterface no arguments", function() {
1249 var controller = makeController();
1250- var nic = {};
1251+ var nic = {
1252+ type: "physical"
1253+ };
1254 $scope.focusInterface = nic;
1255 spyOn($scope, "saveInterface");
1256+ spyOn($scope, "saveInterfaceIPAddress");
1257 $scope.clearFocusInterface();
1258 expect($scope.focusInterface).toBeNull();
1259 expect($scope.saveInterface).toHaveBeenCalledWith(nic);
1260+ expect($scope.saveInterfaceIPAddress).toHaveBeenCalledWith(nic);
1261 });
1262
1263 it("clears focusInterface if same interface", function() {
1264 var controller = makeController();
1265- var nic = {};
1266+ var nic = {
1267+ type: "physical"
1268+ };
1269 $scope.focusInterface = nic;
1270 spyOn($scope, "saveInterface");
1271+ spyOn($scope, "saveInterfaceIPAddress");
1272 $scope.clearFocusInterface(nic);
1273 expect($scope.focusInterface).toBeNull();
1274 expect($scope.saveInterface).toHaveBeenCalledWith(nic);
1275+ expect($scope.saveInterfaceIPAddress).toHaveBeenCalledWith(nic);
1276 });
1277
1278 it("doesnt clear focusInterface if different interface", function() {
1279 var controller = makeController();
1280- var nic = {};
1281+ var nic = {
1282+ type: "physical"
1283+ };
1284 $scope.focusInterface = nic;
1285 spyOn($scope, "saveInterface");
1286+ spyOn($scope, "saveInterfaceIPAddress");
1287 $scope.clearFocusInterface({});
1288 expect($scope.focusInterface).toBe(nic);
1289 expect($scope.saveInterface).not.toHaveBeenCalled();
1290+ expect($scope.saveInterfaceIPAddress).not.toHaveBeenCalled();
1291+ });
1292+
1293+ it("doesnt call save with focusInterface no arguments", function() {
1294+ var controller = makeController();
1295+ var nic = {
1296+ type: "alias"
1297+ };
1298+ $scope.focusInterface = nic;
1299+ spyOn($scope, "saveInterface");
1300+ spyOn($scope, "saveInterfaceIPAddress");
1301+ $scope.clearFocusInterface();
1302+ expect($scope.focusInterface).toBeNull();
1303+ expect($scope.saveInterface).not.toHaveBeenCalled();
1304+ expect($scope.saveInterfaceIPAddress).toHaveBeenCalledWith(nic);
1305+ });
1306+
1307+ it("doesnt call save with focusInterface if same nic", function() {
1308+ var controller = makeController();
1309+ var nic = {
1310+ type: "alias"
1311+ };
1312+ $scope.focusInterface = nic;
1313+ spyOn($scope, "saveInterface");
1314+ spyOn($scope, "saveInterfaceIPAddress");
1315+ $scope.clearFocusInterface(nic);
1316+ expect($scope.focusInterface).toBeNull();
1317+ expect($scope.saveInterface).not.toHaveBeenCalled();
1318+ expect($scope.saveInterfaceIPAddress).toHaveBeenCalledWith(nic);
1319 });
1320 });
1321
1322@@ -812,4 +881,407 @@
1323 expect($scope.saveInterface).toHaveBeenCalledWith(nic);
1324 });
1325 });
1326+
1327+ describe("isLinkModeDisabled", function() {
1328+
1329+ it("enabled when subnet", function() {
1330+ var controller = makeController();
1331+ var nic = {
1332+ subnet : {}
1333+ };
1334+ expect($scope.isLinkModeDisabled(nic)).toBe(false);
1335+ });
1336+
1337+ it("disabled when not subnet", function() {
1338+ var controller = makeController();
1339+ var nic = {
1340+ subnet : null
1341+ };
1342+ expect($scope.isLinkModeDisabled(nic)).toBe(true);
1343+ });
1344+ });
1345+
1346+ describe("getLinkModes", function() {
1347+
1348+ it("only link_up when no subnet", function() {
1349+ var controller = makeController();
1350+ var nic = {
1351+ subnet : null
1352+ };
1353+ expect($scope.getLinkModes(nic)).toEqual([
1354+ {
1355+ "mode": "link_up",
1356+ "text": "No IP"
1357+ }
1358+ ]);
1359+ });
1360+
1361+ it("all modes if only one link", function() {
1362+ var controller = makeController();
1363+ var nic = {
1364+ subnet : {},
1365+ links: [{}]
1366+ };
1367+ expect($scope.getLinkModes(nic)).toEqual([
1368+ {
1369+ "mode": "auto",
1370+ "text": "Auto assign"
1371+ },
1372+ {
1373+ "mode": "static",
1374+ "text": "Static assign"
1375+ },
1376+ {
1377+ "mode": "dhcp",
1378+ "text": "DHCP"
1379+ },
1380+ {
1381+ "mode": "link_up",
1382+ "text": "No IP"
1383+ }
1384+ ]);
1385+ });
1386+
1387+ it("auto and static modes if more than one link", function() {
1388+ var controller = makeController();
1389+ var nic = {
1390+ subnet : {},
1391+ links: [{}, {}]
1392+ };
1393+ expect($scope.getLinkModes(nic)).toEqual([
1394+ {
1395+ "mode": "auto",
1396+ "text": "Auto assign"
1397+ },
1398+ {
1399+ "mode": "static",
1400+ "text": "Static assign"
1401+ }
1402+ ]);
1403+ });
1404+ });
1405+
1406+ describe("saveInterfaceLink", function() {
1407+
1408+ it("calls NodesManager.linkSubnet with params", function() {
1409+ var controller = makeController();
1410+ var nic = {
1411+ id: makeInteger(0, 100),
1412+ mode: "static",
1413+ subnet: { id: makeInteger(0, 100) },
1414+ link_id: makeInteger(0, 100),
1415+ ip_address: "192.168.122.1"
1416+ };
1417+ spyOn(NodesManager, "linkSubnet").and.returnValue(
1418+ $q.defer().promise);
1419+ $scope.saveInterfaceLink(nic);
1420+ expect(NodesManager.linkSubnet).toHaveBeenCalledWith(
1421+ node, nic.id, {
1422+ "mode": "static",
1423+ "subnet": nic.subnet.id,
1424+ "link_id": nic.link_id,
1425+ "ip_address": nic.ip_address
1426+ });
1427+ });
1428+ });
1429+
1430+ describe("subnetChanged", function() {
1431+
1432+ it("sets mode to link_up if set to no subnet", function() {
1433+ var controller = makeController();
1434+ var nic = {
1435+ subnet: null
1436+ };
1437+ spyOn($scope, "saveInterfaceLink");
1438+ $scope.subnetChanged(nic);
1439+ expect(nic.mode).toBe("link_up");
1440+ expect($scope.saveInterfaceLink).toHaveBeenCalledWith(nic);
1441+ });
1442+
1443+ it("doesnt set mode to link_up if set if subnet", function() {
1444+ var controller = makeController();
1445+ var nic = {
1446+ mode: "static",
1447+ subnet: {}
1448+ };
1449+ spyOn($scope, "saveInterfaceLink");
1450+ $scope.subnetChanged(nic);
1451+ expect(nic.mode).toBe("static");
1452+ expect($scope.saveInterfaceLink).toHaveBeenCalledWith(nic);
1453+ });
1454+
1455+ it("clears ip_address", function() {
1456+ var controller = makeController();
1457+ var nic = {
1458+ subnet: null,
1459+ ip_address: makeName("ip")
1460+ };
1461+ spyOn($scope, "saveInterfaceLink");
1462+ $scope.subnetChanged(nic);
1463+ expect(nic.ip_address).toBe("");
1464+ });
1465+ });
1466+
1467+ describe("shouldShowIPAddress", function() {
1468+
1469+ it("true if not static and has ip address", function() {
1470+ var controller = makeController();
1471+ var nic = {
1472+ mode: "auto",
1473+ ip_address: "192.168.122.1"
1474+ };
1475+ expect($scope.shouldShowIPAddress(nic)).toBe(true);
1476+ });
1477+
1478+ it("false if not static and doesn't have ip address", function() {
1479+ var controller = makeController();
1480+ var nic = {
1481+ mode: "dhcp",
1482+ ip_address: ""
1483+ };
1484+ expect($scope.shouldShowIPAddress(nic)).toBe(false);
1485+ });
1486+
1487+ describe("static", function() {
1488+
1489+ it("false if no orginial link", function() {
1490+ var controller = makeController();
1491+ var nic = {
1492+ id: 0,
1493+ mode: "static",
1494+ link_id: -1,
1495+ ip_address: ""
1496+ };
1497+ expect($scope.shouldShowIPAddress(nic)).toBe(false);
1498+ });
1499+
1500+ it("false if orginial link has no IP address", function() {
1501+ var controller = makeController();
1502+ var originalInterface = {
1503+ id: 0,
1504+ links: [
1505+ {
1506+ id: 0,
1507+ mode: "static"
1508+ }
1509+ ]
1510+ };
1511+ $scope.originalInterfaces = [originalInterface];
1512+
1513+ var nic = {
1514+ id: 0,
1515+ mode: "static",
1516+ link_id: 0,
1517+ ip_address: ""
1518+ };
1519+ expect($scope.shouldShowIPAddress(nic)).toBe(false);
1520+ });
1521+
1522+ it("false if orginial link has empty IP address", function() {
1523+ var controller = makeController();
1524+ var originalInterface = {
1525+ id: 0,
1526+ links: [
1527+ {
1528+ id: 0,
1529+ mode: "static",
1530+ ip_address: ""
1531+ }
1532+ ]
1533+ };
1534+ $scope.originalInterfaces = [originalInterface];
1535+
1536+ var nic = {
1537+ id: 0,
1538+ mode: "static",
1539+ link_id: 0,
1540+ ip_address: ""
1541+ };
1542+ expect($scope.shouldShowIPAddress(nic)).toBe(false);
1543+ });
1544+
1545+ it("false if no subnet on nic", function() {
1546+ var controller = makeController();
1547+ var originalInterface = {
1548+ id: 0,
1549+ links: [
1550+ {
1551+ id: 0,
1552+ mode: "static",
1553+ ip_address: "192.168.122.2"
1554+ }
1555+ ]
1556+ };
1557+ $scope.originalInterfaces = [originalInterface];
1558+
1559+ var nic = {
1560+ id: 0,
1561+ mode: "static",
1562+ link_id: 0,
1563+ ip_address: ""
1564+ };
1565+ expect($scope.shouldShowIPAddress(nic)).toBe(false);
1566+ });
1567+
1568+ it("false if the subnets don't match", function() {
1569+ var controller = makeController();
1570+ var originalInterface = {
1571+ id: 0,
1572+ links: [
1573+ {
1574+ id: 0,
1575+ mode: "static",
1576+ ip_address: "192.168.122.2",
1577+ subnet_id: 0
1578+ }
1579+ ]
1580+ };
1581+ $scope.originalInterfaces = [originalInterface];
1582+
1583+ var nic = {
1584+ id: 0,
1585+ mode: "static",
1586+ link_id: 0,
1587+ ip_address: "",
1588+ subnet: {
1589+ id: 1
1590+ }
1591+ };
1592+ expect($scope.shouldShowIPAddress(nic)).toBe(false);
1593+ });
1594+
1595+ it("true if all condititions match", function() {
1596+ var controller = makeController();
1597+ var originalInterface = {
1598+ id: 0,
1599+ links: [
1600+ {
1601+ id: 0,
1602+ mode: "static",
1603+ ip_address: "192.168.122.2",
1604+ subnet_id: 0
1605+ }
1606+ ]
1607+ };
1608+ $scope.originalInterfaces = [originalInterface];
1609+
1610+ var nic = {
1611+ id: 0,
1612+ mode: "static",
1613+ link_id: 0,
1614+ ip_address: "",
1615+ subnet: {
1616+ id: 0
1617+ }
1618+ };
1619+ expect($scope.shouldShowIPAddress(nic)).toBe(true);
1620+ });
1621+ });
1622+ });
1623+
1624+ describe("isIPAddressInvalid", function() {
1625+
1626+ it("true if empty IP address", function() {
1627+ var controller = makeController();
1628+ var nic = {
1629+ ip_address: ""
1630+ };
1631+ expect($scope.isIPAddressInvalid(nic)).toBe(true);
1632+ });
1633+
1634+ it("true if not valid IP address", function() {
1635+ var controller = makeController();
1636+ var nic = {
1637+ ip_address: "192.168.260.5"
1638+ };
1639+ expect($scope.isIPAddressInvalid(nic)).toBe(true);
1640+ });
1641+
1642+ it("true if IP address not in subnet", function() {
1643+ var controller = makeController();
1644+ var nic = {
1645+ ip_address: "192.168.123.10",
1646+ subnet: {
1647+ cidr: "192.168.122.0/24"
1648+ }
1649+ };
1650+ expect($scope.isIPAddressInvalid(nic)).toBe(true);
1651+ });
1652+
1653+ it("false if IP address in subnet", function() {
1654+ var controller = makeController();
1655+ var nic = {
1656+ ip_address: "192.168.122.10",
1657+ subnet: {
1658+ cidr: "192.168.122.0/24"
1659+ }
1660+ };
1661+ expect($scope.isIPAddressInvalid(nic)).toBe(false);
1662+ });
1663+ });
1664+
1665+ describe("saveInterfaceIPAddress", function() {
1666+
1667+ it("resets IP address if invalid doesn't save", function() {
1668+ var controller = makeController();
1669+ var originalInterface = {
1670+ id: 0,
1671+ links: [
1672+ {
1673+ id: 0,
1674+ mode: "static",
1675+ ip_address: "192.168.122.10",
1676+ subnet_id: 0
1677+ }
1678+ ]
1679+ };
1680+ $scope.originalInterfaces = [originalInterface];
1681+
1682+ var nic = {
1683+ id: 0,
1684+ mode: "static",
1685+ link_id: 0,
1686+ ip_address: "192.168.123.10",
1687+ subnet: {
1688+ id: 0,
1689+ cidr: "192.168.122.0/24"
1690+ }
1691+ };
1692+ spyOn($scope, "saveInterfaceLink");
1693+ $scope.saveInterfaceIPAddress(nic);
1694+ expect(nic.ip_address).toBe("192.168.122.10");
1695+ expect($scope.saveInterfaceLink).not.toHaveBeenCalled();
1696+ });
1697+
1698+ it("saves the link if valid", function() {
1699+ var controller = makeController();
1700+ var originalInterface = {
1701+ id: 0,
1702+ links: [
1703+ {
1704+ id: 0,
1705+ mode: "static",
1706+ ip_address: "192.168.122.10",
1707+ subnet_id: 0
1708+ }
1709+ ]
1710+ };
1711+ $scope.originalInterfaces = [originalInterface];
1712+
1713+ var nic = {
1714+ id: 0,
1715+ mode: "static",
1716+ link_id: 0,
1717+ ip_address: "192.168.122.11",
1718+ subnet: {
1719+ id: 0,
1720+ cidr: "192.168.122.0/24"
1721+ }
1722+ };
1723+ spyOn($scope, "saveInterfaceLink");
1724+ $scope.saveInterfaceIPAddress(nic);
1725+ expect(nic.ip_address).toBe("192.168.122.11");
1726+ expect($scope.saveInterfaceLink).toHaveBeenCalledWith(nic);
1727+ });
1728+ });
1729 });
1730
1731=== modified file 'src/maasserver/static/js/angular/factories/nodes.js'
1732--- src/maasserver/static/js/angular/factories/nodes.js 2015-09-30 22:40:08 +0000
1733+++ src/maasserver/static/js/angular/factories/nodes.js 2015-10-01 14:33:47 +0000
1734@@ -90,11 +90,23 @@
1735 "node.update_interface", params);
1736 };
1737
1738- // Send the update information to the region.
1739+ // Create or update the link to the subnet for the interface.
1740+ NodesManager.prototype.linkSubnet = function(
1741+ node, interface_id, params) {
1742+ if(!angular.isObject(params)) {
1743+ params = {};
1744+ }
1745+ params.system_id = node.system_id;
1746+ params.interface_id = interface_id;
1747+ return RegionConnection.callMethod(
1748+ "node.link_subnet", params);
1749+ };
1750+
1751+ // Unmount the filesystem on the block device or partition.
1752 NodesManager.prototype.unmountFilesystem = function(
1753 system_id, block_id, partition_id) {
1754 var self = this;
1755- var method = this._handler + ".unmountFilesystem";
1756+ var method = this._handler + ".unmount_filesystem";
1757 var params = {
1758 system_id: system_id,
1759 block_id: block_id,
1760
1761=== modified file 'src/maasserver/static/js/angular/factories/tests/test_nodes.js'
1762--- src/maasserver/static/js/angular/factories/tests/test_nodes.js 2015-09-30 22:40:08 +0000
1763+++ src/maasserver/static/js/angular/factories/tests/test_nodes.js 2015-10-01 14:33:47 +0000
1764@@ -178,6 +178,49 @@
1765 });
1766 });
1767
1768+ describe("linkSubnet", function() {
1769+
1770+ it("calls node.link_subnet with system_id and interface_id",
1771+ function(done) {
1772+ var node = makeNode(), interface_id = makeInteger(0, 100);
1773+ webSocket.returnData.push(makeFakeResponse("updated"));
1774+ NodesManager.linkSubnet(node, interface_id).then(
1775+ function() {
1776+ var sentObject = angular.fromJson(
1777+ webSocket.sentData[0]);
1778+ expect(sentObject.method).toBe(
1779+ "node.link_subnet");
1780+ expect(sentObject.params.system_id).toBe(
1781+ node.system_id);
1782+ expect(sentObject.params.interface_id).toBe(
1783+ interface_id);
1784+ done();
1785+ });
1786+ });
1787+
1788+ it("calls node.link_subnet with params",
1789+ function(done) {
1790+ var node = makeNode(), interface_id = makeInteger(0, 100);
1791+ var params = {
1792+ name: makeName("eth0")
1793+ };
1794+ webSocket.returnData.push(makeFakeResponse("updated"));
1795+ NodesManager.linkSubnet(node, interface_id, params).then(
1796+ function() {
1797+ var sentObject = angular.fromJson(
1798+ webSocket.sentData[0]);
1799+ expect(sentObject.method).toBe(
1800+ "node.link_subnet");
1801+ expect(sentObject.params.system_id).toBe(
1802+ node.system_id);
1803+ expect(sentObject.params.interface_id).toBe(
1804+ interface_id);
1805+ expect(sentObject.params.name).toBe(params.name);
1806+ done();
1807+ });
1808+ });
1809+ });
1810+
1811 describe("unmountFilesystem", function() {
1812
1813 it("calls node.unmountFilesystem", function(done) {
1814@@ -186,7 +229,7 @@
1815 NodesManager.unmountFilesystem(
1816 makeName("block_id"), null).then(function() {
1817 var sentObject = angular.fromJson(webSocket.sentData[0]);
1818- expect(sentObject.method).toBe("node.unmountFilesystem");
1819+ expect(sentObject.method).toBe("node.unmount_filesystem");
1820 done();
1821 });
1822 });
1823@@ -200,6 +243,7 @@
1824 fakeNode.system_id, block_id, partition_id).then(
1825 function() {
1826 var sentObject = angular.fromJson(webSocket.sentData[0]);
1827+ expect(sentObject.method).toBe("node.unmount_filesystem");
1828 expect(sentObject.params.system_id).toBe(fakeNode.system_id);
1829 expect(sentObject.params.block_id).toBe(block_id);
1830 expect(sentObject.params.partition_id).toBe(partition_id);
1831
1832=== modified file 'src/maasserver/static/partials/node-details.html'
1833--- src/maasserver/static/partials/node-details.html 2015-10-01 11:15:36 +0000
1834+++ src/maasserver/static/partials/node-details.html 2015-10-01 14:33:47 +0000
1835@@ -358,6 +358,7 @@
1836 <div class="table__data table__column--14">
1837 <select class="table__input" name="fabric" id="fabric"
1838 data-ng-model="interface.fabric"
1839+ data-ng-disabled="interface.type == 'alias'"
1840 data-ng-change="fabricChanged(interface)"
1841 data-ng-options="fabric as fabric.name for fabric in fabrics">
1842 </select>
1843@@ -365,39 +366,37 @@
1844 <div class="table__data table__column--14">
1845 <select class="table__input" name="vlan" id="vlan"
1846 data-ng-model="interface.vlan"
1847+ data-ng-disabled="interface.type == 'alias'"
1848 data-ng-change="saveInterface(interface)"
1849- data-ng-options="vlan as vlan.name for vlan in vlans | filterByFabric:interface.fabric">
1850+ data-ng-options="vlan as getVLANText(vlan) for vlan in vlans | filterByFabric:interface.fabric">
1851 </select>
1852 </div>
1853 <div class="table__data table__column--18">
1854- {$ getSubnetName(interface) $}
1855- <!--
1856- TODO: Use when editing is done.
1857- <select class="table__input" name="range" id="range">
1858- <option value="auto">10.10.10.1/6 (managed)</option>
1859+ <select class="table__input" name="subnet" id="subnet"
1860+ data-ng-model="interface.subnet"
1861+ data-ng-change="subnetChanged(interface)"
1862+ data-ng-options="subnet as getSubnetText(subnet) for subnet in subnets | filterByVLAN:interface.vlan">
1863+ <option value="" data-ng-hide="interface.links.length > 1">Unconfigured</option>
1864 </select>
1865- -->
1866 </div>
1867 <div class="table__data table__column--14">
1868 <ul class="no-bullets">
1869 <li>
1870- {$ getLinkModeText(interface) $}
1871- <!--
1872- TODO: Use when editing is done.
1873- <select class="table__input" name="ip-address" id="ip-address">
1874- <option value="auto">Auto assign</option>
1875- <option value="manual" selected>Manual</option>
1876- <option value="DHCP">DHCP</option>
1877- <option value="unconfigured">Unconfigured</option>
1878+ <select class="table__input" name="link-mode" id="link-mode"
1879+ data-ng-model="interface.mode"
1880+ data-ng-change="saveInterfaceLink(interface)"
1881+ data-ng-disabled="isLinkModeDisabled(interface)"
1882+ data-ng-options="mode.mode as mode.text for mode in getLinkModes(interface)">
1883 </select>
1884- -->
1885 </li>
1886- <li class="margin-top--ten" data-ng-show="interface.ip_address">
1887- {$ interface.ip_address $}
1888- <!--
1889- TODO: Use when editing is done.
1890- <input type="text" class="table__input" value="127.0.0.1">
1891- -->
1892+ <li class="margin-top--ten" data-ng-show="shouldShowIPAddress(interface)">
1893+ <input type="text" class="table__input"
1894+ data-ng-model="interface.ip_address"
1895+ data-ng-class="{ invalid: isIPAddressInvalid(interface) }"
1896+ data-maas-enter-blur
1897+ data-ng-focus="setFocusInterface(interface)"
1898+ data-ng-blur="clearFocusInterface(interface)"
1899+ data-ng-disabled="interface.mode != 'static'">
1900 </li>
1901 </ul>
1902 </div>
1903
1904=== modified file 'src/maasserver/websockets/handlers/node.py'
1905--- src/maasserver/websockets/handlers/node.py 2015-09-30 22:40:08 +0000
1906+++ src/maasserver/websockets/handlers/node.py 2015-10-01 14:33:47 +0000
1907@@ -37,6 +37,7 @@
1908 from maasserver.models.nodeprobeddetails import get_single_probed_details
1909 from maasserver.models.partition import Partition
1910 from maasserver.models.physicalblockdevice import PhysicalBlockDevice
1911+from maasserver.models.subnet import Subnet
1912 from maasserver.models.tag import Tag
1913 from maasserver.node_action import compile_node_actions
1914 from maasserver.rpc import getClientFor
1915@@ -105,7 +106,8 @@
1916 'set_active',
1917 'check_power',
1918 'update_interface',
1919- 'unmountFilesystem',
1920+ 'link_subnet',
1921+ 'unmount_filesystem',
1922 ]
1923 form = AdminNodeWithMACAddressesForm
1924 exclude = [
1925@@ -348,7 +350,9 @@
1926
1927 def dehydrate_interface(self, interface, obj):
1928 """Dehydrate a `interface` into a interface definition."""
1929- links = interface.get_links()
1930+ # Sort the links by ID that way they show up in the same order in
1931+ # the UI.
1932+ links = sorted(interface.get_links(), key=itemgetter("id"))
1933 for link in links:
1934 # Replace the subnet object with the subnet_id. The client will
1935 # use this information to pull the subnet information from the
1936@@ -545,7 +549,7 @@
1937 node_obj.save()
1938 return self.full_dehydrate(node_obj)
1939
1940- def unmountFilesystem(self, params):
1941+ def unmount_filesystem(self, params):
1942 node = self.get_object(params)
1943 if params.get('partition_id') is not None:
1944 obj = Partition.objects.get(
1945@@ -601,6 +605,10 @@
1946
1947 def update_interface(self, params):
1948 """Update the interface."""
1949+ # Only admin users can perform update.
1950+ if not self.user.is_superuser:
1951+ raise HandlerPermissionError()
1952+
1953 node = self.get_object(params)
1954 interface = Interface.objects.get(node=node, id=params["interface_id"])
1955 interface_form = InterfaceForm.get_interface_form(interface.type)
1956@@ -610,6 +618,28 @@
1957 else:
1958 raise ValidationError(form.errors)
1959
1960+ def link_subnet(self, params):
1961+ """Create or update the link."""
1962+ # Only admin users can perform update.
1963+ if not self.user.is_superuser:
1964+ raise HandlerPermissionError()
1965+
1966+ node = self.get_object(params)
1967+ interface = Interface.objects.get(node=node, id=params["interface_id"])
1968+ subnet = None
1969+ if "subnet" in params:
1970+ subnet = Subnet.objects.get(id=params["subnet"])
1971+ if "link_id" in params:
1972+ # We are updating an already existing link.
1973+ interface.update_link_by_id(
1974+ params["link_id"], params["mode"], subnet,
1975+ ip_address=params.get("ip_address", None))
1976+ else:
1977+ # We are creating a new link.
1978+ interface.link_subnet(
1979+ params["mode"], subnet,
1980+ ip_address=params.get("ip_address", None))
1981+
1982 @asynchronous
1983 @inlineCallbacks
1984 def check_power(self, params):
1985
1986=== modified file 'src/maasserver/websockets/handlers/tests/test_node.py'
1987--- src/maasserver/websockets/handlers/tests/test_node.py 2015-10-01 13:29:51 +0000
1988+++ src/maasserver/websockets/handlers/tests/test_node.py 2015-10-01 14:33:47 +0000
1989@@ -25,6 +25,7 @@
1990 from lxml import etree
1991 from maasserver.enum import (
1992 FILESYSTEM_FORMAT_TYPE_CHOICES_DICT,
1993+ INTERFACE_LINK_TYPE,
1994 INTERFACE_TYPE,
1995 IPADDRESS_TYPE,
1996 NODE_STATUS,
1997@@ -33,6 +34,7 @@
1998 from maasserver.forms import AdminNodeWithMACAddressesForm
1999 from maasserver.models import interface as interface_module
2000 from maasserver.models.config import Config
2001+from maasserver.models.interface import Interface
2002 from maasserver.models.nodeprobeddetails import get_single_probed_details
2003 from maasserver.node_action import compile_node_actions
2004 from maasserver.rpc.testing.fixtures import MockLiveRegionToClusterRPCFixture
2005@@ -79,7 +81,10 @@
2006 LIST_MODALIASES_OUTPUT_NAME,
2007 LLDP_OUTPUT_NAME,
2008 )
2009-from mock import sentinel
2010+from mock import (
2011+ ANY,
2012+ sentinel,
2013+)
2014 from netaddr import IPAddress
2015 from provisioningserver.power.poweraction import PowerActionFail
2016 from provisioningserver.rpc.cluster import PowerQuery
2017@@ -915,14 +920,14 @@
2018 updated_node = handler.update(node_data)
2019 self.assertItemsEqual([tag_name], updated_node["tags"])
2020
2021- def test_unmountFilesystem(self):
2022+ def test_unmount_filesystem(self):
2023 user = factory.make_admin()
2024 handler = NodeHandler(user, {})
2025 architecture = make_usable_architecture(self)
2026 node = factory.make_Node(interface=True, architecture=architecture)
2027 block_device = factory.make_PhysicalBlockDevice(node=node)
2028 factory.make_Filesystem(block_device=block_device)
2029- handler.unmountFilesystem({
2030+ handler.unmount_filesystem({
2031 'system_id': node.system_id,
2032 'block_id': block_device.id
2033 })
2034@@ -1006,7 +1011,7 @@
2035 node.distro_series, Equals(osystem["releases"][0]["name"]))
2036
2037 def test_update_interface(self):
2038- user = factory.make_User()
2039+ user = factory.make_admin()
2040 node = factory.make_Node()
2041 handler = NodeHandler(user, {})
2042 interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
2043@@ -1023,7 +1028,7 @@
2044 self.assertEquals(new_vlan, interface.vlan)
2045
2046 def test_update_interface_raises_ValidationError(self):
2047- user = factory.make_User()
2048+ user = factory.make_admin()
2049 node = factory.make_Node()
2050 handler = NodeHandler(user, {})
2051 interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
2052@@ -1036,6 +1041,50 @@
2053 "vlan": random.randint(1000, 5000),
2054 })
2055
2056+ def test_link_subnet_calls_update_link_by_id_if_link_id(self):
2057+ user = factory.make_admin()
2058+ node = factory.make_Node()
2059+ handler = NodeHandler(user, {})
2060+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
2061+ subnet = factory.make_Subnet()
2062+ link_id = random.randint(0, 100)
2063+ mode = factory.pick_enum(INTERFACE_LINK_TYPE)
2064+ ip_address = factory.make_ip_address()
2065+ self.patch_autospec(Interface, "update_link_by_id")
2066+ handler.link_subnet({
2067+ "system_id": node.system_id,
2068+ "interface_id": interface.id,
2069+ "link_id": link_id,
2070+ "subnet": subnet.id,
2071+ "mode": mode,
2072+ "ip_address": ip_address,
2073+ })
2074+ self.assertThat(
2075+ Interface.update_link_by_id,
2076+ MockCalledOnceWith(
2077+ ANY, link_id, mode, subnet, ip_address=ip_address))
2078+
2079+ def test_link_subnet_calls_link_subnet_if_not_link_id(self):
2080+ user = factory.make_admin()
2081+ node = factory.make_Node()
2082+ handler = NodeHandler(user, {})
2083+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
2084+ subnet = factory.make_Subnet()
2085+ mode = factory.pick_enum(INTERFACE_LINK_TYPE)
2086+ ip_address = factory.make_ip_address()
2087+ self.patch_autospec(Interface, "link_subnet")
2088+ handler.link_subnet({
2089+ "system_id": node.system_id,
2090+ "interface_id": interface.id,
2091+ "subnet": subnet.id,
2092+ "mode": mode,
2093+ "ip_address": ip_address,
2094+ })
2095+ self.assertThat(
2096+ Interface.link_subnet,
2097+ MockCalledOnceWith(
2098+ ANY, mode, subnet, ip_address=ip_address))
2099+
2100
2101 class TestNodeHandlerCheckPower(MAASTransactionServerTestCase):
2102
2103
2104=== modified file 'src/maastesting/factory.py'
2105--- src/maastesting/factory.py 2015-09-24 16:22:12 +0000
2106+++ src/maastesting/factory.py 2015-10-01 14:33:47 +0000
2107@@ -276,15 +276,31 @@
2108 slash = 128 - host_bits
2109 return self.make_ipv6_network(slash=slash)
2110
2111- def pick_ip_in_dynamic_range(self, ngi):
2112+ def pick_ip_in_dynamic_range(self, ngi, but_not=None):
2113+ if but_not is None:
2114+ but_not = []
2115 first = ngi.get_dynamic_ip_range().first
2116 last = ngi.get_dynamic_ip_range().last
2117- return unicode(IPAddress(random.randrange(first, last)))
2118+ but_not = [IPAddress(but) for but in but_not if but is not None]
2119+ for _ in range(100):
2120+ address = IPAddress(random.randint(first, last))
2121+ if address not in but_not:
2122+ return bytes(address)
2123+ raise TooManyRandomRetries(
2124+ "Could not find available IP in static range")
2125
2126- def pick_ip_in_static_range(self, ngi):
2127+ def pick_ip_in_static_range(self, ngi, but_not=None):
2128+ if but_not is None:
2129+ but_not = []
2130 first = ngi.get_static_ip_range().first
2131 last = ngi.get_static_ip_range().last
2132- return unicode(IPAddress(random.randrange(first, last)))
2133+ but_not = [IPAddress(but) for but in but_not if but is not None]
2134+ for _ in range(100):
2135+ address = IPAddress(random.randint(first, last))
2136+ if address not in but_not:
2137+ return bytes(address)
2138+ raise TooManyRandomRetries(
2139+ "Could not find available IP in static range")
2140
2141 def pick_ip_in_network(self, network, but_not=None):
2142 if but_not is None: