Merge lp:~blake-rouse/maas/fix-1519527-1.9 into lp:~maas-committers/maas/trunk
- fix-1519527-1.9
- Merge into trunk
Proposed by
Blake Rouse
Status: | Superseded |
---|---|
Proposed branch: | lp:~blake-rouse/maas/fix-1519527-1.9 |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
1157 lines (+661/-63) (has conflicts) 18 files modified
.idea/encodings.xml (+3/-1) .idea/vcs.xml (+0/-6) Makefile (+2/-2) src/maasserver/api/devices.py (+17/-7) src/maasserver/api/tests/test_devices.py (+125/-11) src/maasserver/models/interface.py (+101/-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 (+193/-4) src/maasserver/models/tests/test_nodegroupinterface.py (+35/-0) src/maasserver/testing/factory.py (+14/-4) src/provisioningserver/drivers/power/amt.py (+11/-0) src/provisioningserver/drivers/power/ipmi.py (+9/-1) src/provisioningserver/drivers/power/tests/test_amt.py (+17/-0) src/provisioningserver/drivers/power/tests/test_ipmi.py (+5/-0) utilities/remote-reinstall (+70/-0) Text conflict in src/provisioningserver/drivers/power/amt.py Text conflict in src/provisioningserver/drivers/power/ipmi.py Text conflict in src/provisioningserver/drivers/power/tests/test_amt.py Text conflict in src/provisioningserver/drivers/power/tests/test_ipmi.py |
To merge this branch: | bzr merge lp:~blake-rouse/maas/fix-1519527-1.9 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
MAAS Maintainers | Pending | ||
Review via email: mp+279631@code.launchpad.net |
This proposal has been superseded by a proposal from 2015-12-04.
Commit message
Prevent claim_static_ips from allocated more than one IP address per discovered subnet.
Description of the change
To post a comment you must log in.
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-04 17:04:56 +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 'Makefile' |
27 | --- Makefile 2015-11-23 17:48:38 +0000 |
28 | +++ Makefile 2015-12-04 17:04:56 +0000 |
29 | @@ -501,8 +501,8 @@ |
30 | # this. |
31 | |
32 | # Old names. |
33 | -PACKAGING := $(abspath ../packaging.trunk) |
34 | -PACKAGING_BRANCH := lp:~maas-maintainers/maas/packaging |
35 | +PACKAGING := $(abspath ../packaging-1.9) |
36 | +PACKAGING_BRANCH := lp:~maas-maintainers/maas/packaging-1.9 |
37 | |
38 | packaging-tree = $(PACKAGING) |
39 | packaging-branch = $(PACKAGING_BRANCH) |
40 | |
41 | === modified file 'src/maasserver/api/devices.py' |
42 | --- src/maasserver/api/devices.py 2015-11-21 16:53:24 +0000 |
43 | +++ src/maasserver/api/devices.py 2015-12-04 17:04:56 +0000 |
44 | @@ -29,6 +29,7 @@ |
45 | from maasserver.exceptions import ( |
46 | MAASAPIBadRequest, |
47 | MAASAPIValidationError, |
48 | + StaticIPAddressExhaustion, |
49 | ) |
50 | from maasserver.fields import MAC_RE |
51 | from maasserver.forms import ( |
52 | @@ -169,7 +170,7 @@ |
53 | Returns 400 if the mac_address is not found on the device. |
54 | Returns 503 if there are not enough IPs left on the cluster interface |
55 | to which the mac_address is linked. |
56 | - Returns 503 if the requested_address falls in a dynamic range. |
57 | + Returns 503 if the interface does not have an associated subnet. |
58 | Returns 503 if the requested_address falls in a dynamic range. |
59 | Returns 503 if the requested_address is already allocated. |
60 | """ |
61 | @@ -192,8 +193,7 @@ |
62 | "mac_address %s not found on the device" % raw_mac) |
63 | requested_address = request.POST.get('requested_address', None) |
64 | if requested_address is None: |
65 | - sticky_ips = interface.claim_static_ips( |
66 | - requested_address=requested_address) |
67 | + sticky_ips = interface.claim_static_ips() |
68 | else: |
69 | subnet = Subnet.objects.get_best_subnet_for_ip( |
70 | requested_address) |
71 | @@ -203,9 +203,16 @@ |
72 | ip_address=requested_address), |
73 | ] |
74 | |
75 | - maaslog.info( |
76 | - "%s: Sticky IP address(es) allocated: %s", device.hostname, |
77 | - ', '.join(allocation.ip for allocation in sticky_ips)) |
78 | + if len(sticky_ips) == 0: |
79 | + raise StaticIPAddressExhaustion( |
80 | + "%s: An IP address could not be claimed at this time. " |
81 | + "Check your subnet ranges and utilization and try again." % |
82 | + device.hostname) |
83 | + else: |
84 | + maaslog.info( |
85 | + "%s: Sticky IP address(es) allocated: %s", device.hostname, |
86 | + ', '.join(allocation.ip for allocation in sticky_ips)) |
87 | + |
88 | return device |
89 | |
90 | @operation(idempotent=False) |
91 | @@ -272,7 +279,10 @@ |
92 | form = DeviceWithMACsForm(data=request.data, request=request) |
93 | if form.is_valid(): |
94 | device = form.save() |
95 | - maaslog.info("%s: Added new device", device.hostname) |
96 | + parent = device.parent |
97 | + maaslog.info( |
98 | + "%s: Added new device%s", device.hostname, |
99 | + "" if not parent else " (parent: %s)" % parent.hostname) |
100 | return device |
101 | else: |
102 | raise MAASAPIValidationError(form.errors) |
103 | |
104 | === modified file 'src/maasserver/api/tests/test_devices.py' |
105 | --- src/maasserver/api/tests/test_devices.py 2015-09-10 18:28:45 +0000 |
106 | +++ src/maasserver/api/tests/test_devices.py 2015-12-04 17:04:56 +0000 |
107 | @@ -24,8 +24,12 @@ |
108 | INTERFACE_TYPE, |
109 | IPADDRESS_TYPE, |
110 | NODE_STATUS, |
111 | + NODEGROUP_STATUS, |
112 | + NODEGROUPINTERFACE_MANAGEMENT, |
113 | ) |
114 | from maasserver.models import ( |
115 | + Device, |
116 | + Interface, |
117 | interface as interface_module, |
118 | Node, |
119 | NodeGroup, |
120 | @@ -37,6 +41,7 @@ |
121 | ) |
122 | from maasserver.testing.factory import factory |
123 | from maasserver.testing.orm import reload_object |
124 | +from mock import patch |
125 | from testtools.matchers import ( |
126 | HasLength, |
127 | Not, |
128 | @@ -285,17 +290,93 @@ |
129 | class TestClaimStickyIpAddressAPI(APITestCase): |
130 | """Tests for /api/1.0/devices/?op=claim_sticky_ip_address.""" |
131 | |
132 | - def test__claims_ip_address_from_cluster_interface(self): |
133 | - parent = factory.make_Node_with_Interface_on_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 | + def test__claims_ip_address_from_cluster_interface_static_range(self): |
144 | + ng = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED) |
145 | + ngi = factory.make_NodeGroupInterface( |
146 | + ng, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS) |
147 | + parent = factory.make_Node_with_Interface_on_Subnet( |
148 | + nodegroup=ng, subnet=ngi.subnet) |
149 | + device = factory.make_Node( |
150 | + installable=False, parent=parent, interface=True, |
151 | + disable_ipv4=False, owner=self.logged_in_user) |
152 | + # Silence 'update_host_maps'. |
153 | + self.patch_autospec(interface_module, "update_host_maps") |
154 | + response = self.client.post( |
155 | + get_device_uri(device), {'op': 'claim_sticky_ip_address'}) |
156 | + self.assertEqual(httplib.OK, response.status_code, response.content) |
157 | + parsed_device = json.loads(response.content) |
158 | + [returned_ip] = parsed_device["ip_addresses"] |
159 | + static_ip = StaticIPAddress.objects.filter(ip=returned_ip).first() |
160 | + self.assertIsNotNone(static_ip) |
161 | + self.assertEquals(IPADDRESS_TYPE.STICKY, static_ip.alloc_type) |
162 | + |
163 | + def test__claims_ip_address_from_unmanaged_cluster_interface(self): |
164 | + ng = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED) |
165 | + ngi = factory.make_NodeGroupInterface( |
166 | + ng, management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED) |
167 | + parent = factory.make_Node_with_Interface_on_Subnet( |
168 | + nodegroup=ng, subnet=ngi.subnet) |
169 | + device = factory.make_Node( |
170 | + installable=False, parent=parent, interface=True, |
171 | + disable_ipv4=False, owner=self.logged_in_user) |
172 | + # Silence 'update_host_maps'. |
173 | + self.patch_autospec(interface_module, "update_host_maps") |
174 | + response = self.client.post( |
175 | + get_device_uri(device), {'op': 'claim_sticky_ip_address'}) |
176 | + self.assertEqual(httplib.OK, response.status_code, response.content) |
177 | + parsed_device = json.loads(response.content) |
178 | + [returned_ip] = parsed_device["ip_addresses"] |
179 | + static_ip = StaticIPAddress.objects.filter(ip=returned_ip).first() |
180 | + self.assertIsNotNone(static_ip) |
181 | + self.assertEquals(IPADDRESS_TYPE.STICKY, static_ip.alloc_type) |
182 | + |
183 | + def test__claims_ip_address_from_detached_cluster_interface(self): |
184 | + ng = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED) |
185 | + ngi = factory.make_NodeGroupInterface( |
186 | + ng, management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED) |
187 | + subnet = ngi.subnet |
188 | + ngi.subnet = None |
189 | + ngi.save() |
190 | + parent = factory.make_Node_with_Interface_on_Subnet( |
191 | + nodegroup=ng, subnet=subnet, unmanaged=True) |
192 | + device = factory.make_Node( |
193 | + installable=False, parent=parent, interface=True, |
194 | + disable_ipv4=False, owner=self.logged_in_user) |
195 | + # Silence 'update_host_maps'. |
196 | + self.patch_autospec(interface_module, "update_host_maps") |
197 | + response = self.client.post( |
198 | + get_device_uri(device), {'op': 'claim_sticky_ip_address'}) |
199 | + self.assertEqual(httplib.OK, response.status_code, response.content) |
200 | + parsed_device = json.loads(response.content) |
201 | + [returned_ip] = parsed_device["ip_addresses"] |
202 | + static_ip = StaticIPAddress.objects.filter(ip=returned_ip).first() |
203 | + self.assertIsNotNone(static_ip) |
204 | + self.assertEquals(IPADDRESS_TYPE.STICKY, static_ip.alloc_type) |
205 | + |
206 | + def test__claims_ip_address_after_devices_new(self): |
207 | + ng = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED) |
208 | + ngi = factory.make_NodeGroupInterface( |
209 | + ng, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS) |
210 | + parent = factory.make_Node_with_Interface_on_Subnet( |
211 | + nodegroup=ng, subnet=ngi.subnet) |
212 | + # Run 'devices new', as a sanity check to ensure the object is created |
213 | + # the same way as it is when juju does it. |
214 | + self.client.post( |
215 | + reverse('devices_handler'), |
216 | + { |
217 | + 'op': 'new', |
218 | + 'hostname': "lxc-1", |
219 | + 'mac_addresses': "01:02:03:04:05:06", |
220 | + 'parent': parent.system_id, |
221 | + }) |
222 | + # Silence 'update_host_maps'. |
223 | + device = Device.objects.first() |
224 | + self.patch_autospec(interface_module, "update_host_maps") |
225 | + response = self.client.post( |
226 | + get_device_uri(device), {'op': 'claim_sticky_ip_address'}) |
227 | + self.assertEqual(httplib.OK, response.status_code, response.content) |
228 | + parsed_device = json.loads(response.content) |
229 | + # import pdb; pdb.set_trace() |
230 | [returned_ip] = parsed_device["ip_addresses"] |
231 | static_ip = StaticIPAddress.objects.filter(ip=returned_ip).first() |
232 | self.assertIsNotNone(static_ip) |
233 | @@ -333,6 +414,39 @@ |
234 | (returned_ip, returned_ip, given_ip.alloc_type) |
235 | ) |
236 | |
237 | + def test_503_if_no_subnet_found(self): |
238 | + device = factory.make_Node( |
239 | + installable=False, interface=True, disable_ipv4=False, |
240 | + owner=self.logged_in_user) |
241 | + # Silence 'update_host_maps'. |
242 | + self.patch_autospec(interface_module, "update_host_maps") |
243 | + response = self.client.post( |
244 | + get_device_uri(device), |
245 | + { |
246 | + 'op': 'claim_sticky_ip_address', |
247 | + }) |
248 | + self.assertEqual( |
249 | + httplib.SERVICE_UNAVAILABLE, response.status_code, |
250 | + response.content) |
251 | + |
252 | + @patch.object(Interface, 'claim_static_ips') |
253 | + def test_503_if_no_ip_found(self, claim_static_ips): |
254 | + claim_static_ips.side_effect = [list()] |
255 | + |
256 | + device = factory.make_Node( |
257 | + installable=False, interface=True, disable_ipv4=False, |
258 | + owner=self.logged_in_user) |
259 | + # Silence 'update_host_maps'. |
260 | + self.patch_autospec(interface_module, "update_host_maps") |
261 | + response = self.client.post( |
262 | + get_device_uri(device), |
263 | + { |
264 | + 'op': 'claim_sticky_ip_address', |
265 | + }) |
266 | + self.assertEqual( |
267 | + httplib.SERVICE_UNAVAILABLE, response.status_code, |
268 | + response.content) |
269 | + |
270 | def test_creates_ip_for_specific_mac(self): |
271 | requested_address = factory.make_ip_address() |
272 | device = factory.make_Node( |
273 | |
274 | === modified file 'src/maasserver/api/vlans.py' |
275 | === modified file 'src/maasserver/forms.py' |
276 | === modified file 'src/maasserver/models/interface.py' |
277 | --- src/maasserver/models/interface.py 2015-11-20 16:20:38 +0000 |
278 | +++ src/maasserver/models/interface.py 2015-12-04 17:04:56 +0000 |
279 | @@ -52,6 +52,7 @@ |
280 | NODEGROUPINTERFACE_MANAGEMENT, |
281 | ) |
282 | from maasserver.exceptions import ( |
283 | + StaticIPAddressExhaustion, |
284 | StaticIPAddressOutOfRange, |
285 | StaticIPAddressUnavailable, |
286 | ) |
287 | @@ -350,6 +351,13 @@ |
288 | def get_node(self): |
289 | return self.node |
290 | |
291 | + def get_log_string(self): |
292 | + hostname = "<unknown-node>" |
293 | + node = self.get_node() |
294 | + if node is not None: |
295 | + hostname = node.hostname |
296 | + return "%s on %s" % (self.get_name(), hostname) |
297 | + |
298 | def get_name(self): |
299 | return self.name |
300 | |
301 | @@ -879,6 +887,10 @@ |
302 | be identified then its just set to DHCP. |
303 | """ |
304 | found_subnet = None |
305 | + # XXX mpontillo 2015-11-29: since we tend to dump a large number of |
306 | + # subnets into the default VLAN, this assumption might be incorrect in |
307 | + # many cases, leading to interfaces being configured as AUTO when |
308 | + # they should be configured as DHCP. |
309 | for subnet in self.vlan.subnet_set.all(): |
310 | ngi = subnet.get_managed_cluster_interface() |
311 | if ngi is not None: |
312 | @@ -1109,44 +1121,92 @@ |
313 | auto_ip, exclude_addresses) |
314 | if ngi is not None: |
315 | affected_nodegroups.add(ngi.nodegroup) |
316 | - assigned_addresses.append(assigned_ip) |
317 | - exclude_addresses.add(unicode(assigned_ip.ip)) |
318 | + if assigned_ip is not None: |
319 | + assigned_addresses.append(assigned_ip) |
320 | + exclude_addresses.add(unicode(assigned_ip.ip)) |
321 | self._update_dns_zones(affected_nodegroups) |
322 | return assigned_addresses |
323 | |
324 | def _claim_auto_ip(self, auto_ip, exclude_addresses=[]): |
325 | - """Claim an IP address for the `auto_ip`.""" |
326 | + """Claim an IP address for the `auto_ip`. |
327 | + |
328 | + :returns:NodeGroupInterface, new_ip_address |
329 | + """ |
330 | # Check if already has a hostmap allocated for this MAC address. |
331 | subnet = auto_ip.subnet |
332 | + if subnet is None: |
333 | + maaslog.error( |
334 | + "Could not find subnet for interface %s." % |
335 | + (self.get_log_string())) |
336 | + raise StaticIPAddressUnavailable( |
337 | + "Automatic IP address cannot be configured on interface %s " |
338 | + "without an associated subnet." % self.get_name()) |
339 | + |
340 | ngi = subnet.get_managed_cluster_interface() |
341 | + if ngi is None: |
342 | + # Couldn't find a managed cluster interface for this node. So look |
343 | + # for any interface (must be an UNMANAGED interface, since any |
344 | + # managed NodeGroupInterface MUST have a Subnet link) whose |
345 | + # static or dynamic range is within the given subnet. |
346 | + ngi = NodeGroupInterface.objects.get_by_managed_range_for_subnet( |
347 | + subnet) |
348 | + |
349 | + has_existing_mapping = False |
350 | + has_static_range = False |
351 | + has_dynamic_range = False |
352 | + |
353 | if ngi is not None: |
354 | - has_allocations = self._has_static_allocation_on_cluster( |
355 | + has_existing_mapping = self._has_static_allocation_on_cluster( |
356 | ngi.nodegroup, get_subnet_family(subnet)) |
357 | - else: |
358 | - has_allocations = False |
359 | - |
360 | - # Create a new AUTO IP. |
361 | - if ngi is not None: |
362 | + has_static_range = ngi.has_static_ip_range() |
363 | + has_dynamic_range = ngi.has_dynamic_ip_range() |
364 | + |
365 | + if not has_static_range and has_dynamic_range: |
366 | + # This means we found a matching NodeGroupInterface, but only its |
367 | + # dynamic range is defined. Since a dynamic range is defined, that |
368 | + # means this subnet is NOT managed by MAAS (or it's misconfigured), |
369 | + # so we cannot just hand out a random IP address and risk a |
370 | + # duplicate IP address. |
371 | + maaslog.error( |
372 | + "Found matching NodeGroupInterface, but no static range has " |
373 | + "been defined for %s. (did you mean to configure DHCP?) " % |
374 | + (self.get_log_string())) |
375 | + raise StaticIPAddressUnavailable( |
376 | + "Cluster interface for %s only has a dynamic range. Configure " |
377 | + "a static range, or reconfigure the interface." % |
378 | + (self.get_name())) |
379 | + |
380 | + if has_static_range: |
381 | + # Allocate a new AUTO address from the static range. |
382 | network = ngi.network |
383 | static_ip_range_low = ngi.static_ip_range_low |
384 | static_ip_range_high = ngi.static_ip_range_high |
385 | else: |
386 | + # We either found a NodeGroupInterface with no static or dynamic |
387 | + # range, or we have a Subnet not associated with a |
388 | + # NodeGroupInterface. This implies that it's okay to assign any |
389 | + # unused IP address on the subnet. |
390 | network = subnet.get_ipnetwork() |
391 | static_ip_range_low, static_ip_range_high = ( |
392 | get_first_and_last_usable_host_in_network(network)) |
393 | + in_use_ipset = subnet.get_ipranges_in_use() |
394 | new_ip = StaticIPAddress.objects.allocate_new( |
395 | network, static_ip_range_low, static_ip_range_high, |
396 | None, None, alloc_type=IPADDRESS_TYPE.AUTO, |
397 | - subnet=subnet, exclude_addresses=exclude_addresses) |
398 | + subnet=subnet, exclude_addresses=exclude_addresses, |
399 | + in_use_ipset=in_use_ipset) |
400 | self.ip_addresses.add(new_ip) |
401 | + maaslog.info("Allocated automatic%s IP address %s for %s." % ( |
402 | + " static" if has_static_range else "", new_ip.ip, |
403 | + self.get_log_string())) |
404 | |
405 | - # Update the hostmap for the new IP address if needed. |
406 | - if ngi is not None and not has_allocations: |
407 | + if ngi is not None and not has_existing_mapping: |
408 | + # Update DHCP (if needed). |
409 | self._update_host_maps(ngi.nodegroup, new_ip) |
410 | |
411 | - # Made it this far then the AUTO IP address has been assigned and the |
412 | - # hostmap has been updated if needed. We can now remove the original |
413 | - # empty AUTO IP address. |
414 | + # If we made it this far, then the AUTO IP address has been assigned |
415 | + # and the hostmap has been updated if needed. We can now remove the |
416 | + # original empty AUTO IP address. |
417 | auto_ip.delete() |
418 | return ngi, new_ip |
419 | |
420 | @@ -1257,6 +1317,28 @@ |
421 | if ngi is not None and ngi.subnet is not None: |
422 | discovered_subnets.append(ngi.subnet) |
423 | |
424 | + # This must be a set because it is highly possible that the parent |
425 | + # has multiple subnets on the same interface or same subnet on multiple |
426 | + # interfaces. We only want to allocate one ip address per subnet. |
427 | + discovered_subnets = set(discovered_subnets) |
428 | + |
429 | + if len(discovered_subnets) == 0: |
430 | + node = self.node |
431 | + if parent is not None: |
432 | + node = parent |
433 | + if node is None: |
434 | + hostname = "<unknown>" |
435 | + else: |
436 | + hostname = "'%s'" % node.hostname |
437 | + log_string = ( |
438 | + "%s: Attempted to claim a static IP address, but no " |
439 | + "associated subnet could be found. (Recommission node %s " |
440 | + "in order for MAAS to discover the subnet.)" % |
441 | + (self.get_log_string(), hostname) |
442 | + ) |
443 | + maaslog.warning(log_string) |
444 | + raise StaticIPAddressExhaustion(log_string) |
445 | + |
446 | if requested_address is None: |
447 | # No requested address so claim a STATIC IP on all DISCOVERED |
448 | # subnets for this interface. |
449 | @@ -1271,8 +1353,8 @@ |
450 | # No valid subnets could be used to claim a STATIC IP address. |
451 | if not any(static_ips): |
452 | maaslog.error( |
453 | - "Tried to allocate an IP to interface <%s>, but its " |
454 | - "cluster interface is not known.", unicode(self)) |
455 | + "Attempted sticky IP allocation failed for %s: could not " |
456 | + "find a cluster interface.", self.get_log_string()) |
457 | return [] |
458 | else: |
459 | return static_ips |
460 | @@ -1294,7 +1376,8 @@ |
461 | else: |
462 | raise StaticIPAddressOutOfRange( |
463 | "requested_address '%s' is not in a managed subnet for " |
464 | - "this interface '%s'" % (requested_address, self.name)) |
465 | + "interface '%s'." % ( |
466 | + requested_address, self.get_name())) |
467 | |
468 | def _get_parent_node(self): |
469 | """Return the parent node for this interface, if it exists (and this |
470 | |
471 | === modified file 'src/maasserver/models/node.py' |
472 | --- src/maasserver/models/node.py 2015-11-21 16:54:43 +0000 |
473 | +++ src/maasserver/models/node.py 2015-12-04 17:04:56 +0000 |
474 | @@ -2539,7 +2539,9 @@ |
475 | # You can't start a node you don't own unless you're an admin. |
476 | raise PermissionDenied() |
477 | event = EVENT_TYPES.REQUEST_NODE_START |
478 | - # if status is ALLOCATED, this start is actually for a deployment |
479 | + # If status is ALLOCATED, this start is actually for a deployment. |
480 | + # (Note: this is true even when nodes are being deployed from READY |
481 | + # state. See node_action.py; the node is acquired and then started.) |
482 | if self.status == NODE_STATUS.ALLOCATED: |
483 | event = EVENT_TYPES.REQUEST_NODE_START_DEPLOYMENT |
484 | self._register_request_event( |
485 | @@ -2584,6 +2586,8 @@ |
486 | |
487 | if self.status == NODE_STATUS.ALLOCATED: |
488 | # Claim AUTO IP addresses for the node if it's ALLOCATED. |
489 | + # The current state being ALLOCATED is our indication that the node |
490 | + # is being deployed for the first time. |
491 | self.claim_auto_ips() |
492 | transition_monitor = ( |
493 | TransitionMonitor.fromNode(self) |
494 | |
495 | === modified file 'src/maasserver/models/nodegroupinterface.py' |
496 | --- src/maasserver/models/nodegroupinterface.py 2015-11-18 15:44:23 +0000 |
497 | +++ src/maasserver/models/nodegroupinterface.py 2015-12-04 17:04:56 +0000 |
498 | @@ -172,6 +172,40 @@ |
499 | else: |
500 | return None |
501 | |
502 | + find_by_managed_range_for_subnet_query = dedent("""\ |
503 | + SELECT ngi.* |
504 | + FROM |
505 | + maasserver_subnet AS subnet, |
506 | + maasserver_nodegroupinterface AS ngi, |
507 | + maasserver_nodegroup AS ng |
508 | + WHERE |
509 | + ngi.nodegroup_id = ng.id AND |
510 | + ng.status = 1 AND /* NodeGroup must be ENABLED */ |
511 | + ((inet(ngi.ip_range_low) << network(subnet.cidr) AND |
512 | + inet(ngi.ip_range_high) << network(subnet.cidr)) |
513 | + OR (inet(ngi.static_ip_range_low) << network(subnet.cidr) AND |
514 | + inet(ngi.static_ip_range_high) << network(subnet.cidr))) |
515 | + AND subnet.id = %s |
516 | + /* Prefer static ranges, since that's how we'll allocate addresses. */ |
517 | + ORDER BY ngi.static_ip_range_low DESC NULLS LAST, ngi.id |
518 | + """) |
519 | + |
520 | + def get_by_managed_range_for_subnet(self, subnet): |
521 | + """Return the first interface that could contain `address` in its |
522 | + dynamic or static range. (Prefer interfaces static ranges.) |
523 | + """ |
524 | + # Circular imports |
525 | + from maasserver.models import Subnet |
526 | + assert isinstance(subnet, Subnet), ( |
527 | + "%r is not a Subnet" % (subnet,)) |
528 | + interfaces = self.raw( |
529 | + self.find_by_managed_range_for_subnet_query + " LIMIT 1", |
530 | + [subnet.id]) |
531 | + for interface in interfaces: |
532 | + return interface # This is stable because the query is ordered. |
533 | + else: |
534 | + return None |
535 | + |
536 | |
537 | def get_default_vlan(): |
538 | from maasserver.models.vlan import VLAN |
539 | @@ -437,7 +471,7 @@ |
540 | exclude = [] |
541 | self.check_for_network_interface_clashes(exclude) |
542 | |
543 | - def has_dyanamic_ip_range(self): |
544 | + def has_dynamic_ip_range(self): |
545 | """Returns `True` if this `NodeGroupInterface` has a dynamic IP |
546 | range specified.""" |
547 | return self.ip_range_low and self.ip_range_high |
548 | @@ -445,7 +479,7 @@ |
549 | def get_dynamic_ip_range(self): |
550 | """Returns a `MAASIPRange` for this `NodeGroupInterface`, if a dynamic |
551 | range is specified. Otherwise, returns `None`.""" |
552 | - if self.has_dyanamic_ip_range(): |
553 | + if self.has_dynamic_ip_range(): |
554 | return make_iprange( |
555 | self.ip_range_low, self.ip_range_high, |
556 | purpose='dynamic-range') |
557 | @@ -645,7 +679,7 @@ |
558 | assert isinstance(ipnetwork, IPNetwork) |
559 | |
560 | ranges = set() |
561 | - if self.has_dyanamic_ip_range(): |
562 | + if self.has_dynamic_ip_range(): |
563 | dynamic_range = self.get_dynamic_ip_range() |
564 | if dynamic_range in ipnetwork: |
565 | ranges.add(dynamic_range) |
566 | |
567 | === modified file 'src/maasserver/models/staticipaddress.py' |
568 | --- src/maasserver/models/staticipaddress.py 2015-11-21 16:54:43 +0000 |
569 | +++ src/maasserver/models/staticipaddress.py 2015-12-04 17:04:56 +0000 |
570 | @@ -166,7 +166,7 @@ |
571 | dynamic_range_low, dynamic_range_high, |
572 | alloc_type=IPADDRESS_TYPE.AUTO, user=None, |
573 | requested_address=None, hostname=None, subnet=None, |
574 | - exclude_addresses=[]): |
575 | + exclude_addresses=[], in_use_ipset=set()): |
576 | """Return a new StaticIPAddress. |
577 | |
578 | :param network: The network the address should be allocated in. |
579 | @@ -226,7 +226,8 @@ |
580 | requested_address = self._async_find_free_ip( |
581 | static_range_low, static_range_high, static_range, |
582 | alloc_type, user, |
583 | - exclude_addresses=exclude_addresses).wait(30) |
584 | + exclude_addresses=exclude_addresses, |
585 | + in_use_ipset=in_use_ipset).wait(30) |
586 | try: |
587 | return self._attempt_allocation( |
588 | requested_address, alloc_type, user, |
589 | @@ -272,7 +273,7 @@ |
590 | |
591 | def _find_free_ip( |
592 | self, range_low, range_high, static_range, alloc_type, |
593 | - user, exclude_addresses): |
594 | + user, exclude_addresses, in_use_ipset=set()): |
595 | """Helper function that finds a free IP address using a lock.""" |
596 | # The set of _allocated_ addresses in the range is going to be |
597 | # smaller or at least no bigger than the set of addresses in the |
598 | @@ -297,7 +298,8 @@ |
599 | }) |
600 | # Now find the first free address in the range. |
601 | for requested_address in static_range: |
602 | - if requested_address not in existing: |
603 | + if (requested_address not in existing and |
604 | + requested_address not in in_use_ipset): |
605 | return requested_address |
606 | else: |
607 | raise StaticIPAddressExhaustion( |
608 | @@ -840,4 +842,7 @@ |
609 | else: |
610 | # (2) and (3): the Subnet has changed (could be to None) |
611 | subnet = Subnet.objects.get_best_subnet_for_ip(ipaddr) |
612 | + # We must save here, otherwise it's possible that we can't |
613 | + # traverse the interface_set many-to-many. |
614 | + self.save() |
615 | self._set_subnet(subnet, interfaces=self.interface_set.all()) |
616 | |
617 | === modified file 'src/maasserver/models/subnet.py' |
618 | --- src/maasserver/models/subnet.py 2015-11-18 15:44:23 +0000 |
619 | +++ src/maasserver/models/subnet.py 2015-12-04 17:04:56 +0000 |
620 | @@ -389,11 +389,18 @@ |
621 | |
622 | def get_managed_cluster_interface(self): |
623 | """Return the cluster interface that manages this subnet.""" |
624 | + # Prefer enabled, non-UNMANAGED networks. |
625 | interfaces = self.nodegroupinterface_set.filter( |
626 | nodegroup__status=NODEGROUP_STATUS.ENABLED) |
627 | interfaces = interfaces.exclude( |
628 | management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED) |
629 | - return interfaces.first() |
630 | + ngi = interfaces.first() |
631 | + if ngi is None: |
632 | + # Circular imports |
633 | + from maasserver.models import NodeGroupInterface |
634 | + ngi = NodeGroupInterface.objects.get_by_managed_range_for_subnet( |
635 | + self) |
636 | + return ngi |
637 | |
638 | def clean(self, *args, **kwargs): |
639 | self.validate_gateway_ip() |
640 | |
641 | === modified file 'src/maasserver/models/tests/test_interface.py' |
642 | --- src/maasserver/models/tests/test_interface.py 2015-11-18 15:44:23 +0000 |
643 | +++ src/maasserver/models/tests/test_interface.py 2015-12-04 17:04:56 +0000 |
644 | @@ -31,6 +31,7 @@ |
645 | NODEGROUPINTERFACE_MANAGEMENT, |
646 | ) |
647 | from maasserver.exceptions import ( |
648 | + StaticIPAddressExhaustion, |
649 | StaticIPAddressOutOfRange, |
650 | StaticIPAddressUnavailable, |
651 | ) |
652 | @@ -80,6 +81,7 @@ |
653 | MatchesDict, |
654 | MatchesListwise, |
655 | MatchesStructure, |
656 | + Not, |
657 | ) |
658 | |
659 | |
660 | @@ -2224,9 +2226,93 @@ |
661 | self.assertEquals(subnet, observed[0].subnet) |
662 | self.assertTrue( |
663 | IPAddress(observed[0].ip) in ( |
664 | - IPRange(ngi.static_ip_range_low, ngi.static_ip_range_low)), |
665 | - "Assigned IP address should be inside the static range " |
666 | - "on the cluster.") |
667 | + IPRange(ngi.static_ip_range_low, ngi.static_ip_range_high)), |
668 | + "Assigned IP address %s should be inside the static range " |
669 | + "on the cluster (%s - %s)." % ( |
670 | + observed[0].ip, ngi.static_ip_range_low, |
671 | + ngi.static_ip_range_high)) |
672 | + |
673 | + def test__claims_ip_address_in_static_ip_range_skips_gateway_ip(self): |
674 | + from maasserver.dns import config |
675 | + self.patch_autospec(interface_module, "update_host_maps") |
676 | + self.patch_autospec(config, "dns_update_zones") |
677 | + interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) |
678 | + subnet = factory.make_Subnet(vlan=interface.vlan) |
679 | + nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED) |
680 | + ngi = factory.make_NodeGroupInterface( |
681 | + nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS, |
682 | + subnet=subnet) |
683 | + # Make it a really small range, just to be safe. |
684 | + ngi.static_ip_range_high = unicode( |
685 | + IPAddress(ngi.static_ip_range_low) + 1) |
686 | + ngi.save() |
687 | + ngi.subnet.gateway_ip = ngi.static_ip_range_low |
688 | + ngi.subnet.dns_servers = [] |
689 | + ngi.subnet.save() |
690 | + factory.make_StaticIPAddress( |
691 | + alloc_type=IPADDRESS_TYPE.AUTO, ip="", |
692 | + subnet=subnet, interface=interface) |
693 | + observed = interface.claim_auto_ips() |
694 | + self.assertEquals( |
695 | + 1, len(observed), |
696 | + "Should have 1 AUTO IP addresses with an IP address assigned.") |
697 | + self.assertEquals(subnet, observed[0].subnet) |
698 | + self.assertTrue( |
699 | + IPAddress(observed[0].ip) in ( |
700 | + IPRange(ngi.static_ip_range_low, ngi.static_ip_range_high)), |
701 | + "Assigned IP address %s should be inside the static range " |
702 | + "on the cluster (%s - %s)." % ( |
703 | + observed[0].ip, ngi.static_ip_range_low, |
704 | + ngi.static_ip_range_high)) |
705 | + self.assertThat( |
706 | + IPAddress(observed[0].ip), Not(Equals(IPAddress( |
707 | + ngi.subnet.gateway_ip)))) |
708 | + |
709 | + def test__claim_fails_if_subnet_missing(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 | + factory.make_NodeGroupInterface( |
717 | + nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS, |
718 | + subnet=subnet) |
719 | + ip = factory.make_StaticIPAddress( |
720 | + alloc_type=IPADDRESS_TYPE.AUTO, ip="", |
721 | + subnet=subnet, interface=interface) |
722 | + ip.subnet = None |
723 | + ip.save() |
724 | + maaslog = self.patch_autospec(interface_module, "maaslog") |
725 | + with ExpectedException(StaticIPAddressUnavailable): |
726 | + interface.claim_auto_ips() |
727 | + self.expectThat(maaslog.error, MockCalledOnceWith( |
728 | + "Could not find subnet for interface %s." % |
729 | + interface.get_log_string())) |
730 | + |
731 | + def test__claim_fails_if_no_static_range(self): |
732 | + from maasserver.dns import config |
733 | + self.patch_autospec(interface_module, "update_host_maps") |
734 | + self.patch_autospec(config, "dns_update_zones") |
735 | + interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) |
736 | + subnet = factory.make_Subnet(vlan=interface.vlan) |
737 | + nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED) |
738 | + ngi = factory.make_NodeGroupInterface( |
739 | + nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS, |
740 | + subnet=subnet) |
741 | + ngi.static_ip_range_low = "" |
742 | + ngi.static_ip_range_high = "" |
743 | + ngi.save() |
744 | + factory.make_StaticIPAddress( |
745 | + alloc_type=IPADDRESS_TYPE.AUTO, ip="", |
746 | + subnet=subnet, interface=interface) |
747 | + maaslog = self.patch_autospec(interface_module, "maaslog") |
748 | + with ExpectedException(StaticIPAddressUnavailable): |
749 | + interface.claim_auto_ips() |
750 | + self.expectThat(maaslog.error, MockCalledOnceWith( |
751 | + "Found matching NodeGroupInterface, but no static range has " |
752 | + "been defined for %s. (did you mean to configure DHCP?) " % |
753 | + interface.get_log_string())) |
754 | |
755 | def test__calls_update_host_maps(self): |
756 | from maasserver.dns import config |
757 | @@ -2489,6 +2575,42 @@ |
758 | call(INTERFACE_LINK_TYPE.STATIC, subnet_v4), |
759 | call(INTERFACE_LINK_TYPE.STATIC, subnet_v6))) |
760 | |
761 | + def test__without_address_calls_link_subnet_once_per_subnet(self): |
762 | + nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED) |
763 | + interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) |
764 | + network_v4 = factory.make_ipv4_network() |
765 | + subnet_v4 = factory.make_Subnet(cidr=unicode(network_v4.cidr)) |
766 | + factory.make_NodeGroupInterface( |
767 | + nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP, |
768 | + subnet=subnet_v4) |
769 | + factory.make_StaticIPAddress( |
770 | + alloc_type=IPADDRESS_TYPE.DISCOVERED, ip="", |
771 | + subnet=subnet_v4, interface=interface) |
772 | + # Make it have the same subnet twice. |
773 | + factory.make_StaticIPAddress( |
774 | + alloc_type=IPADDRESS_TYPE.DISCOVERED, ip="", |
775 | + subnet=subnet_v4, interface=interface) |
776 | + network_v6 = factory.make_ipv6_network() |
777 | + subnet_v6 = factory.make_Subnet(cidr=unicode(network_v6.cidr)) |
778 | + factory.make_NodeGroupInterface( |
779 | + nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP, |
780 | + subnet=subnet_v6) |
781 | + factory.make_StaticIPAddress( |
782 | + alloc_type=IPADDRESS_TYPE.DISCOVERED, ip="", |
783 | + subnet=subnet_v6, interface=interface) |
784 | + # Make it have the same subnet twice. |
785 | + factory.make_StaticIPAddress( |
786 | + alloc_type=IPADDRESS_TYPE.DISCOVERED, ip="", |
787 | + subnet=subnet_v6, interface=interface) |
788 | + |
789 | + mock_link_subnet = self.patch_autospec(interface, "link_subnet") |
790 | + interface.claim_static_ips() |
791 | + self.assertThat( |
792 | + mock_link_subnet, |
793 | + MockCallsMatch( |
794 | + call(INTERFACE_LINK_TYPE.STATIC, subnet_v4), |
795 | + call(INTERFACE_LINK_TYPE.STATIC, subnet_v6))) |
796 | + |
797 | def test__without_address_does_nothing_if_none_managed(self): |
798 | interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL) |
799 | network_v4 = factory.make_ipv4_network() |
800 | @@ -2518,7 +2640,7 @@ |
801 | StaticIPAddressOutOfRange, interface.claim_static_ips, ip_v6) |
802 | self.assertEquals( |
803 | "requested_address '%s' is not in a managed subnet for " |
804 | - "this interface '%s'" % (ip_v6, interface.name), |
805 | + "interface '%s'." % (ip_v6, interface.name), |
806 | error.message) |
807 | |
808 | def test__with_address_calls_link_subnet_with_ip_address(self): |
809 | @@ -2573,6 +2695,49 @@ |
810 | call(INTERFACE_LINK_TYPE.STATIC, subnet_v4), |
811 | call(INTERFACE_LINK_TYPE.STATIC, subnet_v6))) |
812 | |
813 | + def test__device_no_address_calls_link_subnet_once_per_subnet(self): |
814 | + nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED) |
815 | + parent = factory.make_Node() |
816 | + parent_nic0 = factory.make_Interface( |
817 | + INTERFACE_TYPE.PHYSICAL, node=parent) |
818 | + parent_nic1 = factory.make_Interface( |
819 | + INTERFACE_TYPE.PHYSICAL, node=parent) |
820 | + network_v4 = factory.make_ipv4_network() |
821 | + subnet_v4 = factory.make_Subnet(cidr=unicode(network_v4.cidr)) |
822 | + factory.make_NodeGroupInterface( |
823 | + nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP, |
824 | + subnet=subnet_v4) |
825 | + factory.make_StaticIPAddress( |
826 | + alloc_type=IPADDRESS_TYPE.DISCOVERED, ip="", |
827 | + subnet=subnet_v4, interface=parent_nic0) |
828 | + # Make second interface on the parent have the same subnet. |
829 | + factory.make_StaticIPAddress( |
830 | + alloc_type=IPADDRESS_TYPE.DISCOVERED, ip="", |
831 | + subnet=subnet_v4, interface=parent_nic1) |
832 | + network_v6 = factory.make_ipv6_network() |
833 | + subnet_v6 = factory.make_Subnet(cidr=unicode(network_v6.cidr)) |
834 | + factory.make_NodeGroupInterface( |
835 | + nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP, |
836 | + subnet=subnet_v6) |
837 | + factory.make_StaticIPAddress( |
838 | + alloc_type=IPADDRESS_TYPE.DISCOVERED, ip="", |
839 | + subnet=subnet_v6, interface=parent_nic0) |
840 | + # Make second interface on the parent have the same subnet. |
841 | + factory.make_StaticIPAddress( |
842 | + alloc_type=IPADDRESS_TYPE.DISCOVERED, ip="", |
843 | + subnet=subnet_v6, interface=parent_nic1) |
844 | + device = factory.make_Device(parent=parent) |
845 | + device_interface = factory.make_Interface( |
846 | + INTERFACE_TYPE.PHYSICAL, node=device) |
847 | + |
848 | + mock_link_subnet = self.patch_autospec(device_interface, "link_subnet") |
849 | + device_interface.claim_static_ips() |
850 | + self.assertThat( |
851 | + mock_link_subnet, |
852 | + MockCallsMatch( |
853 | + call(INTERFACE_LINK_TYPE.STATIC, subnet_v4), |
854 | + call(INTERFACE_LINK_TYPE.STATIC, subnet_v6))) |
855 | + |
856 | def test__device_with_address_calls_link_subnet_with_ip_address(self): |
857 | parent = factory.make_Node() |
858 | interface = factory.make_Interface( |
859 | @@ -2610,3 +2775,27 @@ |
860 | self.patch_autospec(iface, "link_subnet") |
861 | claimed_ips = iface.claim_static_ips() |
862 | self.assertThat(claimed_ips, HasLength(1)) |
863 | + |
864 | + def test__claim_static_fails_if_parent_subnet_cannot_be_found(self): |
865 | + from maasserver.dns import config |
866 | + self.patch_autospec(interface_module, "update_host_maps") |
867 | + self.patch_autospec(config, "dns_update_zones") |
868 | + subnet = factory.make_Subnet() |
869 | + nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED) |
870 | + factory.make_NodeGroupInterface( |
871 | + nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED, |
872 | + subnet=subnet) |
873 | + node = factory.make_Node_with_Interface_on_Subnet( |
874 | + subnet=subnet, unmanaged=True, status=NODE_STATUS.READY) |
875 | + # Simulate an unmanaged network without association to a subnet. |
876 | + # (this could happen after a migration) |
877 | + StaticIPAddress.objects.all().delete() |
878 | + interface = node.get_boot_interface() |
879 | + maaslog = self.patch_autospec(interface_module, "maaslog") |
880 | + with ExpectedException(StaticIPAddressExhaustion): |
881 | + interface.claim_static_ips() |
882 | + self.expectThat(maaslog.warning, MockCalledOnceWith( |
883 | + "%s: Attempted to claim a static IP address, but no associated " |
884 | + "subnet could be found. (Recommission node '%s' in order for " |
885 | + "MAAS to discover the subnet.)" % |
886 | + (interface.get_log_string(), node.hostname))) |
887 | |
888 | === modified file 'src/maasserver/models/tests/test_node.py' |
889 | === modified file 'src/maasserver/models/tests/test_nodegroupinterface.py' |
890 | --- src/maasserver/models/tests/test_nodegroupinterface.py 2015-10-29 19:10:30 +0000 |
891 | +++ src/maasserver/models/tests/test_nodegroupinterface.py 2015-12-04 17:04:56 +0000 |
892 | @@ -217,6 +217,41 @@ |
893 | self.assertEqual(if2, get_by_address(address)) |
894 | |
895 | |
896 | +class TestGetManagedRangeForSubnet(MAASServerTestCase): |
897 | + |
898 | + def test__finds_interface_using_static_range(self): |
899 | + nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED) |
900 | + factory.make_NodeGroupInterface(nodegroup, network=IPNetwork( |
901 | + '192.168.2.0/24')) |
902 | + ngi = factory.make_NodeGroupInterface( |
903 | + nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED, |
904 | + ip='192.168.0.1', subnet_mask='', ip_range_low='', |
905 | + ip_range_high='', static_ip_range_low='192.168.1.10', |
906 | + static_ip_range_high='192.168.1.20') |
907 | + factory.make_NodeGroupInterface(nodegroup, network=IPNetwork( |
908 | + '192.168.3.0/24')) |
909 | + subnet = factory.make_Subnet(cidr='192.168.1.0/24') |
910 | + self.assertEqual( |
911 | + ngi, NodeGroupInterface.objects.get_by_managed_range_for_subnet( |
912 | + subnet)) |
913 | + |
914 | + def test__finds_interface_using_dynamic_range(self): |
915 | + nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED) |
916 | + factory.make_NodeGroupInterface(nodegroup, network=IPNetwork( |
917 | + '192.168.2.0/24')) |
918 | + ngi = factory.make_NodeGroupInterface( |
919 | + nodegroup, management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED, |
920 | + ip='192.168.0.1', subnet_mask='', static_ip_range_low='', |
921 | + static_ip_range_high='', ip_range_low='192.168.1.10', |
922 | + ip_range_high='192.168.1.20') |
923 | + factory.make_NodeGroupInterface(nodegroup, network=IPNetwork( |
924 | + '192.168.3.0/24')) |
925 | + subnet = factory.make_Subnet(cidr=IPNetwork('192.168.1.0/24')) |
926 | + result = NodeGroupInterface.objects.get_by_managed_range_for_subnet( |
927 | + subnet) |
928 | + self.assertEqual(ngi, result) |
929 | + |
930 | + |
931 | class TestNodeGroupInterface(MAASServerTestCase): |
932 | |
933 | def test_network(self): |
934 | |
935 | === modified file 'src/maasserver/models/tests/test_partition.py' |
936 | === modified file 'src/maasserver/models/tests/test_partitiontable.py' |
937 | === modified file 'src/maasserver/models/tests/test_staticipaddress.py' |
938 | === modified file 'src/maasserver/node_constraint_filter_forms.py' |
939 | === modified file 'src/maasserver/testing/factory.py' |
940 | --- src/maasserver/testing/factory.py 2015-11-17 23:03:36 +0000 |
941 | +++ src/maasserver/testing/factory.py 2015-12-04 17:04:56 +0000 |
942 | @@ -565,7 +565,7 @@ |
943 | def make_Node_with_Interface_on_Subnet( |
944 | self, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP, |
945 | interface_count=1, nodegroup=None, vlan=None, subnet=None, |
946 | - cidr=None, fabric=None, ifname=None, **kwargs): |
947 | + cidr=None, fabric=None, ifname=None, unmanaged=False, **kwargs): |
948 | """Create a Node that has a Interface which is on a Subnet that has a |
949 | NodeGroupInterface. |
950 | |
951 | @@ -592,7 +592,7 @@ |
952 | ngis = subnet.nodegroupinterface_set.filter(nodegroup=nodegroup) |
953 | ngis = ngis.exclude(management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED) |
954 | ngi = ngis.first() |
955 | - if ngi is None: |
956 | + if ngi is None and not unmanaged: |
957 | self.make_NodeGroupInterface( |
958 | nodegroup, vlan=vlan, management=management, subnet=subnet) |
959 | boot_interface = self.make_Interface( |
960 | @@ -765,8 +765,18 @@ |
961 | self, iftype=INTERFACE_TYPE.PHYSICAL, node=None, mac_address=None, |
962 | vlan=None, parents=None, name=None, cluster_interface=None, |
963 | ip=None, enabled=True, fabric=None): |
964 | - if name is None and iftype != INTERFACE_TYPE.VLAN: |
965 | - name = self.make_name('name') |
966 | + if name is None: |
967 | + if iftype in (INTERFACE_TYPE.PHYSICAL, INTERFACE_TYPE.UNKNOWN): |
968 | + name = self.make_name('eth') |
969 | + elif iftype == INTERFACE_TYPE.ALIAS: |
970 | + name = self.make_name('eth', sep=':') |
971 | + elif iftype == INTERFACE_TYPE.BOND: |
972 | + name = self.make_name('bond') |
973 | + elif iftype == INTERFACE_TYPE.UNKNOWN: |
974 | + name = self.make_name('eth') |
975 | + elif iftype == INTERFACE_TYPE.VLAN: |
976 | + # This will be determined by the VLAN's VID. |
977 | + name = None |
978 | if iftype is None: |
979 | iftype = INTERFACE_TYPE.PHYSICAL |
980 | if vlan is None: |
981 | |
982 | === modified file 'src/maasserver/tests/test_storage_layouts.py' |
983 | === modified file 'src/maasserver/utils/orm.py' |
984 | === modified file 'src/maasserver/websockets/handlers/node.py' |
985 | === modified file 'src/maasserver/websockets/handlers/tests/test_node.py' |
986 | === modified file 'src/provisioningserver/drivers/power/__init__.py' |
987 | === modified file 'src/provisioningserver/drivers/power/amt.py' |
988 | --- src/provisioningserver/drivers/power/amt.py 2015-11-17 02:05:20 +0000 |
989 | +++ src/provisioningserver/drivers/power/amt.py 2015-12-04 17:04:56 +0000 |
990 | @@ -53,6 +53,7 @@ |
991 | missing_packages.append(package) |
992 | return missing_packages |
993 | |
994 | +<<<<<<< TREE |
995 | def _render_wsman_state_xml(self, power_change): |
996 | """Render wsman state XML.""" |
997 | wsman_state_filename = join(dirname(__file__), "amt.wsman-state.xml") |
998 | @@ -373,3 +374,13 @@ |
999 | return self.amttool_query_state(ip_address, power_pass) |
1000 | elif amt_command == 'wsman': |
1001 | return self.wsman_query_state(ip_address, power_pass) |
1002 | +======= |
1003 | + def power_on(self, system_id, context): |
1004 | + raise NotImplementedError |
1005 | + |
1006 | + def power_off(self, system_id, context): |
1007 | + raise NotImplementedError |
1008 | + |
1009 | + def power_query(self, system_id, context): |
1010 | + raise NotImplementedError |
1011 | +>>>>>>> MERGE-SOURCE |
1012 | |
1013 | === modified file 'src/provisioningserver/drivers/power/ipmi.py' |
1014 | --- src/provisioningserver/drivers/power/ipmi.py 2015-11-20 21:08:27 +0000 |
1015 | +++ src/provisioningserver/drivers/power/ipmi.py 2015-12-04 17:04:56 +0000 |
1016 | @@ -44,7 +44,15 @@ |
1017 | """ |
1018 | |
1019 | |
1020 | -maaslog = get_maas_logger("drivers.power.ipmi") |
1021 | +<<<<<<< TREE |
1022 | +maaslog = get_maas_logger("drivers.power.ipmi") |
1023 | +======= |
1024 | +maaslog = get_maas_logger("drivers.power.ipmi") |
1025 | + |
1026 | + |
1027 | +def is_set(setting): |
1028 | + return not (setting is None or setting == "" or setting.isspace()) |
1029 | +>>>>>>> MERGE-SOURCE |
1030 | |
1031 | |
1032 | class IPMIPowerDriver(PowerDriver): |
1033 | |
1034 | === modified file 'src/provisioningserver/drivers/power/tests/test_amt.py' |
1035 | --- src/provisioningserver/drivers/power/tests/test_amt.py 2015-11-17 02:05:20 +0000 |
1036 | +++ src/provisioningserver/drivers/power/tests/test_amt.py 2015-12-04 17:04:56 +0000 |
1037 | @@ -99,6 +99,7 @@ |
1038 | missing = driver.detect_missing_packages() |
1039 | self.assertItemsEqual([], missing) |
1040 | |
1041 | +<<<<<<< TREE |
1042 | def test__render_wsman_state_xml_renders_xml(self): |
1043 | amt_power_driver = AMTPowerDriver() |
1044 | power_change = choice(['on', 'off', 'restart']) |
1045 | @@ -780,3 +781,19 @@ |
1046 | wsman_query_state_mock, MockCalledOnceWith( |
1047 | context['ip_address'], context['power_pass'])) |
1048 | self.expectThat(state, Equals('on')) |
1049 | +======= |
1050 | + def test_power_on(self): |
1051 | + driver = amt_module.AMTPowerDriver() |
1052 | + self.assertRaises( |
1053 | + NotImplementedError, driver.power_on, "fake_id", {}) |
1054 | + |
1055 | + def test_power_off(self): |
1056 | + driver = amt_module.AMTPowerDriver() |
1057 | + self.assertRaises( |
1058 | + NotImplementedError, driver.power_off, "fake_id", {}) |
1059 | + |
1060 | + def test_power_query(self): |
1061 | + driver = amt_module.AMTPowerDriver() |
1062 | + self.assertRaises( |
1063 | + NotImplementedError, driver.power_query, "fake_id", {}) |
1064 | +>>>>>>> MERGE-SOURCE |
1065 | |
1066 | === modified file 'src/provisioningserver/drivers/power/tests/test_ipmi.py' |
1067 | --- src/provisioningserver/drivers/power/tests/test_ipmi.py 2015-11-20 21:08:27 +0000 |
1068 | +++ src/provisioningserver/drivers/power/tests/test_ipmi.py 2015-12-04 17:04:56 +0000 |
1069 | @@ -269,8 +269,13 @@ |
1070 | ipmipower_command, env=env)) |
1071 | self.expectThat(result, Equals('other')) |
1072 | |
1073 | +<<<<<<< TREE |
1074 | def test__issue_ipmi_command_issues_raises_power_auth_error(self): |
1075 | _, _, _, _, _, _, _, context = make_parameters() |
1076 | +======= |
1077 | + def test__issue_ipmi_command_raises_power_fatal_error(self): |
1078 | + _, _, _, _, _, _, _, context = make_parameters() |
1079 | +>>>>>>> MERGE-SOURCE |
1080 | ipmi_power_driver = IPMIPowerDriver() |
1081 | popen_mock = self.patch(ipmi_module, 'Popen') |
1082 | process = popen_mock.return_value |
1083 | |
1084 | === added file 'utilities/remote-reinstall' |
1085 | --- utilities/remote-reinstall 1970-01-01 00:00:00 +0000 |
1086 | +++ utilities/remote-reinstall 2015-12-04 17:04:56 +0000 |
1087 | @@ -0,0 +1,70 @@ |
1088 | +#!/bin/bash |
1089 | +cd $(dirname $0) |
1090 | +cd .. |
1091 | + |
1092 | +get_ip() { |
1093 | + ping -c 1 "$1" | head -1 | tr '(' ')' | cut -d')' -f 2 |
1094 | +} |
1095 | + |
1096 | +trim () { |
1097 | + read -rd '' $1 <<<"${!1}" |
1098 | +} |
1099 | + |
1100 | +die () { |
1101 | + echo "$@" |
1102 | + exit 1 |
1103 | +} |
1104 | + |
1105 | +if [ "$1" == "" ]; then |
1106 | + die "You must supply a hostname." |
1107 | +fi |
1108 | + |
1109 | +hostname="$1" |
1110 | +shift |
1111 | + |
1112 | +remote_basedir=/usr/lib/python2.7/dist-packages |
1113 | +directories="maascli maasserver provisioningserver metadataserver" |
1114 | +rsync_options=rlptv |
1115 | +ssh_run="ssh -oBatchMode=yes -l root $hostname" |
1116 | + |
1117 | +echo "Checking $hostname..." |
1118 | +maas_version=$($ssh_run "dpkg -s maas-region-controller-min | grep ^Version") \ |
1119 | + || die "Cannot SSH to root@$hostname." |
1120 | +ip_address=$(get_ip $hostname) |
1121 | +maas_version=$(echo $maas_version | cut -d':' -f 2) |
1122 | +trim maas_version |
1123 | +trim hostname |
1124 | +trim ip_address |
1125 | +echo "" |
1126 | +echo "Current MAAS version is: $maas_version" |
1127 | +echo "" |
1128 | +echo "WARNING: This will LIVE UPDATE the MAAS server at:" |
1129 | +if [ $hostname == $ip_address ]; then |
1130 | + echo " $hostname" |
1131 | +else |
1132 | + echo " $hostname ($ip_address)" |
1133 | +fi |
1134 | +echo "" |
1135 | +echo "This is a DESTRUCTIVE script that will OVERWRITE files installed by the" |
1136 | +echo "MAAS packages, and DELETE any extra files found on the server." |
1137 | +echo "" |
1138 | +echo "Destination directory:" |
1139 | +echo " $remote_basedir" |
1140 | +echo "" |
1141 | +echo "The following directories (under src/ in this sandbox) will be copied:" |
1142 | +echo " $directories" |
1143 | +echo "" |
1144 | +echo "Press <enter> to continue, ^C to cancel." |
1145 | +read |
1146 | + |
1147 | +echo "Synchronizing files..." |
1148 | +for dir in $directories; do |
1149 | + remote_dir=${remote_basedir}/${dir} |
1150 | + rsync -${rsync_options} --delete-after --exclude 'tests/' src/${dir}/ \ |
1151 | + root@${hostname}:${remote_dir} \ |
1152 | + && echo "Success." || die "Syncrhonization failed." |
1153 | + $ssh_run "python -c \"import compileall; compileall.compile_dir('$remote_dir', force=True)\"" |
1154 | +done |
1155 | +$ssh_run service maas-regiond restart |
1156 | +$ssh_run service apache2 restart |
1157 | +$ssh_run service maas-clusterd restart |