Merge lp:~mpontillo/maas/ip-allocation-bugs-1.9 into lp:maas/1.9

Proposed by Mike Pontillo
Status: Merged
Approved by: Mike Pontillo
Approved revision: no longer in the source branch.
Merged at revision: 4520
Proposed branch: lp:~mpontillo/maas/ip-allocation-bugs-1.9
Merge into: lp:maas/1.9
Diff against target: 935 lines (+533/-60)
13 files modified
.idea/encodings.xml (+3/-1)
.idea/vcs.xml (+0/-6)
src/maasserver/api/devices.py (+17/-7)
src/maasserver/api/tests/test_devices.py (+125/-11)
src/maasserver/models/interface.py (+96/-18)
src/maasserver/models/node.py (+5/-1)
src/maasserver/models/nodegroupinterface.py (+37/-3)
src/maasserver/models/staticipaddress.py (+9/-4)
src/maasserver/models/subnet.py (+8/-1)
src/maasserver/models/tests/test_interface.py (+114/-4)
src/maasserver/models/tests/test_nodegroupinterface.py (+35/-0)
src/maasserver/testing/factory.py (+14/-4)
utilities/remote-reinstall (+70/-0)
To merge this branch: bzr merge lp:~mpontillo/maas/ip-allocation-bugs-1.9
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+278925@code.launchpad.net

Commit message

Clean up the claiming of AUTO IP addresses.

 * In the case of an UNMANAGED subnet which could not be linked to its NodeGroupInterface at migration time (for example, due to the lack of having a subnet mask), add a query to check if any NodeGroupInterface has static or dynamic ranges within the subnet we are about to allocate for.
 * Throw an error if a NodeGroupInterface being deployed with an AUTO IP address has a dynamic range without a static range. (this means the user needs to define a static range, or select a different IP allocation method.) Note that subnets can still be fully MAAS-managed if no dynamic range is defined.
 * Throw an error if the linked Subnet for an AUTO IP address cannot be found. (this should not happen, but if it does, we want a nicer error.)
 * Run the algorithm to determine ancillary IP addresses on a subnet before handing out an AUTO IP address on a managed or unmanaged subnet. (this fixes the issue where a known router address will be used on a fully unmanaged network)
 * Add logging in maas.log for when an AUTO IP address is allocated.
 * Fix typo in function name (dyanamic -> dynamic)
 * Add a utility script (remote-reinstall) to easily overwrite an installed version of MAAS on a remote host with a development version of MAAS. Requires SSH access to the root account on that host. (This greatly speeds development when iterative manual testing is required.)

To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) wrote :

I'm going to put this up for review feedback even though I haven't done the unit tests, because I've spent most of the afternoon manually testing and iterating on this.

This will have unit tests before I land it.

Revision history for this message
Mike Pontillo (mpontillo) wrote :

Also, I plan to test and review this on 1.9 first. But I will merge and land this on trunk before landing this on 1.9.

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

Looks good. Just a few comments that need looking at. Please change the WebUI adjustment before landing.

Also make sure that you land this in trunk as well. We need to be very sure that every branch we land in 1.9 without going through trunk first also lands in trunk. Without it, it means a regression in the next release of MAAS. I would prefer to see this land in trunk first.

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

Ah just hit me right after I set it to approved. You have not added any tests, so this needs to have unit tests before this can land. Silly me.

review: Needs Fixing
Revision history for this message
Mike Pontillo (mpontillo) wrote :

Thanks for the review. Yeah, I'll get those tests in so this can land. (but first I want to finish triaging that juju issue)

Revision history for this message
Mike Pontillo (mpontillo) wrote :

Blake, I've added some unit tests. I think this is ready for another look. Thanks!

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

Looks good. Don't forget to land this in trunk as well.

review: Approve
Revision history for this message
Mike Pontillo (mpontillo) wrote :

It looks like the juju bug may need further triage work; I'm going to land this now so I can base any additional fixes on top of this.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.idea/encodings.xml'
2--- .idea/encodings.xml 2015-05-14 04:26:55 +0000
3+++ .idea/encodings.xml 2015-12-03 19:55:35 +0000
4@@ -1,4 +1,6 @@
5 <?xml version="1.0" encoding="UTF-8"?>
6 <project version="4">
7- <component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false" />
8+ <component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false">
9+ <file url="PROJECT" charset="UTF-8" />
10+ </component>
11 </project>
12\ No newline at end of file
13
14=== removed file '.idea/vcs.xml'
15--- .idea/vcs.xml 2015-05-14 04:26:55 +0000
16+++ .idea/vcs.xml 1970-01-01 00:00:00 +0000
17@@ -1,6 +0,0 @@
18-<?xml version="1.0" encoding="UTF-8"?>
19-<project version="4">
20- <component name="VcsDirectoryMappings">
21- <mapping directory="" vcs="" />
22- </component>
23-</project>
24\ No newline at end of file
25
26=== modified file 'src/maasserver/api/devices.py'
27--- src/maasserver/api/devices.py 2015-09-10 18:28:45 +0000
28+++ src/maasserver/api/devices.py 2015-12-03 19:55:35 +0000
29@@ -29,6 +29,7 @@
30 from maasserver.exceptions import (
31 MAASAPIBadRequest,
32 MAASAPIValidationError,
33+ StaticIPAddressExhaustion,
34 )
35 from maasserver.fields import MAC_RE
36 from maasserver.forms import (
37@@ -169,7 +170,7 @@
38 Returns 400 if the mac_address is not found on the device.
39 Returns 503 if there are not enough IPs left on the cluster interface
40 to which the mac_address is linked.
41- Returns 503 if the requested_address falls in a dynamic range.
42+ Returns 503 if the interface does not have an associated subnet.
43 Returns 503 if the requested_address falls in a dynamic range.
44 Returns 503 if the requested_address is already allocated.
45 """
46@@ -192,8 +193,7 @@
47 "mac_address %s not found on the device" % raw_mac)
48 requested_address = request.POST.get('requested_address', None)
49 if requested_address is None:
50- sticky_ips = interface.claim_static_ips(
51- requested_address=requested_address)
52+ sticky_ips = interface.claim_static_ips()
53 else:
54 subnet = Subnet.objects.get_best_subnet_for_ip(
55 requested_address)
56@@ -203,9 +203,16 @@
57 ip_address=requested_address),
58 ]
59
60- maaslog.info(
61- "%s: Sticky IP address(es) allocated: %s", device.hostname,
62- ', '.join(allocation.ip for allocation in sticky_ips))
63+ if len(sticky_ips) == 0:
64+ raise StaticIPAddressExhaustion(
65+ "%s: An IP address could not be claimed at this time. "
66+ "Check your subnet ranges and utilization and try again." %
67+ device.hostname)
68+ else:
69+ maaslog.info(
70+ "%s: Sticky IP address(es) allocated: %s", device.hostname,
71+ ', '.join(allocation.ip for allocation in sticky_ips))
72+
73 return device
74
75 @operation(idempotent=False)
76@@ -272,7 +279,10 @@
77 form = DeviceWithMACsForm(data=request.data, request=request)
78 if form.is_valid():
79 device = form.save()
80- maaslog.info("%s: Added new device", device.hostname)
81+ parent = device.parent
82+ maaslog.info(
83+ "%s: Added new device%s", device.hostname,
84+ "" if not parent else " (parent: %s)" % parent.hostname)
85 return device
86 else:
87 raise MAASAPIValidationError(form.errors)
88
89=== modified file 'src/maasserver/api/tests/test_devices.py'
90--- src/maasserver/api/tests/test_devices.py 2015-09-10 18:28:45 +0000
91+++ src/maasserver/api/tests/test_devices.py 2015-12-03 19:55:35 +0000
92@@ -24,8 +24,12 @@
93 INTERFACE_TYPE,
94 IPADDRESS_TYPE,
95 NODE_STATUS,
96+ NODEGROUP_STATUS,
97+ NODEGROUPINTERFACE_MANAGEMENT,
98 )
99 from maasserver.models import (
100+ Device,
101+ Interface,
102 interface as interface_module,
103 Node,
104 NodeGroup,
105@@ -37,6 +41,7 @@
106 )
107 from maasserver.testing.factory import factory
108 from maasserver.testing.orm import reload_object
109+from mock import patch
110 from testtools.matchers import (
111 HasLength,
112 Not,
113@@ -285,17 +290,93 @@
114 class TestClaimStickyIpAddressAPI(APITestCase):
115 """Tests for /api/1.0/devices/?op=claim_sticky_ip_address."""
116
117- def test__claims_ip_address_from_cluster_interface(self):
118- parent = factory.make_Node_with_Interface_on_Subnet()
119- device = factory.make_Node(
120- installable=False, parent=parent, interface=True,
121- disable_ipv4=False, owner=self.logged_in_user)
122- # Silence 'update_host_maps'.
123- self.patch_autospec(interface_module, "update_host_maps")
124- response = self.client.post(
125- get_device_uri(device), {'op': 'claim_sticky_ip_address'})
126- self.assertEqual(httplib.OK, response.status_code, response.content)
127- parsed_device = json.loads(response.content)
128+ def test__claims_ip_address_from_cluster_interface_static_range(self):
129+ ng = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
130+ ngi = factory.make_NodeGroupInterface(
131+ ng, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS)
132+ parent = factory.make_Node_with_Interface_on_Subnet(
133+ nodegroup=ng, subnet=ngi.subnet)
134+ device = factory.make_Node(
135+ installable=False, parent=parent, interface=True,
136+ disable_ipv4=False, owner=self.logged_in_user)
137+ # Silence 'update_host_maps'.
138+ self.patch_autospec(interface_module, "update_host_maps")
139+ response = self.client.post(
140+ get_device_uri(device), {'op': 'claim_sticky_ip_address'})
141+ self.assertEqual(httplib.OK, response.status_code, response.content)
142+ parsed_device = json.loads(response.content)
143+ [returned_ip] = parsed_device["ip_addresses"]
144+ static_ip = StaticIPAddress.objects.filter(ip=returned_ip).first()
145+ self.assertIsNotNone(static_ip)
146+ self.assertEquals(IPADDRESS_TYPE.STICKY, static_ip.alloc_type)
147+
148+ def test__claims_ip_address_from_unmanaged_cluster_interface(self):
149+ ng = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
150+ ngi = factory.make_NodeGroupInterface(
151+ ng, management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
152+ parent = factory.make_Node_with_Interface_on_Subnet(
153+ nodegroup=ng, subnet=ngi.subnet)
154+ device = factory.make_Node(
155+ installable=False, parent=parent, interface=True,
156+ disable_ipv4=False, owner=self.logged_in_user)
157+ # Silence 'update_host_maps'.
158+ self.patch_autospec(interface_module, "update_host_maps")
159+ response = self.client.post(
160+ get_device_uri(device), {'op': 'claim_sticky_ip_address'})
161+ self.assertEqual(httplib.OK, response.status_code, response.content)
162+ parsed_device = json.loads(response.content)
163+ [returned_ip] = parsed_device["ip_addresses"]
164+ static_ip = StaticIPAddress.objects.filter(ip=returned_ip).first()
165+ self.assertIsNotNone(static_ip)
166+ self.assertEquals(IPADDRESS_TYPE.STICKY, static_ip.alloc_type)
167+
168+ def test__claims_ip_address_from_detached_cluster_interface(self):
169+ ng = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
170+ ngi = factory.make_NodeGroupInterface(
171+ ng, management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
172+ subnet = ngi.subnet
173+ ngi.subnet = None
174+ ngi.save()
175+ parent = factory.make_Node_with_Interface_on_Subnet(
176+ nodegroup=ng, subnet=subnet, unmanaged=True)
177+ device = factory.make_Node(
178+ installable=False, parent=parent, interface=True,
179+ disable_ipv4=False, owner=self.logged_in_user)
180+ # Silence 'update_host_maps'.
181+ self.patch_autospec(interface_module, "update_host_maps")
182+ response = self.client.post(
183+ get_device_uri(device), {'op': 'claim_sticky_ip_address'})
184+ self.assertEqual(httplib.OK, response.status_code, response.content)
185+ parsed_device = json.loads(response.content)
186+ [returned_ip] = parsed_device["ip_addresses"]
187+ static_ip = StaticIPAddress.objects.filter(ip=returned_ip).first()
188+ self.assertIsNotNone(static_ip)
189+ self.assertEquals(IPADDRESS_TYPE.STICKY, static_ip.alloc_type)
190+
191+ def test__claims_ip_address_after_devices_new(self):
192+ ng = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
193+ ngi = factory.make_NodeGroupInterface(
194+ ng, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS)
195+ parent = factory.make_Node_with_Interface_on_Subnet(
196+ nodegroup=ng, subnet=ngi.subnet)
197+ # Run 'devices new', as a sanity check to ensure the object is created
198+ # the same way as it is when juju does it.
199+ self.client.post(
200+ reverse('devices_handler'),
201+ {
202+ 'op': 'new',
203+ 'hostname': "lxc-1",
204+ 'mac_addresses': "01:02:03:04:05:06",
205+ 'parent': parent.system_id,
206+ })
207+ # Silence 'update_host_maps'.
208+ device = Device.objects.first()
209+ self.patch_autospec(interface_module, "update_host_maps")
210+ response = self.client.post(
211+ get_device_uri(device), {'op': 'claim_sticky_ip_address'})
212+ self.assertEqual(httplib.OK, response.status_code, response.content)
213+ parsed_device = json.loads(response.content)
214+ # import pdb; pdb.set_trace()
215 [returned_ip] = parsed_device["ip_addresses"]
216 static_ip = StaticIPAddress.objects.filter(ip=returned_ip).first()
217 self.assertIsNotNone(static_ip)
218@@ -333,6 +414,39 @@
219 (returned_ip, returned_ip, given_ip.alloc_type)
220 )
221
222+ def test_503_if_no_subnet_found(self):
223+ device = factory.make_Node(
224+ installable=False, interface=True, disable_ipv4=False,
225+ owner=self.logged_in_user)
226+ # Silence 'update_host_maps'.
227+ self.patch_autospec(interface_module, "update_host_maps")
228+ response = self.client.post(
229+ get_device_uri(device),
230+ {
231+ 'op': 'claim_sticky_ip_address',
232+ })
233+ self.assertEqual(
234+ httplib.SERVICE_UNAVAILABLE, response.status_code,
235+ response.content)
236+
237+ @patch.object(Interface, 'claim_static_ips')
238+ def test_503_if_no_ip_found(self, claim_static_ips):
239+ claim_static_ips.side_effect = [list()]
240+
241+ device = factory.make_Node(
242+ installable=False, interface=True, disable_ipv4=False,
243+ owner=self.logged_in_user)
244+ # Silence 'update_host_maps'.
245+ self.patch_autospec(interface_module, "update_host_maps")
246+ response = self.client.post(
247+ get_device_uri(device),
248+ {
249+ 'op': 'claim_sticky_ip_address',
250+ })
251+ self.assertEqual(
252+ httplib.SERVICE_UNAVAILABLE, response.status_code,
253+ response.content)
254+
255 def test_creates_ip_for_specific_mac(self):
256 requested_address = factory.make_ip_address()
257 device = factory.make_Node(
258
259=== modified file 'src/maasserver/models/interface.py'
260--- src/maasserver/models/interface.py 2015-11-11 01:11:12 +0000
261+++ src/maasserver/models/interface.py 2015-12-03 19:55:35 +0000
262@@ -52,6 +52,7 @@
263 NODEGROUPINTERFACE_MANAGEMENT,
264 )
265 from maasserver.exceptions import (
266+ StaticIPAddressExhaustion,
267 StaticIPAddressOutOfRange,
268 StaticIPAddressUnavailable,
269 )
270@@ -350,6 +351,13 @@
271 def get_node(self):
272 return self.node
273
274+ def get_log_string(self):
275+ hostname = "<unknown-node>"
276+ node = self.get_node()
277+ if node is not None:
278+ hostname = node.hostname
279+ return "%s on %s" % (self.get_name(), hostname)
280+
281 def get_name(self):
282 return self.name
283
284@@ -879,6 +887,10 @@
285 be identified then its just set to DHCP.
286 """
287 found_subnet = None
288+ # XXX mpontillo 2015-11-29: since we tend to dump a large number of
289+ # subnets into the default VLAN, this assumption might be incorrect in
290+ # many cases, leading to interfaces being configured as AUTO when
291+ # they should be configured as DHCP.
292 for subnet in self.vlan.subnet_set.all():
293 ngi = subnet.get_managed_cluster_interface()
294 if ngi is not None:
295@@ -1109,44 +1121,92 @@
296 auto_ip, exclude_addresses)
297 if ngi is not None:
298 affected_nodegroups.add(ngi.nodegroup)
299- assigned_addresses.append(assigned_ip)
300- exclude_addresses.add(unicode(assigned_ip.ip))
301+ if assigned_ip is not None:
302+ assigned_addresses.append(assigned_ip)
303+ exclude_addresses.add(unicode(assigned_ip.ip))
304 self._update_dns_zones(affected_nodegroups)
305 return assigned_addresses
306
307 def _claim_auto_ip(self, auto_ip, exclude_addresses=[]):
308- """Claim an IP address for the `auto_ip`."""
309+ """Claim an IP address for the `auto_ip`.
310+
311+ :returns:NodeGroupInterface, new_ip_address
312+ """
313 # Check if already has a hostmap allocated for this MAC address.
314 subnet = auto_ip.subnet
315+ if subnet is None:
316+ maaslog.error(
317+ "Could not find subnet for interface %s." %
318+ (self.get_log_string()))
319+ raise StaticIPAddressUnavailable(
320+ "Automatic IP address cannot be configured on interface %s "
321+ "without an associated subnet." % self.get_name())
322+
323 ngi = subnet.get_managed_cluster_interface()
324+ if ngi is None:
325+ # Couldn't find a managed cluster interface for this node. So look
326+ # for any interface (must be an UNMANAGED interface, since any
327+ # managed NodeGroupInterface MUST have a Subnet link) whose
328+ # static or dynamic range is within the given subnet.
329+ ngi = NodeGroupInterface.objects.get_by_managed_range_for_subnet(
330+ subnet)
331+
332+ has_existing_mapping = False
333+ has_static_range = False
334+ has_dynamic_range = False
335+
336 if ngi is not None:
337- has_allocations = self._has_static_allocation_on_cluster(
338+ has_existing_mapping = self._has_static_allocation_on_cluster(
339 ngi.nodegroup, get_subnet_family(subnet))
340- else:
341- has_allocations = False
342-
343- # Create a new AUTO IP.
344- if ngi is not None:
345+ has_static_range = ngi.has_static_ip_range()
346+ has_dynamic_range = ngi.has_dynamic_ip_range()
347+
348+ if not has_static_range and has_dynamic_range:
349+ # This means we found a matching NodeGroupInterface, but only its
350+ # dynamic range is defined. Since a dynamic range is defined, that
351+ # means this subnet is NOT managed by MAAS (or it's misconfigured),
352+ # so we cannot just hand out a random IP address and risk a
353+ # duplicate IP address.
354+ maaslog.error(
355+ "Found matching NodeGroupInterface, but no static range has "
356+ "been defined for %s. (did you mean to configure DHCP?) " %
357+ (self.get_log_string()))
358+ raise StaticIPAddressUnavailable(
359+ "Cluster interface for %s only has a dynamic range. Configure "
360+ "a static range, or reconfigure the interface." %
361+ (self.get_name()))
362+
363+ if has_static_range:
364+ # Allocate a new AUTO address from the static range.
365 network = ngi.network
366 static_ip_range_low = ngi.static_ip_range_low
367 static_ip_range_high = ngi.static_ip_range_high
368 else:
369+ # We either found a NodeGroupInterface with no static or dynamic
370+ # range, or we have a Subnet not associated with a
371+ # NodeGroupInterface. This implies that it's okay to assign any
372+ # unused IP address on the subnet.
373 network = subnet.get_ipnetwork()
374 static_ip_range_low, static_ip_range_high = (
375 get_first_and_last_usable_host_in_network(network))
376+ in_use_ipset = subnet.get_ipranges_in_use()
377 new_ip = StaticIPAddress.objects.allocate_new(
378 network, static_ip_range_low, static_ip_range_high,
379 None, None, alloc_type=IPADDRESS_TYPE.AUTO,
380- subnet=subnet, exclude_addresses=exclude_addresses)
381+ subnet=subnet, exclude_addresses=exclude_addresses,
382+ in_use_ipset=in_use_ipset)
383 self.ip_addresses.add(new_ip)
384+ maaslog.info("Allocated automatic%s IP address %s for %s." % (
385+ " static" if has_static_range else "", new_ip.ip,
386+ self.get_log_string()))
387
388- # Update the hostmap for the new IP address if needed.
389- if ngi is not None and not has_allocations:
390+ if ngi is not None and not has_existing_mapping:
391+ # Update DHCP (if needed).
392 self._update_host_maps(ngi.nodegroup, new_ip)
393
394- # Made it this far then the AUTO IP address has been assigned and the
395- # hostmap has been updated if needed. We can now remove the original
396- # empty AUTO IP address.
397+ # If we made it this far, then the AUTO IP address has been assigned
398+ # and the hostmap has been updated if needed. We can now remove the
399+ # original empty AUTO IP address.
400 auto_ip.delete()
401 return ngi, new_ip
402
403@@ -1257,6 +1317,23 @@
404 if ngi is not None and ngi.subnet is not None:
405 discovered_subnets.append(ngi.subnet)
406
407+ if len(discovered_subnets) == 0:
408+ node = self.node
409+ if parent is not None:
410+ node = parent
411+ if node is None:
412+ hostname = "<unknown>"
413+ else:
414+ hostname = "'%s'" % node.hostname
415+ log_string = (
416+ "%s: Attempted to claim a static IP address, but no "
417+ "associated subnet could be found. (Recommission node %s "
418+ "in order for MAAS to discover the subnet.)" %
419+ (self.get_log_string(), hostname)
420+ )
421+ maaslog.warning(log_string)
422+ raise StaticIPAddressExhaustion(log_string)
423+
424 if requested_address is None:
425 # No requested address so claim a STATIC IP on all DISCOVERED
426 # subnets for this interface.
427@@ -1271,8 +1348,8 @@
428 # No valid subnets could be used to claim a STATIC IP address.
429 if not any(static_ips):
430 maaslog.error(
431- "Tried to allocate an IP to interface <%s>, but its "
432- "cluster interface is not known.", unicode(self))
433+ "Attempted sticky IP allocation failed for %s: could not "
434+ "find a cluster interface.", self.get_log_string())
435 return []
436 else:
437 return static_ips
438@@ -1294,7 +1371,8 @@
439 else:
440 raise StaticIPAddressOutOfRange(
441 "requested_address '%s' is not in a managed subnet for "
442- "this interface '%s'" % (requested_address, self.name))
443+ "interface '%s'." % (
444+ requested_address, self.get_name()))
445
446 def _get_parent_node(self):
447 """Return the parent node for this interface, if it exists (and this
448
449=== modified file 'src/maasserver/models/node.py'
450--- src/maasserver/models/node.py 2015-11-21 02:16:17 +0000
451+++ src/maasserver/models/node.py 2015-12-03 19:55:35 +0000
452@@ -2530,7 +2530,9 @@
453 # You can't start a node you don't own unless you're an admin.
454 raise PermissionDenied()
455 event = EVENT_TYPES.REQUEST_NODE_START
456- # if status is ALLOCATED, this start is actually for a deployment
457+ # If status is ALLOCATED, this start is actually for a deployment.
458+ # (Note: this is true even when nodes are being deployed from READY
459+ # state. See node_action.py; the node is acquired and then started.)
460 if self.status == NODE_STATUS.ALLOCATED:
461 event = EVENT_TYPES.REQUEST_NODE_START_DEPLOYMENT
462 self._register_request_event(
463@@ -2575,6 +2577,8 @@
464
465 if self.status == NODE_STATUS.ALLOCATED:
466 # Claim AUTO IP addresses for the node if it's ALLOCATED.
467+ # The current state being ALLOCATED is our indication that the node
468+ # is being deployed for the first time.
469 self.claim_auto_ips()
470 transition_monitor = (
471 TransitionMonitor.fromNode(self)
472
473=== modified file 'src/maasserver/models/nodegroupinterface.py'
474--- src/maasserver/models/nodegroupinterface.py 2015-10-29 19:10:30 +0000
475+++ src/maasserver/models/nodegroupinterface.py 2015-12-03 19:55:35 +0000
476@@ -172,6 +172,40 @@
477 else:
478 return None
479
480+ find_by_managed_range_for_subnet_query = dedent("""\
481+ SELECT ngi.*
482+ FROM
483+ maasserver_subnet AS subnet,
484+ maasserver_nodegroupinterface AS ngi,
485+ maasserver_nodegroup AS ng
486+ WHERE
487+ ngi.nodegroup_id = ng.id AND
488+ ng.status = 1 AND /* NodeGroup must be ENABLED */
489+ ((inet(ngi.ip_range_low) << network(subnet.cidr) AND
490+ inet(ngi.ip_range_high) << network(subnet.cidr))
491+ OR (inet(ngi.static_ip_range_low) << network(subnet.cidr) AND
492+ inet(ngi.static_ip_range_high) << network(subnet.cidr)))
493+ AND subnet.id = %s
494+ /* Prefer static ranges, since that's how we'll allocate addresses. */
495+ ORDER BY ngi.static_ip_range_low DESC NULLS LAST, ngi.id
496+ """)
497+
498+ def get_by_managed_range_for_subnet(self, subnet):
499+ """Return the first interface that could contain `address` in its
500+ dynamic or static range. (Prefer interfaces static ranges.)
501+ """
502+ # Circular imports
503+ from maasserver.models import Subnet
504+ assert isinstance(subnet, Subnet), (
505+ "%r is not a Subnet" % (subnet,))
506+ interfaces = self.raw(
507+ self.find_by_managed_range_for_subnet_query + " LIMIT 1",
508+ [subnet.id])
509+ for interface in interfaces:
510+ return interface # This is stable because the query is ordered.
511+ else:
512+ return None
513+
514
515 def get_default_vlan():
516 from maasserver.models.vlan import VLAN
517@@ -437,7 +471,7 @@
518 exclude = []
519 self.check_for_network_interface_clashes(exclude)
520
521- def has_dyanamic_ip_range(self):
522+ def has_dynamic_ip_range(self):
523 """Returns `True` if this `NodeGroupInterface` has a dynamic IP
524 range specified."""
525 return self.ip_range_low and self.ip_range_high
526@@ -445,7 +479,7 @@
527 def get_dynamic_ip_range(self):
528 """Returns a `MAASIPRange` for this `NodeGroupInterface`, if a dynamic
529 range is specified. Otherwise, returns `None`."""
530- if self.has_dyanamic_ip_range():
531+ if self.has_dynamic_ip_range():
532 return make_iprange(
533 self.ip_range_low, self.ip_range_high,
534 purpose='dynamic-range')
535@@ -645,7 +679,7 @@
536 assert isinstance(ipnetwork, IPNetwork)
537
538 ranges = set()
539- if self.has_dyanamic_ip_range():
540+ if self.has_dynamic_ip_range():
541 dynamic_range = self.get_dynamic_ip_range()
542 if dynamic_range in ipnetwork:
543 ranges.add(dynamic_range)
544
545=== modified file 'src/maasserver/models/staticipaddress.py'
546--- src/maasserver/models/staticipaddress.py 2015-11-17 00:33:35 +0000
547+++ src/maasserver/models/staticipaddress.py 2015-12-03 19:55:35 +0000
548@@ -166,7 +166,7 @@
549 dynamic_range_low, dynamic_range_high,
550 alloc_type=IPADDRESS_TYPE.AUTO, user=None,
551 requested_address=None, hostname=None, subnet=None,
552- exclude_addresses=[]):
553+ exclude_addresses=[], in_use_ipset=set()):
554 """Return a new StaticIPAddress.
555
556 :param network: The network the address should be allocated in.
557@@ -226,7 +226,8 @@
558 requested_address = self._async_find_free_ip(
559 static_range_low, static_range_high, static_range,
560 alloc_type, user,
561- exclude_addresses=exclude_addresses).wait(30)
562+ exclude_addresses=exclude_addresses,
563+ in_use_ipset=in_use_ipset).wait(30)
564 try:
565 return self._attempt_allocation(
566 requested_address, alloc_type, user,
567@@ -272,7 +273,7 @@
568
569 def _find_free_ip(
570 self, range_low, range_high, static_range, alloc_type,
571- user, exclude_addresses):
572+ user, exclude_addresses, in_use_ipset=set()):
573 """Helper function that finds a free IP address using a lock."""
574 # The set of _allocated_ addresses in the range is going to be
575 # smaller or at least no bigger than the set of addresses in the
576@@ -297,7 +298,8 @@
577 })
578 # Now find the first free address in the range.
579 for requested_address in static_range:
580- if requested_address not in existing:
581+ if (requested_address not in existing and
582+ requested_address not in in_use_ipset):
583 return requested_address
584 else:
585 raise StaticIPAddressExhaustion(
586@@ -840,4 +842,7 @@
587 else:
588 # (2) and (3): the Subnet has changed (could be to None)
589 subnet = Subnet.objects.get_best_subnet_for_ip(ipaddr)
590+ # We must save here, otherwise it's possible that we can't
591+ # traverse the interface_set many-to-many.
592+ self.save()
593 self._set_subnet(subnet, interfaces=self.interface_set.all())
594
595=== modified file 'src/maasserver/models/subnet.py'
596--- src/maasserver/models/subnet.py 2015-11-09 22:20:01 +0000
597+++ src/maasserver/models/subnet.py 2015-12-03 19:55:35 +0000
598@@ -389,11 +389,18 @@
599
600 def get_managed_cluster_interface(self):
601 """Return the cluster interface that manages this subnet."""
602+ # Prefer enabled, non-UNMANAGED networks.
603 interfaces = self.nodegroupinterface_set.filter(
604 nodegroup__status=NODEGROUP_STATUS.ENABLED)
605 interfaces = interfaces.exclude(
606 management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
607- return interfaces.first()
608+ ngi = interfaces.first()
609+ if ngi is None:
610+ # Circular imports
611+ from maasserver.models import NodeGroupInterface
612+ ngi = NodeGroupInterface.objects.get_by_managed_range_for_subnet(
613+ self)
614+ return ngi
615
616 def clean(self, *args, **kwargs):
617 self.validate_gateway_ip()
618
619=== modified file 'src/maasserver/models/tests/test_interface.py'
620--- src/maasserver/models/tests/test_interface.py 2015-11-11 01:04:18 +0000
621+++ src/maasserver/models/tests/test_interface.py 2015-12-03 19:55:35 +0000
622@@ -31,6 +31,7 @@
623 NODEGROUPINTERFACE_MANAGEMENT,
624 )
625 from maasserver.exceptions import (
626+ StaticIPAddressExhaustion,
627 StaticIPAddressOutOfRange,
628 StaticIPAddressUnavailable,
629 )
630@@ -80,6 +81,7 @@
631 MatchesDict,
632 MatchesListwise,
633 MatchesStructure,
634+ Not,
635 )
636
637
638@@ -2224,9 +2226,93 @@
639 self.assertEquals(subnet, observed[0].subnet)
640 self.assertTrue(
641 IPAddress(observed[0].ip) in (
642- IPRange(ngi.static_ip_range_low, ngi.static_ip_range_low)),
643- "Assigned IP address should be inside the static range "
644- "on the cluster.")
645+ IPRange(ngi.static_ip_range_low, ngi.static_ip_range_high)),
646+ "Assigned IP address %s should be inside the static range "
647+ "on the cluster (%s - %s)." % (
648+ observed[0].ip, ngi.static_ip_range_low,
649+ ngi.static_ip_range_high))
650+
651+ def test__claims_ip_address_in_static_ip_range_skips_gateway_ip(self):
652+ from maasserver.dns import config
653+ self.patch_autospec(interface_module, "update_host_maps")
654+ self.patch_autospec(config, "dns_update_zones")
655+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
656+ subnet = factory.make_Subnet(vlan=interface.vlan)
657+ nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
658+ ngi = factory.make_NodeGroupInterface(
659+ nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS,
660+ subnet=subnet)
661+ # Make it a really small range, just to be safe.
662+ ngi.static_ip_range_high = unicode(
663+ IPAddress(ngi.static_ip_range_low) + 1)
664+ ngi.save()
665+ ngi.subnet.gateway_ip = ngi.static_ip_range_low
666+ ngi.subnet.dns_servers = []
667+ ngi.subnet.save()
668+ factory.make_StaticIPAddress(
669+ alloc_type=IPADDRESS_TYPE.AUTO, ip="",
670+ subnet=subnet, interface=interface)
671+ observed = interface.claim_auto_ips()
672+ self.assertEquals(
673+ 1, len(observed),
674+ "Should have 1 AUTO IP addresses with an IP address assigned.")
675+ self.assertEquals(subnet, observed[0].subnet)
676+ self.assertTrue(
677+ IPAddress(observed[0].ip) in (
678+ IPRange(ngi.static_ip_range_low, ngi.static_ip_range_high)),
679+ "Assigned IP address %s should be inside the static range "
680+ "on the cluster (%s - %s)." % (
681+ observed[0].ip, ngi.static_ip_range_low,
682+ ngi.static_ip_range_high))
683+ self.assertThat(
684+ IPAddress(observed[0].ip), Not(Equals(IPAddress(
685+ ngi.subnet.gateway_ip))))
686+
687+ def test__claim_fails_if_subnet_missing(self):
688+ from maasserver.dns import config
689+ self.patch_autospec(interface_module, "update_host_maps")
690+ self.patch_autospec(config, "dns_update_zones")
691+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
692+ subnet = factory.make_Subnet(vlan=interface.vlan)
693+ nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
694+ factory.make_NodeGroupInterface(
695+ nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS,
696+ subnet=subnet)
697+ ip = factory.make_StaticIPAddress(
698+ alloc_type=IPADDRESS_TYPE.AUTO, ip="",
699+ subnet=subnet, interface=interface)
700+ ip.subnet = None
701+ ip.save()
702+ maaslog = self.patch_autospec(interface_module, "maaslog")
703+ with ExpectedException(StaticIPAddressUnavailable):
704+ interface.claim_auto_ips()
705+ self.expectThat(maaslog.error, MockCalledOnceWith(
706+ "Could not find subnet for interface %s." %
707+ interface.get_log_string()))
708+
709+ def test__claim_fails_if_no_static_range(self):
710+ from maasserver.dns import config
711+ self.patch_autospec(interface_module, "update_host_maps")
712+ self.patch_autospec(config, "dns_update_zones")
713+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
714+ subnet = factory.make_Subnet(vlan=interface.vlan)
715+ nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
716+ ngi = factory.make_NodeGroupInterface(
717+ nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS,
718+ subnet=subnet)
719+ ngi.static_ip_range_low = ""
720+ ngi.static_ip_range_high = ""
721+ ngi.save()
722+ factory.make_StaticIPAddress(
723+ alloc_type=IPADDRESS_TYPE.AUTO, ip="",
724+ subnet=subnet, interface=interface)
725+ maaslog = self.patch_autospec(interface_module, "maaslog")
726+ with ExpectedException(StaticIPAddressUnavailable):
727+ interface.claim_auto_ips()
728+ self.expectThat(maaslog.error, MockCalledOnceWith(
729+ "Found matching NodeGroupInterface, but no static range has "
730+ "been defined for %s. (did you mean to configure DHCP?) " %
731+ interface.get_log_string()))
732
733 def test__calls_update_host_maps(self):
734 from maasserver.dns import config
735@@ -2518,7 +2604,7 @@
736 StaticIPAddressOutOfRange, interface.claim_static_ips, ip_v6)
737 self.assertEquals(
738 "requested_address '%s' is not in a managed subnet for "
739- "this interface '%s'" % (ip_v6, interface.name),
740+ "interface '%s'." % (ip_v6, interface.name),
741 error.message)
742
743 def test__with_address_calls_link_subnet_with_ip_address(self):
744@@ -2610,3 +2696,27 @@
745 self.patch_autospec(iface, "link_subnet")
746 claimed_ips = iface.claim_static_ips()
747 self.assertThat(claimed_ips, HasLength(1))
748+
749+ def test__claim_static_fails_if_parent_subnet_cannot_be_found(self):
750+ from maasserver.dns import config
751+ self.patch_autospec(interface_module, "update_host_maps")
752+ self.patch_autospec(config, "dns_update_zones")
753+ subnet = factory.make_Subnet()
754+ nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
755+ factory.make_NodeGroupInterface(
756+ nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED,
757+ subnet=subnet)
758+ node = factory.make_Node_with_Interface_on_Subnet(
759+ subnet=subnet, unmanaged=True, status=NODE_STATUS.READY)
760+ # Simulate an unmanaged network without association to a subnet.
761+ # (this could happen after a migration)
762+ StaticIPAddress.objects.all().delete()
763+ interface = node.get_boot_interface()
764+ maaslog = self.patch_autospec(interface_module, "maaslog")
765+ with ExpectedException(StaticIPAddressExhaustion):
766+ interface.claim_static_ips()
767+ self.expectThat(maaslog.warning, MockCalledOnceWith(
768+ "%s: Attempted to claim a static IP address, but no associated "
769+ "subnet could be found. (Recommission node '%s' in order for "
770+ "MAAS to discover the subnet.)" %
771+ (interface.get_log_string(), node.hostname)))
772
773=== modified file 'src/maasserver/models/tests/test_nodegroupinterface.py'
774--- src/maasserver/models/tests/test_nodegroupinterface.py 2015-10-29 19:10:30 +0000
775+++ src/maasserver/models/tests/test_nodegroupinterface.py 2015-12-03 19:55:35 +0000
776@@ -217,6 +217,41 @@
777 self.assertEqual(if2, get_by_address(address))
778
779
780+class TestGetManagedRangeForSubnet(MAASServerTestCase):
781+
782+ def test__finds_interface_using_static_range(self):
783+ nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
784+ factory.make_NodeGroupInterface(nodegroup, network=IPNetwork(
785+ '192.168.2.0/24'))
786+ ngi = factory.make_NodeGroupInterface(
787+ nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED,
788+ ip='192.168.0.1', subnet_mask='', ip_range_low='',
789+ ip_range_high='', static_ip_range_low='192.168.1.10',
790+ static_ip_range_high='192.168.1.20')
791+ factory.make_NodeGroupInterface(nodegroup, network=IPNetwork(
792+ '192.168.3.0/24'))
793+ subnet = factory.make_Subnet(cidr='192.168.1.0/24')
794+ self.assertEqual(
795+ ngi, NodeGroupInterface.objects.get_by_managed_range_for_subnet(
796+ subnet))
797+
798+ def test__finds_interface_using_dynamic_range(self):
799+ nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
800+ factory.make_NodeGroupInterface(nodegroup, network=IPNetwork(
801+ '192.168.2.0/24'))
802+ ngi = factory.make_NodeGroupInterface(
803+ nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED,
804+ ip='192.168.0.1', subnet_mask='', static_ip_range_low='',
805+ static_ip_range_high='', ip_range_low='192.168.1.10',
806+ ip_range_high='192.168.1.20')
807+ factory.make_NodeGroupInterface(nodegroup, network=IPNetwork(
808+ '192.168.3.0/24'))
809+ subnet = factory.make_Subnet(cidr=IPNetwork('192.168.1.0/24'))
810+ result = NodeGroupInterface.objects.get_by_managed_range_for_subnet(
811+ subnet)
812+ self.assertEqual(ngi, result)
813+
814+
815 class TestNodeGroupInterface(MAASServerTestCase):
816
817 def test_network(self):
818
819=== modified file 'src/maasserver/testing/factory.py'
820--- src/maasserver/testing/factory.py 2015-11-18 18:14:30 +0000
821+++ src/maasserver/testing/factory.py 2015-12-03 19:55:35 +0000
822@@ -565,7 +565,7 @@
823 def make_Node_with_Interface_on_Subnet(
824 self, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP,
825 interface_count=1, nodegroup=None, vlan=None, subnet=None,
826- cidr=None, fabric=None, ifname=None, **kwargs):
827+ cidr=None, fabric=None, ifname=None, unmanaged=False, **kwargs):
828 """Create a Node that has a Interface which is on a Subnet that has a
829 NodeGroupInterface.
830
831@@ -592,7 +592,7 @@
832 ngis = subnet.nodegroupinterface_set.filter(nodegroup=nodegroup)
833 ngis = ngis.exclude(management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
834 ngi = ngis.first()
835- if ngi is None:
836+ if ngi is None and not unmanaged:
837 self.make_NodeGroupInterface(
838 nodegroup, vlan=vlan, management=management, subnet=subnet)
839 boot_interface = self.make_Interface(
840@@ -765,8 +765,18 @@
841 self, iftype=INTERFACE_TYPE.PHYSICAL, node=None, mac_address=None,
842 vlan=None, parents=None, name=None, cluster_interface=None,
843 ip=None, enabled=True, fabric=None):
844- if name is None and iftype != INTERFACE_TYPE.VLAN:
845- name = self.make_name('name')
846+ if name is None:
847+ if iftype in (INTERFACE_TYPE.PHYSICAL, INTERFACE_TYPE.UNKNOWN):
848+ name = self.make_name('eth')
849+ elif iftype == INTERFACE_TYPE.ALIAS:
850+ name = self.make_name('eth', sep=':')
851+ elif iftype == INTERFACE_TYPE.BOND:
852+ name = self.make_name('bond')
853+ elif iftype == INTERFACE_TYPE.UNKNOWN:
854+ name = self.make_name('eth')
855+ elif iftype == INTERFACE_TYPE.VLAN:
856+ # This will be determined by the VLAN's VID.
857+ name = None
858 if iftype is None:
859 iftype = INTERFACE_TYPE.PHYSICAL
860 if vlan is None:
861
862=== added file 'utilities/remote-reinstall'
863--- utilities/remote-reinstall 1970-01-01 00:00:00 +0000
864+++ utilities/remote-reinstall 2015-12-03 19:55:35 +0000
865@@ -0,0 +1,70 @@
866+#!/bin/bash
867+cd $(dirname $0)
868+cd ..
869+
870+get_ip() {
871+ ping -c 1 "$1" | head -1 | tr '(' ')' | cut -d')' -f 2
872+}
873+
874+trim () {
875+ read -rd '' $1 <<<"${!1}"
876+}
877+
878+die () {
879+ echo "$@"
880+ exit 1
881+}
882+
883+if [ "$1" == "" ]; then
884+ die "You must supply a hostname."
885+fi
886+
887+hostname="$1"
888+shift
889+
890+remote_basedir=/usr/lib/python2.7/dist-packages
891+directories="maascli maasserver provisioningserver metadataserver"
892+rsync_options=rlptv
893+ssh_run="ssh -oBatchMode=yes -l root $hostname"
894+
895+echo "Checking $hostname..."
896+maas_version=$($ssh_run "dpkg -s maas-region-controller-min | grep ^Version") \
897+ || die "Cannot SSH to root@$hostname."
898+ip_address=$(get_ip $hostname)
899+maas_version=$(echo $maas_version | cut -d':' -f 2)
900+trim maas_version
901+trim hostname
902+trim ip_address
903+echo ""
904+echo "Current MAAS version is: $maas_version"
905+echo ""
906+echo "WARNING: This will LIVE UPDATE the MAAS server at:"
907+if [ $hostname == $ip_address ]; then
908+ echo " $hostname"
909+else
910+ echo " $hostname ($ip_address)"
911+fi
912+echo ""
913+echo "This is a DESTRUCTIVE script that will OVERWRITE files installed by the"
914+echo "MAAS packages, and DELETE any extra files found on the server."
915+echo ""
916+echo "Destination directory:"
917+echo " $remote_basedir"
918+echo ""
919+echo "The following directories (under src/ in this sandbox) will be copied:"
920+echo " $directories"
921+echo ""
922+echo "Press <enter> to continue, ^C to cancel."
923+read
924+
925+echo "Synchronizing files..."
926+for dir in $directories; do
927+ remote_dir=${remote_basedir}/${dir}
928+ rsync -${rsync_options} --delete-after --exclude 'tests/' src/${dir}/ \
929+ root@${hostname}:${remote_dir} \
930+ && echo "Success." || die "Syncrhonization failed."
931+ $ssh_run "python -c \"import compileall; compileall.compile_dir('$remote_dir', force=True)\""
932+done
933+$ssh_run service maas-regiond restart
934+$ssh_run service apache2 restart
935+$ssh_run service maas-clusterd restart

Subscribers

People subscribed via source and target branches