Merge lp:~blake-rouse/maas/fix-1519527-1.9 into lp:~maas-committers/maas/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
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.

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