Merge lp:~mpontillo/maas/ip-allocation-bugs-1.9 into lp:maas/1.9
- ip-allocation-bugs-1.9
- Merge into 1.9
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 |
Related bugs: |
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.)
Description of the change
Mike Pontillo (mpontillo) wrote : | # |
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.
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.
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.
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)
Mike Pontillo (mpontillo) wrote : | # |
Blake, I've added some unit tests. I think this is ready for another look. Thanks!
Blake Rouse (blake-rouse) wrote : | # |
Looks good. Don't forget to land this in trunk as well.
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
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 |
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.