Merge lp:~mpontillo/maas/update-host-maps-cleanup-bug-1440102 into lp:~maas-committers/maas/trunk

Proposed by Mike Pontillo
Status: Merged
Approved by: Mike Pontillo
Approved revision: no longer in the source branch.
Merged at revision: 4020
Proposed branch: lp:~mpontillo/maas/update-host-maps-cleanup-bug-1440102
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 1794 lines (+399/-244)
15 files modified
src/maasserver/api/devices.py (+4/-23)
src/maasserver/api/ip_addresses.py (+7/-9)
src/maasserver/api/nodes.py (+1/-7)
src/maasserver/api/tests/test_devices.py (+17/-17)
src/maasserver/api/tests/test_ipaddresses.py (+19/-13)
src/maasserver/api/tests/test_node.py (+7/-6)
src/maasserver/models/macaddress.py (+141/-38)
src/maasserver/models/macipaddresslink.py (+2/-0)
src/maasserver/models/node.py (+41/-24)
src/maasserver/models/signals/dns.py (+10/-10)
src/maasserver/models/tests/test_macaddress.py (+65/-43)
src/maasserver/models/tests/test_node.py (+48/-35)
src/maasserver/websockets/handlers/device.py (+4/-4)
src/maasserver/websockets/handlers/tests/test_device.py (+23/-14)
src/maastesting/testcase.py (+10/-1)
To merge this branch: bzr merge lp:~mpontillo/maas/update-host-maps-cleanup-bug-1440102
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+261351@code.launchpad.net

Commit message

Move responsibility for calling update_host_maps() and update_dns_zones() away from the caller, and into the model.

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

I'm putting this up for review even though there is a little more cleanup to be done, so people can get started looking at it. All the test cases are passing in this iteration, though I'll be doing some manual testing and running it through CI as well, to ensure it's okay.

This change consolidates the logic for updating host maps and DNS zones within the claim_static_ips() and set_static_ip() methods inside macaddress.py. (Sounds easy? Not really, because lots of tests depend on using a mock to forego updating DNS and/or host maps, depending on their particular use case.)

I'm not completely happy with this change, mainly because moving around dozens of mocks makes it extremely hard to refactor this code.

What I'd like to do, given the time, is to change all callers of update_host_maps(), update_dns_zones() (and any related functions) to call via a module, instead of importing a function. This way when we mock the calls, we already know what to mock, and we can refactor as much as we like without impacting the dozens of tests that care about it.

Also on the TODO list is changing the signature of the update_nodegroup_host_maps() function, which is currently an awkward (nodegroup, claims, nodegroups=None). (I extracted the method in a hurry.)

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

Updated update_nodegroup_host_maps() signature. (it now just takes a list of nodegroups, but the old update_host_maps() will remain the same and ignore the node's nodegroup if passed in, because it breaks test cases without that behavior; even though I'm not positive the behavior is correct, I don't want to risk it.)

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

The other thing I don't like about this change is that updating the model suddenly has far-reaching side effects.

I made the side effect explicit by passing in the "update_host_maps" boolean to a few functions. I might have preferred a "dependency injection" style approach, but at least this gives callers (especially tests) the option to avoid the side effect.

If the side-effect was easy to mock in a consistent way (as I suggested by saying it should be called via its own module), I think it would be okay to remove the boolean. But in this case, adding the boolean prevented an even worse explosion in terms of test cases that needed to be updated.

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

Note: CI failed with the following errors:

2015-06-11 18:40:58 ERROR juju.provider.common bootstrap.go:122 bootstrap failed: cannot start bootstrap instance: gomaasapi: got error back from server: 500 INTERNAL SERVER ERROR ('MAC' object has no attribute 'encode')
2015-06-11 18:40:58 INFO juju.cmd cmd.go:113 Bootstrap failed, destroying environment
2015-06-11 18:40:58 INFO juju.provider.common destroy.go:15 destroying environment "maas"
2015-06-11 18:41:02 ERROR juju.cmd supercommand.go:323 cannot start bootstrap instance: gomaasapi: got error back from server: 500 INTERNAL SERVER ERROR ('MAC' object has no attribute 'encode')

I'm looking into this...

Revision history for this message
Raphaël Badin (rvb) wrote :

> Note: CI failed with the following errors:
>
> 2015-06-11 18:40:58 ERROR juju.provider.common bootstrap.go:122 bootstrap
> failed: cannot start bootstrap instance: gomaasapi: got error back from
> server: 500 INTERNAL SERVER ERROR ('MAC' object has no attribute 'encode')
> 2015-06-11 18:40:58 INFO juju.cmd cmd.go:113 Bootstrap failed, destroying
> environment
> 2015-06-11 18:40:58 INFO juju.provider.common destroy.go:15 destroying
> environment "maas"
> 2015-06-11 18:41:02 ERROR juju.cmd supercommand.go:323 cannot start bootstrap
> instance: gomaasapi: got error back from server: 500 INTERNAL SERVER ERROR
> ('MAC' object has no attribute 'encode')
>
> I'm looking into this...

I found the problem: CreateHostMaps expects its 'mac_address' parameter(s)to be something that can be converted into unicode (see CreateHostMaps's signature in ps/rpc/cluster.py). But here you're passing the raw MAC (maasserver/fields.py) object instead of passing the mac address as a string. That is because you removed the code http://paste.ubuntu.com/11700836/. I assume the same problem will happen for the IP address.

Revision history for this message
Raphaël Badin (rvb) :
Revision history for this message
Gavin Panella (allenap) wrote :

No objections, but I don't understand what the problem was and how this improves the situation. Some background please :)

review: Needs Information
Revision history for this message
Raphaël Badin (rvb) wrote :

@Gavin: have a look at the related bug for some context. But in a nutshell this is trying to reduce some code duplication that was added at the end of last cycle. The code in question is the code that does: claim_ip/update host map. It's duplicated in a couple of places, we've got a bunch of slightly different code that does very similar things at different levels in the tree (similar code in the API and in the model). This code will be changed when we move to the networking model and this is an attempt at rationalizing it before we even have to change it.

Revision history for this message
Raphaël Badin (rvb) wrote :

@Mike: you missed the create() method in websockets/handlers/device.py. It needs refactoring as well. There is even a mention of the bug you're fixing in that code. If applying your refactoring there improves this code, this is definitely proof that this change is all for the better.

Revision history for this message
Gavin Panella (allenap) wrote :

I don't much like the update_host_maps boolean flag, but it's okay; if we figure out a better way it'll be easy to change.

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

@Gavin, yes, I don't like it either, but I think it's a step in the right direction. There are a couple of places where we need it still; specifically where we combine the use of this function with "commit_within_atomic_block()" and try to make (for example) the creation of an IP address + reservation "atomic" (manually).

Another thing I noticed is, this code would be much easier to refactor if we didn't import *specific functions* so much. If we call via indirection (i.e. import maasserver.dhcp, dhcp.update_host_maps) it would have made it orders of magnitude easier to update all these tests. (and update them again in the future.)

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

I think this is ready to land, but I'm doing some final testing first.

I've changed the way the mocks are done for most of the affected tests; rather than using string constants, I've added a way to "self.patch()" using just a symbol. This requires importing a module instead of a function, but I think it's worth it. (When I first joined the MAAS team, the way mocks were used really violated the principle of least surprise for me... and the alternatives don't seem DRY-compliant.)

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

This version didn't pass CI. I think I missed a spot where I need to do MAC(...) instead of assuming it's the proper type. (and not, say, unicode). Line 716 in the current diff:

ip_mapping = [(static_ip.ip, self.mac_address.get_raw())]

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (77.9 KiB)

The attempt to merge lp:~mpontillo/maas/update-host-maps-cleanup-bug-1440102 into lp:maas failed. Below is the output from the failed tests.

Ign http://security.ubuntu.com trusty-security InRelease
Hit http://security.ubuntu.com trusty-security Release.gpg
Hit http://security.ubuntu.com trusty-security Release
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Get:1 http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg [933 B]
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Hit http://security.ubuntu.com trusty-security/main Sources
Get:2 http://nova.clouds.archive.ubuntu.com trusty-updates Release [63.5 kB]
Hit http://security.ubuntu.com trusty-security/universe Sources
Hit http://security.ubuntu.com trusty-security/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Hit http://security.ubuntu.com trusty-security/universe amd64 Packages
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Get:3 http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources [207 kB]
Get:4 http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources [120 kB]
Get:5 http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages [537 kB]
Get:6 http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages [286 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Fetched 1,214 kB in 3s (376 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 authbind bind9 bind9utils build-essential bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libpq-dev make nodejs-legacy npm pep8 phantomjs postgresql pyflakes python-apt python-bson python-bzrlib python-convoy python-coverage python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-extras python-fixtures python-flake8 python-formencode python-hivex python-httplib2 python-iscpy python-jinja2 python-jsonschema python-lockfile python-lxml python-mock python-netaddr python-netifaces python-nose python-oauth python-openssl python-paramiko ...

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

The failure was in:

provisioningserver.rpc.tests.test_clusterservice.TestClusterProtocol_PowerQuery.test_returns_power_state

This seems to be a flaky test case. It fails for me about ~10-20% of the time.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/maasserver/api/devices.py'
--- src/maasserver/api/devices.py 2015-04-23 17:15:45 +0000
+++ src/maasserver/api/devices.py 2015-06-15 08:11:13 +0000
@@ -21,7 +21,6 @@
21 OperationsHandler,21 OperationsHandler,
22)22)
23from maasserver.api.utils import get_optional_list23from maasserver.api.utils import get_optional_list
24from maasserver.dns.config import dns_update_zones
25from maasserver.enum import (24from maasserver.enum import (
26 IPADDRESS_TYPE,25 IPADDRESS_TYPE,
27 NODE_PERMISSION,26 NODE_PERMISSION,
@@ -37,10 +36,7 @@
37 DeviceWithMACsForm,36 DeviceWithMACsForm,
38 ReleaseIPForm,37 ReleaseIPForm,
39)38)
40from maasserver.models import (39from maasserver.models import MACAddress
41 MACAddress,
42 NodeGroup,
43)
44from maasserver.models.node import Device40from maasserver.models.node import Device
45from piston.utils import rc41from piston.utils import rc
4642
@@ -185,27 +181,12 @@
185 if requested_address is None:181 if requested_address is None:
186 sticky_ips = mac_address.claim_static_ips(182 sticky_ips = mac_address.claim_static_ips(
187 alloc_type=IPADDRESS_TYPE.STICKY,183 alloc_type=IPADDRESS_TYPE.STICKY,
188 requested_address=requested_address)184 requested_address=requested_address, update_host_maps=True)
189 claims = [
190 (static_ip.ip, mac_address.mac_address.get_raw())
191 for static_ip in sticky_ips]
192 device.update_host_maps(claims)
193 else:185 else:
194 sticky_ip = mac_address.set_static_ip(186 sticky_ip = mac_address.set_static_ip(
195 requested_address, request.user)187 requested_address, request.user, update_host_maps=True)
196 sticky_ips = [sticky_ip]188 sticky_ips = [sticky_ip]
197 dhcp_managed_clusters = [189
198 cluster
199 for cluster in NodeGroup.objects.all()
200 if cluster.manages_dhcp()
201 ]
202 device.update_host_maps(
203 [(sticky_ip.ip, mac_address.mac_address.get_raw())],
204 dhcp_managed_clusters)
205 # Use the master cluster DNS zone for devices.
206 # This is a temporary measure until we support setting a specific
207 # zone for each MAC.
208 dns_update_zones([NodeGroup.objects.ensure_master()])
209 maaslog.info(190 maaslog.info(
210 "%s: Sticky IP address(es) allocated: %s", device.hostname,191 "%s: Sticky IP address(es) allocated: %s", device.hostname,
211 ', '.join(allocation.ip for allocation in sticky_ips))192 ', '.join(allocation.ip for allocation in sticky_ips))
212193
=== modified file 'src/maasserver/api/ip_addresses.py'
--- src/maasserver/api/ip_addresses.py 2015-06-10 11:20:09 +0000
+++ src/maasserver/api/ip_addresses.py 2015-06-15 08:11:13 +0000
@@ -26,10 +26,7 @@
26 get_mandatory_param,26 get_mandatory_param,
27 get_optional_param,27 get_optional_param,
28)28)
29from maasserver.clusterrpc.dhcp import (29from maasserver.clusterrpc import dhcp
30 remove_host_maps,
31 update_host_maps,
32)
33from maasserver.enum import IPADDRESS_TYPE30from maasserver.enum import IPADDRESS_TYPE
34from maasserver.exceptions import (31from maasserver.exceptions import (
35 MAASAPIBadRequest,32 MAASAPIBadRequest,
@@ -89,6 +86,8 @@
89 alloc_type=IPADDRESS_TYPE.USER_RESERVED,86 alloc_type=IPADDRESS_TYPE.USER_RESERVED,
90 requested_address=requested_address,87 requested_address=requested_address,
91 user=user, hostname=hostname)88 user=user, hostname=hostname)
89 from maasserver.dns import config as dns_config
90 dns_config.dns_update_zones([interface.nodegroup])
92 maaslog.info("User %s was allocated IP %s", user.username, sip.ip)91 maaslog.info("User %s was allocated IP %s", user.username, sip.ip)
93 else:92 else:
94 # The user has requested a static IP linked to a MAC93 # The user has requested a static IP linked to a MAC
@@ -116,10 +115,11 @@
116 sip.ip: mac_address.mac_address,115 sip.ip: mac_address.mac_address,
117 }116 }
118 }117 }
118
119 # Commit the DB changes before we do RPC calls.119 # Commit the DB changes before we do RPC calls.
120 commit_within_atomic_block()120 commit_within_atomic_block()
121 update_host_maps_failures = list(121 update_host_maps_failures = list(
122 update_host_maps(host_map_updates))122 dhcp.update_host_maps(host_map_updates))
123 if len(update_host_maps_failures) > 0:123 if len(update_host_maps_failures) > 0:
124 # Deallocate the static IPs and delete the MAC address124 # Deallocate the static IPs and delete the MAC address
125 # if it doesn't have a Node attached.125 # if it doesn't have a Node attached.
@@ -130,6 +130,7 @@
130130
131 # There will only ever be one error, so raise that.131 # There will only ever be one error, so raise that.
132 raise update_host_maps_failures[0].value132 raise update_host_maps_failures[0].value
133
133 maaslog.info(134 maaslog.info(
134 "User %s was allocated IP %s for MAC address %s",135 "User %s was allocated IP %s for MAC address %s",
135 user.username, sip.ip, mac_address.mac_address)136 user.username, sip.ip, mac_address.mac_address)
@@ -201,9 +202,6 @@
201 request.user, ngi, requested_address, mac_address,202 request.user, ngi, requested_address, mac_address,
202 hostname=hostname)203 hostname=hostname)
203204
204 from maasserver.dns.config import dns_update_zones
205 dns_update_zones([ngi.nodegroup])
206
207 return sip205 return sip
208206
209 @operation(idempotent=False)207 @operation(idempotent=False)
@@ -235,7 +233,7 @@
235 for interface in linked_mac_address_interfaces233 for interface in linked_mac_address_interfaces
236 }234 }
237 remove_host_maps_failures = list(235 remove_host_maps_failures = list(
238 remove_host_maps(host_maps_to_remove))236 dhcp.remove_host_maps(host_maps_to_remove))
239 if len(remove_host_maps_failures) > 0:237 if len(remove_host_maps_failures) > 0:
240 # There's only going to be one failure, so raise that.238 # There's only going to be one failure, so raise that.
241 raise remove_host_maps_failures[0].value239 raise remove_host_maps_failures[0].value
242240
=== modified file 'src/maasserver/api/nodes.py'
--- src/maasserver/api/nodes.py 2015-05-28 09:38:40 +0000
+++ src/maasserver/api/nodes.py 2015-06-15 08:11:13 +0000
@@ -40,7 +40,6 @@
40 get_optional_param,40 get_optional_param,
41)41)
42from maasserver.clusterrpc.power_parameters import get_power_types42from maasserver.clusterrpc.power_parameters import get_power_types
43from maasserver.dns.config import dns_update_zones
44from maasserver.enum import (43from maasserver.enum import (
45 IPADDRESS_TYPE,44 IPADDRESS_TYPE,
46 NODE_PERMISSION,45 NODE_PERMISSION,
@@ -549,12 +548,7 @@
549548
550 sticky_ips = mac_address.claim_static_ips(549 sticky_ips = mac_address.claim_static_ips(
551 alloc_type=IPADDRESS_TYPE.STICKY,550 alloc_type=IPADDRESS_TYPE.STICKY,
552 requested_address=requested_address)551 requested_address=requested_address, update_host_maps=True)
553 claims = [
554 (static_ip.ip, mac_address.mac_address.get_raw())
555 for static_ip in sticky_ips]
556 node.update_host_maps(claims)
557 dns_update_zones([node.nodegroup])
558 maaslog.info(552 maaslog.info(
559 "%s: Sticky IP address(es) allocated: %s", node.hostname,553 "%s: Sticky IP address(es) allocated: %s", node.hostname,
560 ', '.join(allocation.ip for allocation in sticky_ips))554 ', '.join(allocation.ip for allocation in sticky_ips))
561555
=== modified file 'src/maasserver/api/tests/test_devices.py'
--- src/maasserver/api/tests/test_devices.py 2015-05-16 06:21:20 +0000
+++ src/maasserver/api/tests/test_devices.py 2015-06-15 08:11:13 +0000
@@ -21,7 +21,7 @@
2121
22from django.core.urlresolvers import reverse22from django.core.urlresolvers import reverse
23from django.db import transaction23from django.db import transaction
24from maasserver.api import devices as api_devices24from maasserver.dns import config as dns_config
25from maasserver.enum import (25from maasserver.enum import (
26 IPADDRESS_TYPE,26 IPADDRESS_TYPE,
27 NODE_STATUS,27 NODE_STATUS,
@@ -281,7 +281,7 @@
281 installable=False, parent=parent, mac=True, disable_ipv4=False,281 installable=False, parent=parent, mac=True, disable_ipv4=False,
282 owner=self.logged_in_user)282 owner=self.logged_in_user)
283 # Silence 'update_host_maps'.283 # Silence 'update_host_maps'.
284 self.patch(node_module, "update_host_maps")284 self.patch(Node.update_host_maps)
285 response = self.client.post(285 response = self.client.post(
286 get_device_uri(device), {'op': 'claim_sticky_ip_address'})286 get_device_uri(device), {'op': 'claim_sticky_ip_address'})
287 self.assertEqual(httplib.OK, response.status_code, response.content)287 self.assertEqual(httplib.OK, response.status_code, response.content)
@@ -299,8 +299,8 @@
299 device = factory.make_Node(299 device = factory.make_Node(
300 installable=False, parent=parent, mac=True, disable_ipv4=False,300 installable=False, parent=parent, mac=True, disable_ipv4=False,
301 owner=self.logged_in_user, nodegroup=parent.nodegroup)301 owner=self.logged_in_user, nodegroup=parent.nodegroup)
302 dns_update_zones = self.patch(api_devices, 'dns_update_zones')302 dns_update_zones = self.patch(dns_config.dns_update_zones)
303 update_host_maps = self.patch(node_module, "update_host_maps")303 update_host_maps = self.patch(Node.update_host_maps)
304 update_host_maps.return_value = [] # No failures.304 update_host_maps.return_value = [] # No failures.
305 response = self.client.post(305 response = self.client.post(
306 get_device_uri(device), {'op': 'claim_sticky_ip_address'})306 get_device_uri(device), {'op': 'claim_sticky_ip_address'})
@@ -327,7 +327,7 @@
327 device = factory.make_Node(327 device = factory.make_Node(
328 installable=False, parent=parent, mac=True, disable_ipv4=False,328 installable=False, parent=parent, mac=True, disable_ipv4=False,
329 owner=factory.make_User())329 owner=factory.make_User())
330 self.patch(node_module, "update_host_maps")330 self.patch(Node.update_host_maps)
331 response = self.client.post(331 response = self.client.post(
332 get_device_uri(device), {'op': 'claim_sticky_ip_address'})332 get_device_uri(device), {'op': 'claim_sticky_ip_address'})
333 self.assertEqual(httplib.FORBIDDEN, response.status_code)333 self.assertEqual(httplib.FORBIDDEN, response.status_code)
@@ -339,7 +339,7 @@
339 installable=False, mac=True, disable_ipv4=False,339 installable=False, mac=True, disable_ipv4=False,
340 owner=self.logged_in_user)340 owner=self.logged_in_user)
341 # Silence 'update_host_maps'.341 # Silence 'update_host_maps'.
342 self.patch(node_module, "update_host_maps")342 self.patch(Node.update_host_maps)
343 response = self.client.post(343 response = self.client.post(
344 get_device_uri(device),344 get_device_uri(device),
345 {345 {
@@ -364,7 +364,7 @@
364 owner=self.logged_in_user)364 owner=self.logged_in_user)
365 second_mac = factory.make_MACAddress(node=device)365 second_mac = factory.make_MACAddress(node=device)
366 # Silence 'update_host_maps'.366 # Silence 'update_host_maps'.
367 self.patch(node_module, "update_host_maps")367 self.patch(Node.update_host_maps)
368 response = self.client.post(368 response = self.client.post(
369 get_device_uri(device),369 get_device_uri(device),
370 {370 {
@@ -383,6 +383,8 @@
383 self.assertItemsEqual([second_mac], given_ip.macaddress_set.all())383 self.assertItemsEqual([second_mac], given_ip.macaddress_set.all())
384384
385 def test_creates_host_DHCP_and_DNS_mappings_with_given_ip(self):385 def test_creates_host_DHCP_and_DNS_mappings_with_given_ip(self):
386 dns_update_zones = self.patch(dns_config.dns_update_zones)
387 update_host_maps = self.patch(Node.update_host_maps)
386 # Create a nodegroup for which we manage DHCP.388 # Create a nodegroup for which we manage DHCP.
387 factory.make_NodeGroup(389 factory.make_NodeGroup(
388 status=NODEGROUP_STATUS.ENABLED,390 status=NODEGROUP_STATUS.ENABLED,
@@ -394,8 +396,6 @@
394 device = factory.make_Node(396 device = factory.make_Node(
395 installable=False, mac=True, disable_ipv4=False,397 installable=False, mac=True, disable_ipv4=False,
396 owner=self.logged_in_user)398 owner=self.logged_in_user)
397 dns_update_zones = self.patch(api_devices, 'dns_update_zones')
398 update_host_maps = self.patch(node_module, "update_host_maps")
399 update_host_maps.return_value = [] # No failures.399 update_host_maps.return_value = [] # No failures.
400 requested_address = factory.make_ip_address()400 requested_address = factory.make_ip_address()
401 response = self.client.post(401 response = self.client.post(
@@ -473,7 +473,7 @@
473 installable=False, mac=True, disable_ipv4=False,473 installable=False, mac=True, disable_ipv4=False,
474 owner=self.logged_in_user)474 owner=self.logged_in_user)
475 # Silence 'update_host_maps'.475 # Silence 'update_host_maps'.
476 self.patch(node_module, "update_host_maps")476 self.patch(Node.update_host_maps)
477 response = self.client.post(477 response = self.client.post(
478 get_device_uri(device),478 get_device_uri(device),
479 {479 {
@@ -494,8 +494,8 @@
494 installable=False, parent=parent, mac=True, disable_ipv4=False,494 installable=False, parent=parent, mac=True, disable_ipv4=False,
495 owner=self.logged_in_user)495 owner=self.logged_in_user)
496 # Silence 'update_host_maps' and 'remove_host_maps'496 # Silence 'update_host_maps' and 'remove_host_maps'
497 self.patch(node_module, "update_host_maps")497 self.patch(Node.update_host_maps)
498 self.patch(node_module, "remove_host_maps")498 self.patch(node_module, node_module.remove_host_maps.__name__)
499 response = self.client.post(499 response = self.client.post(
500 get_device_uri(device), {'op': 'claim_sticky_ip_address'})500 get_device_uri(device), {'op': 'claim_sticky_ip_address'})
501 self.assertEqual(httplib.OK, response.status_code, response.content)501 self.assertEqual(httplib.OK, response.status_code, response.content)
@@ -549,8 +549,8 @@
549 installable=False, mac_count=5, network=network,549 installable=False, mac_count=5, network=network,
550 disable_ipv4=False, owner=self.logged_in_user)550 disable_ipv4=False, owner=self.logged_in_user)
551 # Silence 'update_host_maps' and 'remove_host_maps'551 # Silence 'update_host_maps' and 'remove_host_maps'
552 self.patch(node_module, "update_host_maps")552 self.patch(Node.update_host_maps)
553 self.patch(node_module, "remove_host_maps")553 self.patch(node_module, node_module.remove_host_maps.__name__)
554 self.assertThat(MACAddress.objects.all(), HasLength(5))554 self.assertThat(MACAddress.objects.all(), HasLength(5))
555 for mac in MACAddress.objects.all():555 for mac in MACAddress.objects.all():
556 with transaction.atomic():556 with transaction.atomic():
@@ -569,8 +569,8 @@
569 installable=False, mac_count=2, network=network,569 installable=False, mac_count=2, network=network,
570 disable_ipv4=False, owner=self.logged_in_user)570 disable_ipv4=False, owner=self.logged_in_user)
571 # Silence 'update_host_maps' and 'remove_host_maps'571 # Silence 'update_host_maps' and 'remove_host_maps'
572 self.patch(node_module, "update_host_maps")572 self.patch(Node.update_host_maps)
573 self.patch(node_module, "remove_host_maps")573 self.patch(node_module, node_module.remove_host_maps.__name__)
574 self.assertThat(MACAddress.objects.all(), HasLength(2))574 self.assertThat(MACAddress.objects.all(), HasLength(2))
575 ips = []575 ips = []
576 for mac in MACAddress.objects.all():576 for mac in MACAddress.objects.all():
@@ -596,7 +596,7 @@
596 installable=False, parent=parent, mac=True, disable_ipv4=False,596 installable=False, parent=parent, mac=True, disable_ipv4=False,
597 owner=factory.make_User())597 owner=factory.make_User())
598 # Silence 'update_host_maps' and 'remove_host_maps'598 # Silence 'update_host_maps' and 'remove_host_maps'
599 self.patch(node_module, "update_host_maps")599 self.patch(Node.update_host_maps)
600 self.patch(node_module, "remove_host_maps")600 self.patch(node_module, "remove_host_maps")
601 with transaction.atomic():601 with transaction.atomic():
602 device.claim_static_ip_addresses(alloc_type=IPADDRESS_TYPE.STICKY)602 device.claim_static_ip_addresses(alloc_type=IPADDRESS_TYPE.STICKY)
603603
=== modified file 'src/maasserver/api/tests/test_ipaddresses.py'
--- src/maasserver/api/tests/test_ipaddresses.py 2015-05-16 06:21:20 +0000
+++ src/maasserver/api/tests/test_ipaddresses.py 2015-06-15 08:11:13 +0000
@@ -18,7 +18,7 @@
18import json18import json
1919
20from django.core.urlresolvers import reverse20from django.core.urlresolvers import reverse
21from maasserver.api import ip_addresses as ip_addresses_module21from maasserver.clusterrpc import dhcp as dhcp_module
22from maasserver.dns import config as dns_config_module22from maasserver.dns import config as dns_config_module
23from maasserver.enum import (23from maasserver.enum import (
24 IPADDRESS_TYPE,24 IPADDRESS_TYPE,
@@ -104,7 +104,7 @@
104 self.assertEqual(self.logged_in_user, staticipaddress.user)104 self.assertEqual(self.logged_in_user, staticipaddress.user)
105105
106 def test_POST_reserve_with_MAC_links_MAC_to_ip_address(self):106 def test_POST_reserve_with_MAC_links_MAC_to_ip_address(self):
107 update_host_maps = self.patch(ip_addresses_module, 'update_host_maps')107 update_host_maps = self.patch(dhcp_module, 'update_host_maps')
108 interface = self.make_interface()108 interface = self.make_interface()
109 net = interface.network109 net = interface.network
110 mac = factory.make_mac_address()110 mac = factory.make_mac_address()
@@ -124,7 +124,7 @@
124 {interface.nodegroup: {returned_address['ip']: mac}}))124 {interface.nodegroup: {returned_address['ip']: mac}}))
125125
126 def test_POST_reserve_with_MAC_returns_503_if_hostmap_update_fails(self):126 def test_POST_reserve_with_MAC_returns_503_if_hostmap_update_fails(self):
127 update_host_maps = self.patch(ip_addresses_module, 'update_host_maps')127 update_host_maps = self.patch(dhcp_module, 'update_host_maps')
128 # We a specific exception here because update_host_maps() will128 # We a specific exception here because update_host_maps() will
129 # fail with RPC-specific errors.129 # fail with RPC-specific errors.
130 update_host_maps.return_value = [130 update_host_maps.return_value = [
@@ -149,7 +149,7 @@
149 def test_POST_returns_CONFLICT_when_static_ip_for_MAC_already_exists(self):149 def test_POST_returns_CONFLICT_when_static_ip_for_MAC_already_exists(self):
150 interface = self.make_interface()150 interface = self.make_interface()
151 mac = factory.make_MACAddress(cluster_interface=interface)151 mac = factory.make_MACAddress(cluster_interface=interface)
152 mac.claim_static_ips()152 mac.claim_static_ips(update_host_maps=False)
153 net = interface.network153 net = interface.network
154154
155 response = self.post_reservation_request(net=net, mac=mac.mac_address)155 response = self.post_reservation_request(net=net, mac=mac.mac_address)
@@ -161,7 +161,7 @@
161 Equals(1))161 Equals(1))
162162
163 def test_POST_allows_claiming_of_new_static_ips_for_existing_MAC(self):163 def test_POST_allows_claiming_of_new_static_ips_for_existing_MAC(self):
164 self.patch(ip_addresses_module, 'update_host_maps')164 self.patch(dhcp_module, 'update_host_maps')
165165
166 interface = self.make_interface()166 interface = self.make_interface()
167 net = interface.network167 net = interface.network
@@ -363,36 +363,40 @@
363 self.assertIsNone(reload_object(ipaddress))363 self.assertIsNone(reload_object(ipaddress))
364364
365 def test_POST_release_deletes_floating_MAC_address(self):365 def test_POST_release_deletes_floating_MAC_address(self):
366 self.patch(ip_addresses_module, 'remove_host_maps')366 self.patch(dhcp_module, dhcp_module.remove_host_maps.__name__)
367367
368 interface = self.make_interface()368 interface = self.make_interface()
369 floating_mac = factory.make_MACAddress(cluster_interface=interface)369 floating_mac = factory.make_MACAddress(cluster_interface=interface)
370 [ipaddress] = floating_mac.claim_static_ips(370 [ipaddress] = floating_mac.claim_static_ips(
371 alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user)371 alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user,
372 update_host_maps=False)
372373
373 self.post_release_request(ipaddress.ip)374 self.post_release_request(ipaddress.ip)
374 self.assertIsNone(reload_object(floating_mac))375 self.assertIsNone(reload_object(floating_mac))
375376
376 def test_POST_release_does_not_delete_MACs_linked_to_nodes(self):377 def test_POST_release_does_not_delete_MACs_linked_to_nodes(self):
377 self.patch(ip_addresses_module, 'remove_host_maps')378 self.patch(dhcp_module, dhcp_module.remove_host_maps.__name__)
378379
379 interface = self.make_interface()380 interface = self.make_interface()
380 node = factory.make_Node(nodegroup=interface.nodegroup)381 node = factory.make_Node(nodegroup=interface.nodegroup)
381 attached_mac = factory.make_MACAddress(382 attached_mac = factory.make_MACAddress(
382 node=node, cluster_interface=interface)383 node=node, cluster_interface=interface)
383 [ipaddress] = attached_mac.claim_static_ips(384 [ipaddress] = attached_mac.claim_static_ips(
384 alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user)385 alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user,
386 update_host_maps=False)
385387
386 self.post_release_request(ipaddress.ip)388 self.post_release_request(ipaddress.ip)
387 self.assertEqual(attached_mac, reload_object(attached_mac))389 self.assertEqual(attached_mac, reload_object(attached_mac))
388390
389 def test_POST_release_updates_DNS_and_DHCP(self):391 def test_POST_release_updates_DNS_and_DHCP(self):
390 remove_host_maps = self.patch(ip_addresses_module, 'remove_host_maps')392 remove_host_maps = self.patch(
393 dhcp_module, dhcp_module.remove_host_maps.__name__)
391394
392 interface = self.make_interface()395 interface = self.make_interface()
393 floating_mac = factory.make_MACAddress(cluster_interface=interface)396 floating_mac = factory.make_MACAddress(cluster_interface=interface)
394 [ipaddress] = floating_mac.claim_static_ips(397 [ipaddress] = floating_mac.claim_static_ips(
395 alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user)398 alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user,
399 update_host_maps=False)
396400
397 self.post_release_request(ipaddress.ip)401 self.post_release_request(ipaddress.ip)
398 self.expectThat(402 self.expectThat(
@@ -400,7 +404,8 @@
400 {interface.nodegroup: [ipaddress.ip]}))404 {interface.nodegroup: [ipaddress.ip]}))
401405
402 def test_POST_release_raises_503_if_removing_host_maps_errors(self):406 def test_POST_release_raises_503_if_removing_host_maps_errors(self):
403 remove_host_maps = self.patch(ip_addresses_module, 'remove_host_maps')407 remove_host_maps = self.patch(
408 dhcp_module, dhcp_module.remove_host_maps.__name__)
404 # Failures in remove_host_maps() will be RPC-related exceptions,409 # Failures in remove_host_maps() will be RPC-related exceptions,
405 # so we use one of those explicitly.410 # so we use one of those explicitly.
406 remove_host_maps.return_value = [411 remove_host_maps.return_value = [
@@ -412,7 +417,8 @@
412 interface = self.make_interface()417 interface = self.make_interface()
413 floating_mac = factory.make_MACAddress(cluster_interface=interface)418 floating_mac = factory.make_MACAddress(cluster_interface=interface)
414 [ipaddress] = floating_mac.claim_static_ips(419 [ipaddress] = floating_mac.claim_static_ips(
415 alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user)420 alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user,
421 update_host_maps=False)
416422
417 response = self.post_release_request(ipaddress.ip)423 response = self.post_release_request(ipaddress.ip)
418 self.expectThat(424 self.expectThat(
419425
=== modified file 'src/maasserver/api/tests/test_node.py'
--- src/maasserver/api/tests/test_node.py 2015-05-28 13:26:45 +0000
+++ src/maasserver/api/tests/test_node.py 2015-06-15 08:11:13 +0000
@@ -24,7 +24,7 @@
24from django.core.urlresolvers import reverse24from django.core.urlresolvers import reverse
25from django.db import transaction25from django.db import transaction
26from maasserver import forms26from maasserver import forms
27from maasserver.api import nodes as api_nodes27from maasserver.dns import config as dns_config
28from maasserver.enum import (28from maasserver.enum import (
29 IPADDRESS_TYPE,29 IPADDRESS_TYPE,
30 NODE_BOOT,30 NODE_BOOT,
@@ -1195,9 +1195,9 @@
1195 self.become_admin()1195 self.become_admin()
1196 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface()1196 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface()
1197 # Silence 'update_host_maps'.1197 # Silence 'update_host_maps'.
1198 self.patch(node_module, "update_host_maps")1198 self.patch(Node.update_host_maps)
1199 [existing_ip] = node.get_primary_mac().claim_static_ips(1199 [existing_ip] = node.get_primary_mac().claim_static_ips(
1200 alloc_type=IPADDRESS_TYPE.STICKY)1200 alloc_type=IPADDRESS_TYPE.STICKY, update_host_maps=False)
1201 response = self.client.post(1201 response = self.client.post(
1202 self.get_node_uri(node), {'op': 'claim_sticky_ip_address'})1202 self.get_node_uri(node), {'op': 'claim_sticky_ip_address'})
1203 self.assertEqual(httplib.OK, response.status_code, response.content)1203 self.assertEqual(httplib.OK, response.status_code, response.content)
@@ -1214,7 +1214,8 @@
1214 random_alloc_type = factory.pick_enum(1214 random_alloc_type = factory.pick_enum(
1215 IPADDRESS_TYPE,1215 IPADDRESS_TYPE,
1216 but_not=[IPADDRESS_TYPE.STICKY, IPADDRESS_TYPE.USER_RESERVED])1216 but_not=[IPADDRESS_TYPE.STICKY, IPADDRESS_TYPE.USER_RESERVED])
1217 node.get_primary_mac().claim_static_ips(alloc_type=random_alloc_type)1217 node.get_primary_mac().claim_static_ips(
1218 alloc_type=random_alloc_type, update_host_maps=False)
1218 response = self.client.post(1219 response = self.client.post(
1219 self.get_node_uri(node), {'op': 'claim_sticky_ip_address'})1220 self.get_node_uri(node), {'op': 'claim_sticky_ip_address'})
1220 self.assertEqual(1221 self.assertEqual(
@@ -1265,7 +1266,7 @@
1265 def test_claim_ip_address_creates_host_DHCP_and_DNS_mappings(self):1266 def test_claim_ip_address_creates_host_DHCP_and_DNS_mappings(self):
1266 self.become_admin()1267 self.become_admin()
1267 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface()1268 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface()
1268 dns_update_zones = self.patch(api_nodes, 'dns_update_zones')1269 dns_update_zones = self.patch(dns_config.dns_update_zones)
1269 update_host_maps = self.patch(node_module, "update_host_maps")1270 update_host_maps = self.patch(node_module, "update_host_maps")
1270 update_host_maps.return_value = [] # No failures.1271 update_host_maps.return_value = [] # No failures.
1271 response = self.client.post(1272 response = self.client.post(
@@ -1371,7 +1372,7 @@
1371 requested_address = IPAddress(ngi.static_ip_range_low) + 11372 requested_address = IPAddress(ngi.static_ip_range_low) + 1
1372 requested_address = requested_address.format()1373 requested_address = requested_address.format()
1373 other_node.get_primary_mac().claim_static_ips(1374 other_node.get_primary_mac().claim_static_ips(
1374 requested_address=requested_address)1375 requested_address=requested_address, update_host_maps=False)
13751376
1376 # Use the API to try to duplicate the same IP on the other node.1377 # Use the API to try to duplicate the same IP on the other node.
1377 response = self.client.post(1378 response = self.client.post(
13781379
=== modified file 'src/maasserver/models/macaddress.py'
--- src/maasserver/models/macaddress.py 2015-05-13 11:33:20 +0000
+++ src/maasserver/models/macaddress.py 2015-06-15 08:11:13 +0000
@@ -43,6 +43,7 @@
43from maasserver.models.macipaddresslink import MACStaticIPAddressLink43from maasserver.models.macipaddresslink import MACStaticIPAddressLink
44from maasserver.models.managers import BulkManager44from maasserver.models.managers import BulkManager
45from maasserver.models.network import Network45from maasserver.models.network import Network
46from maasserver.models.nodegroup import NodeGroup
46from maasserver.models.nodegroupinterface import NodeGroupInterface47from maasserver.models.nodegroupinterface import NodeGroupInterface
47from maasserver.models.staticipaddress import StaticIPAddress48from maasserver.models.staticipaddress import StaticIPAddress
48from maasserver.models.timestampedmodel import TimestampedModel49from maasserver.models.timestampedmodel import TimestampedModel
@@ -267,24 +268,48 @@
267 return self.node.parent.get_pxe_mac().cluster_interface268 return self.node.parent.get_pxe_mac().cluster_interface
268 return None269 return None
269270
270 def claim_static_ips(self, alloc_type=IPADDRESS_TYPE.AUTO,271 def _get_attached_clusters_with_static_ranges(self):
271 requested_address=None, user=None):272 """Returns a list of cluster interfaces attached to this MAC address,
273 where each cluster interface has a defined static range.
274 """
275 return [
276 interface
277 for interface in self.get_cluster_interfaces()
278 if interface.get_static_ip_range()
279 ]
280
281 def _get_hostname_log_prefix(self):
282 """Returns a string that represents the hostname for this MAC address,
283 suitable for prepending to a log statement.
284 """
285 if self.node is not None:
286 hostname_string = "%s: " % self.node.hostname
287 else:
288 hostname_string = ""
289 return hostname_string
290
291 def claim_static_ips(
292 self, alloc_type=IPADDRESS_TYPE.AUTO, requested_address=None,
293 fabric=None, user=None, update_host_maps=True):
272 """Assign static IP addresses to this MAC.294 """Assign static IP addresses to this MAC.
273295
274 Allocates one address per managed cluster interface connected to this296 Allocates one address per managed cluster interface connected to this
275 MAC. Typically this will be either just one IPv4 address, or an IPv4297 MAC. Typically this will be either just one IPv4 address, or an IPv4
276 address and an IPv6 address.298 address and an IPv6 address.
299 Calls update_host_maps() on the related Node in order to update
277300
278 It is the caller's responsibility to update the DHCP server.301 any DHCP mappings.
279302
280 :param alloc_type: See :class:`StaticIPAddress`.alloc_type.303 :param alloc_type: See :class:`StaticIPAddress`.alloc_type.
281 This parameter musn't be IPADDRESS_TYPE.USER_RESERVED.304 This parameter musn't be IPADDRESS_TYPE.USER_RESERVED.
282 :param requested_address: Optional IP address to claim. Must be in305 :param requested_address: Optional IP address to claim. Must be in
283 the range defined on some cluster interface to which this306 the range defined on some cluster interface to which this
284 MACAddress is related. If given, no allocations will be made on307 MACAddress is related. If given, no allocations will be made on
285 any other cluster interfaces the MAC may be connected to.308 any other cluster interfaces the MAC may be connected to.
286 :param user: Optional User who will be given ownership of any309 :param user: Optional User who will be given ownership of any
287 `StaticIPAddress`es claimed.310 `StaticIPAddress`es claimed.
311 :param update_host_maps: If True, will update any relevant DHCP
312 mappings in addition to allocating the address.
288 :return: A list of :class:`StaticIPAddress`. Returns empty if313 :return: A list of :class:`StaticIPAddress`. Returns empty if
289 the cluster_interface is not yet known, or the314 the cluster_interface is not yet known, or the
290 static_ip_range_low/high values values are not set on the315 static_ip_range_low/high values values are not set on the
@@ -297,7 +322,12 @@
297 the cluster interface's defined range.322 the cluster interface's defined range.
298 :raises: StaticIPAddressUnavailable if the requested_address is already323 :raises: StaticIPAddressUnavailable if the requested_address is already
299 allocated.324 allocated.
325 :raises: StaticIPAddressForbidden if the address occurs within
326 an existing dynamic range within the specified fabric.
300 """327 """
328 if fabric is not None:
329 raise NotImplementedError("Fabrics are not yet supported.")
330
301 # This method depends on a database isolation level of SERIALIZABLE331 # This method depends on a database isolation level of SERIALIZABLE
302 # (or perhaps REPEATABLE READ) to avoid race conditions.332 # (or perhaps REPEATABLE READ) to avoid race conditions.
303333
@@ -307,19 +337,12 @@
307 # different representations for "none" values in IP addresses.337 # different representations for "none" values in IP addresses.
308 if self.get_cluster_interface() is None:338 if self.get_cluster_interface() is None:
309 # No known cluster interface. Nothing we can do.339 # No known cluster interface. Nothing we can do.
310 if self.node is not None:340 hostname_string = self._get_hostname_log_prefix()
311 hostname_string = "%s: " % self.node.hostname
312 else:
313 hostname_string = ""
314 maaslog.error(341 maaslog.error(
315 "%s tried to allocate an IP to MAC %s but its cluster "342 "%sTried to allocate an IP to MAC %s, but its cluster "
316 "interface is not known", hostname_string, self)343 "interface is not known", hostname_string, self)
317 return []344 return []
318 cluster_interfaces = [345 cluster_interfaces = self._get_attached_clusters_with_static_ranges()
319 interface
320 for interface in self.get_cluster_interfaces()
321 if interface.get_static_ip_range()
322 ]
323 if len(cluster_interfaces) == 0:346 if len(cluster_interfaces) == 0:
324 # There were cluster interfaces, but none of them had a static347 # There were cluster interfaces, but none of them had a static
325 # range. Can't allocate anything.348 # range. Can't allocate anything.
@@ -328,6 +351,10 @@
328 if requested_address is not None:351 if requested_address is not None:
329 # A specific IP address was requested. We restrict our attention352 # A specific IP address was requested. We restrict our attention
330 # to the cluster interface that is responsible for that address.353 # to the cluster interface that is responsible for that address.
354 # In addition, claiming addresses inside a dynamic range on the
355 # requested fabric is not allowed.
356 self._raise_if_address_inside_dynamic_range(
357 requested_address, fabric)
331 cluster_interface = find_cluster_interface_responsible_for_ip(358 cluster_interface = find_cluster_interface_responsible_for_ip(
332 cluster_interfaces, IPAddress(requested_address))359 cluster_interfaces, IPAddress(requested_address))
333 if cluster_interface is None:360 if cluster_interface is None:
@@ -338,25 +365,34 @@
338365
339 allocations = self._map_allocated_addresses(cluster_interfaces)366 allocations = self._map_allocated_addresses(cluster_interfaces)
340367
341 if None not in allocations.values():368 # Check if we already have a full complement of static IP addresses
342 # We already have a full complement of static IP addresses369 # allocated, none of which are the same type.
343 # allocated. Check for a clash.370 if (None not in allocations.values() and alloc_type not in
344 types = [sip.alloc_type for sip in allocations.values()]371 [a.alloc_type for a in allocations.values()]):
345 if alloc_type not in types:372 raise StaticIPAddressTypeClash(
346 # None of the prior allocations are for the same type that's373 "MAC address %s already has IP addresses of different "
347 # being requested now. This is a complete clash.374 "types than the ones requested." % self)
348 raise StaticIPAddressTypeClash(
349 "MAC address %s already has IP adresses of different "
350 "types than the ones requested." % self)
351375
376 new_allocations = []
352 # Allocate IP addresses on all relevant cluster interfaces where this377 # Allocate IP addresses on all relevant cluster interfaces where this
353 # MAC does not have any address allocated yet.378 # MAC does not have any address allocated yet.
354 for interface in cluster_interfaces:379 for interface in cluster_interfaces:
355 if allocations[interface] is None:380 if allocations[interface] is None:
356 # No IP address yet on this cluster interface. Get one.381 # No IP address yet on this cluster interface. Get one.
357 allocations[interface] = self._allocate_static_address(382 static_ip = self._allocate_static_address(
358 interface, alloc_type, requested_address, user=user)383 interface, alloc_type, requested_address, user=user)
384 allocations[interface] = static_ip
385 mac_address = MAC(self.mac_address)
386 new_allocations.append(
387 (static_ip.ip, mac_address.get_raw()))
359388
389 # Note: the previous behavior of the product (MAAS < 1.8) was to
390 # update host maps with *every* address, not just changed addresses.
391 # This should only impact separately-claimed IPv6 and IPv4 addresses.
392 if update_host_maps:
393 if self.node is not None:
394 self.node.update_host_maps(new_allocations)
395 self.update_related_dns_zones()
360 # We now have a static IP allocated to each of our cluster interfaces.396 # We now have a static IP allocated to each of our cluster interfaces.
361 # Ignore the clashes. Return the ones that have the right type: those397 # Ignore the clashes. Return the ones that have the right type: those
362 # are either matching pre-existing allocations or fresh ones.398 # are either matching pre-existing allocations or fresh ones.
@@ -366,14 +402,69 @@
366 if sip.alloc_type == alloc_type402 if sip.alloc_type == alloc_type
367 ]403 ]
368404
369 def set_static_ip(self, requested_address, user):405 def _get_device_cluster_or_default(self):
406 """Returns a cluster interface for this MAC, first by checking for a
407 direct link, then by checking the parent node,
408 (via get_cluster_interface()) and finally by getting the default,
409 if all else fails.
410 """
411 cluster_interface = self.get_cluster_interface()
412 if cluster_interface is not None:
413 return cluster_interface.nodegroup
414 else:
415 return NodeGroup.objects.ensure_master()
416
417 def update_related_dns_zones(self):
418 """Updates DNS for the cluster related to this MAC."""
419 # Prevent circular imports
420 from maasserver.dns import config as dns_config
421 dns_config.dns_update_zones([self._get_device_cluster_or_default()])
422
423 def _raise_if_address_inside_dynamic_range(
424 self, requested_address, fabric=None):
425 """
426 Checks if the specified IP address, inside the specified fabric,
427 is inside a MAAS-managed dynamic range.
428
429 :raises: StaticIPAddressForbidden if the address occurs within
430 an existing dynamic range within the specified fabric.
431 """
432 if fabric is not None:
433 raise NotImplementedError("Fabrics are not yet supported.")
434
435 requested_address_ip = IPAddress(requested_address)
436 for interface in NodeGroupInterface.objects.all():
437 if interface.is_managed:
438 dynamic_range = interface.get_dynamic_ip_range()
439 if requested_address_ip in dynamic_range:
440 raise StaticIPAddressForbidden(
441 "Requested IP address %s is in a dynamic range." %
442 requested_address)
443
444 def _get_dhcp_managed_clusters(self, fabric=None):
445 """Returns the DHCP-managed clusters relevant to the specified fabric.
446
447 :param fabric: The fabric whose DHCP-managed clusters to update.
448 """
449 if fabric is not None:
450 raise NotImplementedError("Fabrics are not yet supported.")
451
452 return [
453 cluster
454 for cluster in NodeGroup.objects.all()
455 if cluster.manages_dhcp()
456 ]
457
458 def set_static_ip(
459 self, requested_address, user, fabric=None, update_host_maps=True):
370 """Assign a static (sticky) IP address to this MAC.460 """Assign a static (sticky) IP address to this MAC.
371461
372 This is meant to be called on a device's MAC address: the IP address462 This is meant to be called on a device's MAC address: the IP address
373 can be anything. Only if the MAC is linked to a network will this463 can be anything. Only if the MAC is linked to a network will this
374 method enforce that the IP address if part of the referenced network.464 method enforce that the IP address if part of the referenced network.
375465
376 It is the caller's responsibility to update the DHCP server.466 Calls update_host_maps() on the related Node in order to update
467 any DHCP mappings.
377468
378 :param requested_address: IP address to claim. Must not be in469 :param requested_address: IP address to claim. Must not be in
379 the dynamic range of any cluster interface.470 the dynamic range of any cluster interface.
@@ -390,6 +481,9 @@
390 :raises: StaticIPAddressUnavailable if the requested_address is already481 :raises: StaticIPAddressUnavailable if the requested_address is already
391 allocated.482 allocated.
392 """483 """
484 if fabric is not None:
485 raise NotImplementedError("Fabrics are not yet supported.")
486
393 # If this MAC is linked to a cluster interface, make sure the487 # If this MAC is linked to a cluster interface, make sure the
394 # requested_address is part of the cluster interface's network.488 # requested_address is part of the cluster interface's network.
395 cluster_interface = self.get_cluster_interface()489 cluster_interface = self.get_cluster_interface()
@@ -402,14 +496,7 @@
402496
403 # Raise a StaticIPAddressForbidden exception if the requested_address497 # Raise a StaticIPAddressForbidden exception if the requested_address
404 # is in a dynamic range.498 # is in a dynamic range.
405 requested_address_ip = IPAddress(requested_address)499 self._raise_if_address_inside_dynamic_range(requested_address, fabric)
406 for interface in NodeGroupInterface.objects.all():
407 if interface.is_managed:
408 dynamic_range = interface.get_dynamic_ip_range()
409 if requested_address_ip in dynamic_range:
410 raise StaticIPAddressForbidden(
411 "Requested IP address %s is in a dynamic range." %
412 requested_address)
413500
414 # Allocate IP if it isn't allocated already.501 # Allocate IP if it isn't allocated already.
415 static_ip, created = StaticIPAddress.objects.get_or_create(502 static_ip, created = StaticIPAddress.objects.get_or_create(
@@ -434,4 +521,20 @@
434 "Requested IP address %s is already allocated "521 "Requested IP address %s is already allocated "
435 "to a different MAC address." %522 "to a different MAC address." %
436 requested_address)523 requested_address)
524
525 if update_host_maps:
526 # XXX:fabric We need to restrict this to cluster interfaces in the
527 # appropriate fabric!
528 if cluster_interface is not None:
529 relevant_clusters = [cluster_interface.nodegroup]
530 else:
531 relevant_clusters = self._get_dhcp_managed_clusters(fabric)
532
533 mac_address = MAC(self.mac_address)
534 ip_mapping = [(static_ip.ip, mac_address.get_raw())]
535
536 self.node.update_host_maps(
537 ip_mapping, nodegroups=relevant_clusters)
538 self.update_related_dns_zones()
539
437 return static_ip540 return static_ip
438541
=== modified file 'src/maasserver/models/macipaddresslink.py'
--- src/maasserver/models/macipaddresslink.py 2015-05-07 18:14:38 +0000
+++ src/maasserver/models/macipaddresslink.py 2015-06-15 08:11:13 +0000
@@ -33,6 +33,8 @@
3333
34class MACStaticIPAddressLink(CleanSave, TimestampedModel):34class MACStaticIPAddressLink(CleanSave, TimestampedModel):
3535
36 # XXX:fabric IP addresses are unique within each fabric, so this
37 # code needs to be updated.
36 class Meta(DefaultMeta):38 class Meta(DefaultMeta):
37 unique_together = ('ip_address', 'mac_address')39 unique_together = ('ip_address', 'mac_address')
3840
3941
=== modified file 'src/maasserver/models/node.py'
--- src/maasserver/models/node.py 2015-05-22 15:03:29 +0000
+++ src/maasserver/models/node.py 2015-06-15 08:11:13 +0000
@@ -1742,7 +1742,7 @@
17421742
1743 def claim_static_ip_addresses(1743 def claim_static_ip_addresses(
1744 self, mac=None, alloc_type=IPADDRESS_TYPE.AUTO,1744 self, mac=None, alloc_type=IPADDRESS_TYPE.AUTO,
1745 requested_address=None):1745 requested_address=None, update_host_maps=True):
1746 """Assign static IPs to a node's MAC.1746 """Assign static IPs to a node's MAC.
1747 If no MAC is specified, defaults to its PXE MAC.1747 If no MAC is specified, defaults to its PXE MAC.
17481748
@@ -1759,7 +1759,6 @@
1759 :raises: `StaticIPAddressUnavailable` if the supplied1759 :raises: `StaticIPAddressUnavailable` if the supplied
1760 requested_address is already in use.1760 requested_address is already in use.
1761 """1761 """
1762
1763 if mac is None:1762 if mac is None:
1764 mac = self.get_pxe_mac()1763 mac = self.get_pxe_mac()
1765 if mac is None:1764 if mac is None:
@@ -1767,7 +1766,8 @@
17671766
1768 try:1767 try:
1769 static_ips = mac.claim_static_ips(1768 static_ips = mac.claim_static_ips(
1770 alloc_type=alloc_type, requested_address=requested_address)1769 alloc_type=alloc_type, requested_address=requested_address,
1770 update_host_maps=update_host_maps)
1771 except StaticIPAddressTypeClash:1771 except StaticIPAddressTypeClash:
1772 # There's already a non-AUTO IP.1772 # There's already a non-AUTO IP.
1773 return []1773 return []
@@ -1776,31 +1776,49 @@
1776 # because it's all-or-nothing (hence the atomic context).1776 # because it's all-or-nothing (hence the atomic context).
1777 return [(static_ip.ip, unicode(mac)) for static_ip in static_ips]1777 return [(static_ip.ip, unicode(mac)) for static_ip in static_ips]
17781778
1779 def update_host_maps(self, claims, nodegroups=None):1779 @staticmethod
1780 def update_nodegroup_host_maps(nodegroups, claims):
1780 """Update host maps for the given MAC->IP mappings.1781 """Update host maps for the given MAC->IP mappings.
17811782
1782 If a nodegroup list is given, update all of them. If not, only1783 If a nodegroup list is given, update all of them. If not, only
1783 update the nodegroup of the node.1784 update the nodegroup of the node.
1784 """1785 """
1785 static_mappings = defaultdict(dict)1786 static_mappings = defaultdict(dict)
1786 if nodegroups is None:1787 for nodegroup in nodegroups:
1787 static_mappings[self.nodegroup].update(claims)1788 static_mappings[nodegroup].update(claims)
1788 else:
1789 for nodegroup in nodegroups:
1790 static_mappings[nodegroup].update(claims)
1791 update_host_maps_failures = list(1789 update_host_maps_failures = list(
1792 update_host_maps(static_mappings))1790 update_host_maps(static_mappings))
1793 if len(update_host_maps_failures) != 0:1791 num_failures = len(update_host_maps_failures)
1792 if num_failures != 0:
1794 # We've hit an error, so release any IPs we've claimed1793 # We've hit an error, so release any IPs we've claimed
1795 # and then raise the error for the call site to1794 # and then raise the error for the call site to
1796 # handle.1795 # handle.
1797 StaticIPAddress.objects.deallocate_by_node(self)1796 # StaticIPAddress.objects.deallocate_by_node(self)
1797 # StaticIPAddress.objects.deallocate_by_node()
1798 for claim in claims:
1799 StaticIPAddress.objects.get(ip=claim[0]).deallocate()
1800
1798 # We know there's only one error because we only1801 # We know there's only one error because we only
1799 # sent one mapping to update_host_maps(), so we1802 # sent one mapping to update_host_maps(), so we
1800 # extract the exception from the Failure and raise1803 # extract the exception from the Failure and raise
1801 # it.1804 # it.
1802 raise update_host_maps_failures[0].raiseException()1805 raise update_host_maps_failures[0].raiseException()
18031806
1807 def update_host_maps(self, claims, nodegroups=None):
1808 """Update host maps for the given MAC->IP mappings.
1809
1810 If a nodegroup list is given, update all of them. If not, only
1811 update the nodegroup of the node.
1812 """
1813
1814 # For some reason, we can call this method on a Node, but the intent
1815 # is to update nodegroups not on this node. That's why it's not
1816 # an "append" here.
1817 if nodegroups is None:
1818 nodegroups = [self.nodegroup]
1819
1820 self.update_nodegroup_host_maps(nodegroups, claims)
1821
1804 def deallocate_static_ip_addresses(1822 def deallocate_static_ip_addresses(
1805 self, alloc_type=IPADDRESS_TYPE.AUTO, ip=None):1823 self, alloc_type=IPADDRESS_TYPE.AUTO, ip=None):
1806 """Release the `StaticIPAddress` that is assigned to this node and1824 """Release the `StaticIPAddress` that is assigned to this node and
@@ -1813,9 +1831,10 @@
1813 self, alloc_type=alloc_type, ip=ip)1831 self, alloc_type=alloc_type, ip=ip)
18141832
1815 if len(deallocated_ips) > 0:1833 if len(deallocated_ips) > 0:
1834 # Prevent circular imports
1835 from maasserver.dns import config as dns_config
1816 self.delete_host_maps(deallocated_ips)1836 self.delete_host_maps(deallocated_ips)
1817 from maasserver.dns.config import dns_update_zones1837 dns_config.dns_update_zones([self.nodegroup])
1818 dns_update_zones([self.nodegroup])
18191838
1820 return deallocated_ips1839 return deallocated_ips
18211840
@@ -1916,7 +1935,7 @@
1916 return False1935 return False
19171936
1918 @transactional1937 @transactional
1919 def start(self, by_user, user_data=None):1938 def start(self, by_user, user_data=None, update_host_maps=True):
1920 """Request on given user's behalf that the node be started up.1939 """Request on given user's behalf that the node be started up.
19211940
1922 :param by_user: Requesting user.1941 :param by_user: Requesting user.
@@ -1927,7 +1946,7 @@
1927 :type user_data: unicode1946 :type user_data: unicode
19281947
1929 :raise StaticIPAddressExhaustion: if there are not enough IP addresses1948 :raise StaticIPAddressExhaustion: if there are not enough IP addresses
1930 left in the static range for this node toget all the addresses it1949 left in the static range for this node to get all the addresses it
1931 needs.1950 needs.
1932 :raise PermissionDenied: If `by_user` does not have permission to1951 :raise PermissionDenied: If `by_user` does not have permission to
1933 start this node.1952 start this node.
@@ -1941,7 +1960,6 @@
1941 """1960 """
1942 # Avoid circular imports.1961 # Avoid circular imports.
1943 from metadataserver.models import NodeUserData1962 from metadataserver.models import NodeUserData
1944 from maasserver.dns.config import dns_update_zones
19451963
1946 if not by_user.has_perm(NODE_PERMISSION.EDIT, self):1964 if not by_user.has_perm(NODE_PERMISSION.EDIT, self):
1947 # You can't start a node you don't own unless you're an admin.1965 # You can't start a node you don't own unless you're an admin.
@@ -1954,11 +1972,13 @@
19541972
1955 # Claim static IP addresses for the node if it's ALLOCATED.1973 # Claim static IP addresses for the node if it's ALLOCATED.
1956 if self.status == NODE_STATUS.ALLOCATED:1974 if self.status == NODE_STATUS.ALLOCATED:
1957 claims = self.claim_static_ip_addresses()1975
1958 # If the PXE mac is on a managed interface then we can ask1976 # Don't update host maps if we're not on a managed interface.
1959 # the cluster to generate the DHCP host map(s).1977 if not self.is_pxe_mac_on_managed_interface() and update_host_maps:
1960 if self.is_pxe_mac_on_managed_interface():1978 update_host_maps = False
1961 self.update_host_maps(claims)1979
1980 self.claim_static_ip_addresses(
1981 update_host_maps=update_host_maps)
19621982
1963 if self.status == NODE_STATUS.ALLOCATED:1983 if self.status == NODE_STATUS.ALLOCATED:
1964 transition_monitor = (1984 transition_monitor = (
@@ -1969,9 +1989,6 @@
1969 else:1989 else:
1970 transition_monitor = None1990 transition_monitor = None
19711991
1972 # Update the DNS zone with the new static IP info as necessary.
1973 dns_update_zones(self.nodegroup)
1974
1975 power_info = self.get_effective_power_info()1992 power_info = self.get_effective_power_info()
1976 if not power_info.can_be_started:1993 if not power_info.can_be_started:
1977 # The node can't be powered on by MAAS, so return early.1994 # The node can't be powered on by MAAS, so return early.
19781995
=== modified file 'src/maasserver/models/signals/dns.py'
--- src/maasserver/models/signals/dns.py 2015-06-10 11:24:44 +0000
+++ src/maasserver/models/signals/dns.py 2015-06-15 08:11:13 +0000
@@ -81,8 +81,8 @@
81def dns_post_delete_Node(sender, instance, **kwargs):81def dns_post_delete_Node(sender, instance, **kwargs):
82 """When a Node is deleted, update the Node's zone file."""82 """When a Node is deleted, update the Node's zone file."""
83 try:83 try:
84 from maasserver.dns.config import dns_update_zones84 from maasserver.dns import config as dns_config
85 dns_update_zones(instance.nodegroup)85 dns_config.dns_update_zones(instance.nodegroup)
86 except NodeGroup.DoesNotExist:86 except NodeGroup.DoesNotExist:
87 # If this Node is being deleted because the whole NodeGroup87 # If this Node is being deleted because the whole NodeGroup
88 # has been deleted, no need to update the zone file because88 # has been deleted, no need to update the zone file because
@@ -92,29 +92,29 @@
9292
93def dns_post_edit_hostname_Node(instance, old_values, **kwargs):93def dns_post_edit_hostname_Node(instance, old_values, **kwargs):
94 """When a Node has been flagged, update the related zone."""94 """When a Node has been flagged, update the related zone."""
95 from maasserver.dns.config import dns_update_zones95 from maasserver.dns import config as dns_config
96 dns_update_zones(instance.nodegroup)96 dns_config.dns_update_zones(instance.nodegroup)
9797
9898
99connect_to_field_change(dns_post_edit_hostname_Node, Node, ['hostname'])99connect_to_field_change(dns_post_edit_hostname_Node, Node, ['hostname'])
100100
101101
102def dns_setting_changed(sender, instance, created, **kwargs):102def dns_setting_changed(sender, instance, created, **kwargs):
103 from maasserver.dns.config import dns_update_all_zones103 from maasserver.dns import config as dns_config
104 dns_update_all_zones()104 dns_config.dns_update_all_zones()
105105
106106
107@receiver(post_save, sender=Network)107@receiver(post_save, sender=Network)
108def dns_post_save_Network(sender, instance, **kwargs):108def dns_post_save_Network(sender, instance, **kwargs):
109 """When a network is added/changed, put it in the DNS trusted networks."""109 """When a network is added/changed, put it in the DNS trusted networks."""
110 from maasserver.dns.config import dns_update_all_zones110 from maasserver.dns import config as dns_config
111 dns_update_all_zones()111 dns_config.dns_update_all_zones()
112112
113113
114@receiver(post_delete, sender=Network)114@receiver(post_delete, sender=Network)
115def dns_post_delete_Network(sender, instance, **kwargs):115def dns_post_delete_Network(sender, instance, **kwargs):
116 from maasserver.dns.config import dns_update_all_zones116 from maasserver.dns import config as dns_config
117 dns_update_all_zones()117 dns_config.dns_update_all_zones()
118118
119119
120Config.objects.config_changed_connect("upstream_dns", dns_setting_changed)120Config.objects.config_changed_connect("upstream_dns", dns_setting_changed)
121121
=== modified file 'src/maasserver/models/tests/test_macaddress.py'
--- src/maasserver/models/tests/test_macaddress.py 2015-05-16 06:21:20 +0000
+++ src/maasserver/models/tests/test_macaddress.py 2015-06-15 08:11:13 +0000
@@ -370,12 +370,12 @@
370 def test__returns_empty_if_no_cluster_interface(self):370 def test__returns_empty_if_no_cluster_interface(self):
371 # If mac.cluster_interface is None, we can't allocate any IP.371 # If mac.cluster_interface is None, we can't allocate any IP.
372 mac = factory.make_MACAddress_with_Node()372 mac = factory.make_MACAddress_with_Node()
373 self.assertEquals([], mac.claim_static_ips())373 self.assertEquals([], mac.claim_static_ips(update_host_maps=False))
374374
375 def test__reserves_an_ip_address(self):375 def test__reserves_an_ip_address(self):
376 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface()376 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface()
377 mac = node.get_primary_mac()377 mac = node.get_primary_mac()
378 [claimed_ip] = mac.claim_static_ips()378 [claimed_ip] = mac.claim_static_ips(update_host_maps=False)
379 self.assertIsInstance(claimed_ip, StaticIPAddress)379 self.assertIsInstance(claimed_ip, StaticIPAddress)
380 self.assertNotEqual([], list(node.static_ip_addresses()))380 self.assertNotEqual([], list(node.static_ip_addresses()))
381 self.assertEqual(381 self.assertEqual(
@@ -387,7 +387,8 @@
387 node = make_node_attached_to_cluster_interfaces(387 node = make_node_attached_to_cluster_interfaces(
388 ipv4_network=ipv4_network, ipv6_network=ipv6_network)388 ipv4_network=ipv4_network, ipv6_network=ipv6_network)
389389
390 allocation = node.get_primary_mac().claim_static_ips()390 allocation = node.get_primary_mac().claim_static_ips(
391 update_host_maps=False)
391392
392 # Allocated one IPv4 address and one IPv6 address.393 # Allocated one IPv4 address and one IPv6 address.
393 self.assertThat(allocation, HasLength(2))394 self.assertThat(allocation, HasLength(2))
@@ -401,7 +402,8 @@
401 def test__sets_type_as_required(self):402 def test__sets_type_as_required(self):
402 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface()403 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface()
403 mac = node.get_primary_mac()404 mac = node.get_primary_mac()
404 [claimed_ip] = mac.claim_static_ips(alloc_type=IPADDRESS_TYPE.STICKY)405 [claimed_ip] = mac.claim_static_ips(
406 alloc_type=IPADDRESS_TYPE.STICKY, update_host_maps=False)
405 self.assertEqual(IPADDRESS_TYPE.STICKY, claimed_ip.alloc_type)407 self.assertEqual(IPADDRESS_TYPE.STICKY, claimed_ip.alloc_type)
406408
407 def test__returns_empty_if_no_static_range_defined(self):409 def test__returns_empty_if_no_static_range_defined(self):
@@ -410,7 +412,7 @@
410 mac.cluster_interface.static_ip_range_low = None412 mac.cluster_interface.static_ip_range_low = None
411 mac.cluster_interface.static_ip_range_high = None413 mac.cluster_interface.static_ip_range_high = None
412 mac.cluster_interface.save()414 mac.cluster_interface.save()
413 self.assertEqual([], mac.claim_static_ips())415 self.assertEqual([], mac.claim_static_ips(update_host_maps=False))
414416
415 def test__returns_only_addresses_for_interfaces_with_static_ranges(self):417 def test__returns_only_addresses_for_interfaces_with_static_ranges(self):
416 ipv6_network = factory.make_ipv6_network()418 ipv6_network = factory.make_ipv6_network()
@@ -423,7 +425,8 @@
423 ipv6_interface.static_ip_range_high = None425 ipv6_interface.static_ip_range_high = None
424 ipv6_interface.save()426 ipv6_interface.save()
425427
426 allocation = node.get_primary_mac().claim_static_ips()428 allocation = node.get_primary_mac().claim_static_ips(
429 update_host_maps=False)
427 self.assertThat(allocation, HasLength(1))430 self.assertThat(allocation, HasLength(1))
428 [sip] = allocation431 [sip] = allocation
429 self.assertEqual(4, IPAddress(sip.ip).version)432 self.assertEqual(4, IPAddress(sip.ip).version)
@@ -432,7 +435,7 @@
432 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface()435 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface()
433 mac = node.get_primary_mac()436 mac = node.get_primary_mac()
434 iptype, iptype2 = pick_two_allocation_types()437 iptype, iptype2 = pick_two_allocation_types()
435 mac.claim_static_ips(alloc_type=iptype)438 mac.claim_static_ips(alloc_type=iptype, update_host_maps=False)
436 self.assertRaises(439 self.assertRaises(
437 StaticIPAddressTypeClash,440 StaticIPAddressTypeClash,
438 mac.claim_static_ips, alloc_type=iptype2)441 mac.claim_static_ips, alloc_type=iptype2)
@@ -448,7 +451,8 @@
448 iptype, iptype2 = pick_two_allocation_types()451 iptype, iptype2 = pick_two_allocation_types()
449 mac.claim_static_ips(452 mac.claim_static_ips(
450 alloc_type=iptype,453 alloc_type=iptype,
451 requested_address=ipv4_interface.static_ip_range_low)454 requested_address=ipv4_interface.static_ip_range_low,
455 update_host_maps=False)
452456
453 self.assertRaises(457 self.assertRaises(
454 StaticIPAddressTypeClash,458 StaticIPAddressTypeClash,
@@ -467,7 +471,8 @@
467 iptype, iptype2 = pick_two_allocation_types()471 iptype, iptype2 = pick_two_allocation_types()
468 mac.claim_static_ips(472 mac.claim_static_ips(
469 alloc_type=iptype,473 alloc_type=iptype,
470 requested_address=ipv6_interface.static_ip_range_low)474 requested_address=ipv6_interface.static_ip_range_low,
475 update_host_maps=False)
471476
472 self.assertRaises(477 self.assertRaises(
473 StaticIPAddressTypeClash,478 StaticIPAddressTypeClash,
@@ -487,9 +492,11 @@
487 iptype, iptype2 = pick_two_allocation_types()492 iptype, iptype2 = pick_two_allocation_types()
488 mac.claim_static_ips(493 mac.claim_static_ips(
489 alloc_type=iptype,494 alloc_type=iptype,
490 requested_address=ipv4_interface.static_ip_range_low)495 requested_address=ipv4_interface.static_ip_range_low,
496 update_host_maps=False)
491497
492 allocation = mac.claim_static_ips(alloc_type=iptype2)498 allocation = mac.claim_static_ips(
499 alloc_type=iptype2, update_host_maps=False)
493500
494 self.assertThat(allocation, HasLength(1))501 self.assertThat(allocation, HasLength(1))
495 [sip] = allocation502 [sip] = allocation
@@ -507,9 +514,11 @@
507 iptype, iptype2 = pick_two_allocation_types()514 iptype, iptype2 = pick_two_allocation_types()
508 mac.claim_static_ips(515 mac.claim_static_ips(
509 alloc_type=iptype,516 alloc_type=iptype,
510 requested_address=ipv6_interface.static_ip_range_low)517 requested_address=ipv6_interface.static_ip_range_low,
518 update_host_maps=False)
511519
512 allocation = mac.claim_static_ips(alloc_type=iptype2)520 allocation = mac.claim_static_ips(
521 alloc_type=iptype2, update_host_maps=False)
513522
514 self.assertThat(allocation, HasLength(1))523 self.assertThat(allocation, HasLength(1))
515 [sip] = allocation524 [sip] = allocation
@@ -524,7 +533,7 @@
524 ipv4_network=ipv4_network, ipv6_network=ipv6_network)533 ipv4_network=ipv4_network, ipv6_network=ipv6_network)
525 mac = node.get_primary_mac()534 mac = node.get_primary_mac()
526 iptype, iptype2 = pick_two_allocation_types()535 iptype, iptype2 = pick_two_allocation_types()
527 mac.claim_static_ips(alloc_type=iptype)536 mac.claim_static_ips(alloc_type=iptype, update_host_maps=False)
528537
529 self.assertRaises(538 self.assertRaises(
530 StaticIPAddressTypeClash,539 StaticIPAddressTypeClash,
@@ -542,15 +551,17 @@
542 # Clashing IPv6 address:551 # Clashing IPv6 address:
543 mac.claim_static_ips(552 mac.claim_static_ips(
544 alloc_type=iptype,553 alloc_type=iptype,
545 requested_address=ipv6_interface.static_ip_range_low)554 requested_address=ipv6_interface.static_ip_range_low,
555 update_host_maps=False)
546 # Pre-existing, but non-clashing, IPv4 address:556 # Pre-existing, but non-clashing, IPv4 address:
547 [ipv4_sip] = mac.claim_static_ips(557 [ipv4_sip] = mac.claim_static_ips(
548 alloc_type=iptype2,558 alloc_type=iptype2,
549 requested_address=ipv4_interface.static_ip_range_low)559 requested_address=ipv4_interface.static_ip_range_low,
560 update_host_maps=False)
550561
551 self.assertItemsEqual(562 self.assertItemsEqual(
552 [ipv4_sip],563 [ipv4_sip],
553 mac.claim_static_ips(alloc_type=iptype2))564 mac.claim_static_ips(alloc_type=iptype2, update_host_maps=False))
554565
555 def test__returns_existing_IPv6_if_IPv4_clashes(self):566 def test__returns_existing_IPv6_if_IPv4_clashes(self):
556 # If the MAC is attached to IPv4 and IPv6 cluster interfaces, there's567 # If the MAC is attached to IPv4 and IPv6 cluster interfaces, there's
@@ -564,15 +575,17 @@
564 # Clashing IPv4 address:575 # Clashing IPv4 address:
565 mac.claim_static_ips(576 mac.claim_static_ips(
566 alloc_type=iptype,577 alloc_type=iptype,
567 requested_address=ipv4_interface.static_ip_range_low)578 requested_address=ipv4_interface.static_ip_range_low,
579 update_host_maps=False)
568 # Pre-existing, but non-clashing, IPv6 address:580 # Pre-existing, but non-clashing, IPv6 address:
569 [ipv6_sip] = mac.claim_static_ips(581 [ipv6_sip] = mac.claim_static_ips(
570 alloc_type=iptype2,582 alloc_type=iptype2,
571 requested_address=ipv6_interface.static_ip_range_low)583 requested_address=ipv6_interface.static_ip_range_low,
584 update_host_maps=False)
572585
573 self.assertItemsEqual(586 self.assertItemsEqual(
574 [ipv6_sip],587 [ipv6_sip],
575 mac.claim_static_ips(alloc_type=iptype2))588 mac.claim_static_ips(alloc_type=iptype2, update_host_maps=False))
576589
577 def test__ignores_clashing_IPv4_when_requesting_IPv6(self):590 def test__ignores_clashing_IPv4_when_requesting_IPv6(self):
578 node = make_node_attached_to_cluster_interfaces()591 node = make_node_attached_to_cluster_interfaces()
@@ -582,11 +595,13 @@
582 iptype, iptype2 = pick_two_allocation_types()595 iptype, iptype2 = pick_two_allocation_types()
583 mac.claim_static_ips(596 mac.claim_static_ips(
584 alloc_type=iptype,597 alloc_type=iptype,
585 requested_address=ipv4_interface.static_ip_range_low)598 requested_address=ipv4_interface.static_ip_range_low,
599 update_host_maps=False)
586600
587 allocation = mac.claim_static_ips(601 allocation = mac.claim_static_ips(
588 alloc_type=iptype2,602 alloc_type=iptype2,
589 requested_address=ipv6_interface.static_ip_range_low)603 requested_address=ipv6_interface.static_ip_range_low,
604 update_host_maps=False)
590605
591 self.assertThat(allocation, HasLength(1))606 self.assertThat(allocation, HasLength(1))
592 [ipv6_sip] = allocation607 [ipv6_sip] = allocation
@@ -600,11 +615,13 @@
600 iptype, iptype2 = pick_two_allocation_types()615 iptype, iptype2 = pick_two_allocation_types()
601 mac.claim_static_ips(616 mac.claim_static_ips(
602 alloc_type=iptype,617 alloc_type=iptype,
603 requested_address=ipv6_interface.static_ip_range_low)618 requested_address=ipv6_interface.static_ip_range_low,
619 update_host_maps=False)
604620
605 allocation = mac.claim_static_ips(621 allocation = mac.claim_static_ips(
606 alloc_type=iptype2,622 alloc_type=iptype2,
607 requested_address=ipv4_interface.static_ip_range_low)623 requested_address=ipv4_interface.static_ip_range_low,
624 update_host_maps=False)
608625
609 self.assertThat(allocation, HasLength(1))626 self.assertThat(allocation, HasLength(1))
610 [ipv4_sip] = allocation627 [ipv4_sip] = allocation
@@ -615,22 +632,24 @@
615 mac = node.get_primary_mac()632 mac = node.get_primary_mac()
616 iptype = factory.pick_enum(633 iptype = factory.pick_enum(
617 IPADDRESS_TYPE, but_not=[IPADDRESS_TYPE.USER_RESERVED])634 IPADDRESS_TYPE, but_not=[IPADDRESS_TYPE.USER_RESERVED])
618 [ip] = mac.claim_static_ips(alloc_type=iptype)635 [ip] = mac.claim_static_ips(alloc_type=iptype, update_host_maps=False)
619 self.assertEqual(636 self.assertEqual(
620 [ip], mac.claim_static_ips(alloc_type=iptype))637 [ip], mac.claim_static_ips(
638 alloc_type=iptype, update_host_maps=False))
621639
622 def test__combines_existing_and_new_addresses(self):640 def test__combines_existing_and_new_addresses(self):
623 node = make_node_attached_to_cluster_interfaces()641 node = make_node_attached_to_cluster_interfaces()
624 # node = factory.make_node_with_mac_attached_to_nodegroupinterface()
625 mac = node.get_primary_mac()642 mac = node.get_primary_mac()
626 ipv4_interface = find_cluster_interface(node.nodegroup, 4)643 ipv4_interface = find_cluster_interface(node.nodegroup, 4)
627 iptype = factory.pick_enum(644 iptype = factory.pick_enum(
628 IPADDRESS_TYPE, but_not=[IPADDRESS_TYPE.USER_RESERVED])645 IPADDRESS_TYPE, but_not=[IPADDRESS_TYPE.USER_RESERVED])
629 [ipv4_sip] = mac.claim_static_ips(646 [ipv4_sip] = mac.claim_static_ips(
630 alloc_type=iptype,647 alloc_type=iptype,
631 requested_address=ipv4_interface.static_ip_range_low)648 requested_address=ipv4_interface.static_ip_range_low,
649 update_host_maps=False)
632650
633 allocation = mac.claim_static_ips(alloc_type=iptype)651 allocation = mac.claim_static_ips(
652 alloc_type=iptype, update_host_maps=False)
634653
635 self.assertIn(ipv4_sip, allocation)654 self.assertIn(ipv4_sip, allocation)
636 self.assertThat(allocation, HasLength(2))655 self.assertThat(allocation, HasLength(2))
@@ -639,7 +658,8 @@
639 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface()658 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface()
640 mac = node.get_primary_mac()659 mac = node.get_primary_mac()
641 ip = node.get_primary_mac().cluster_interface.static_ip_range_high660 ip = node.get_primary_mac().cluster_interface.static_ip_range_high
642 [allocation] = mac.claim_static_ips(requested_address=ip)661 [allocation] = mac.claim_static_ips(
662 requested_address=ip, update_host_maps=False)
643 self.assertEqual(ip, allocation.ip)663 self.assertEqual(ip, allocation.ip)
644664
645 def test__allocates_only_IPv4_if_IPv4_address_requested(self):665 def test__allocates_only_IPv4_if_IPv4_address_requested(self):
@@ -648,7 +668,7 @@
648 requested_ip = ipv4_interface.static_ip_range_low668 requested_ip = ipv4_interface.static_ip_range_low
649669
650 allocation = node.get_primary_mac().claim_static_ips(670 allocation = node.get_primary_mac().claim_static_ips(
651 requested_address=requested_ip)671 requested_address=requested_ip, update_host_maps=False)
652672
653 self.assertThat(allocation, HasLength(1))673 self.assertThat(allocation, HasLength(1))
654 [sip] = allocation674 [sip] = allocation
@@ -660,7 +680,7 @@
660 requested_ip = ipv6_interface.static_ip_range_low680 requested_ip = ipv6_interface.static_ip_range_low
661681
662 allocation = node.get_primary_mac().claim_static_ips(682 allocation = node.get_primary_mac().claim_static_ips(
663 requested_address=requested_ip)683 requested_address=requested_ip, update_host_maps=False)
664684
665 self.assertThat(allocation, HasLength(1))685 self.assertThat(allocation, HasLength(1))
666 [sip] = allocation686 [sip] = allocation
@@ -673,7 +693,8 @@
673 cluster_interface=cluster_interface)693 cluster_interface=cluster_interface)
674 user = factory.make_User()694 user = factory.make_User()
675 [sip] = mac_address.claim_static_ips(695 [sip] = mac_address.claim_static_ips(
676 user=user, alloc_type=IPADDRESS_TYPE.USER_RESERVED)696 user=user, alloc_type=IPADDRESS_TYPE.USER_RESERVED,
697 update_host_maps=False)
677 self.assertEqual(sip.user, user)698 self.assertEqual(sip.user, user)
678699
679700
@@ -881,7 +902,7 @@
881 mac = factory.make_MACAddress_with_Node()902 mac = factory.make_MACAddress_with_Node()
882 user = factory.make_User()903 user = factory.make_User()
883 ip_address = factory.make_ip_address()904 ip_address = factory.make_ip_address()
884 static_ip = mac.set_static_ip(ip_address, user)905 static_ip = mac.set_static_ip(ip_address, user, update_host_maps=False)
885906
886 matcher = MatchesStructure(907 matcher = MatchesStructure(
887 id=Not(Is(None)), # IP persisted in the DB.908 id=Not(Is(None)), # IP persisted in the DB.
@@ -905,7 +926,7 @@
905 static_range = IPRange(926 static_range = IPRange(
906 ngi.static_ip_range_low, ngi.static_ip_range_high)927 ngi.static_ip_range_low, ngi.static_ip_range_high)
907 ip_address = factory.pick_ip_in_network(static_range)928 ip_address = factory.pick_ip_in_network(static_range)
908 static_ip = mac.set_static_ip(ip_address, user)929 static_ip = mac.set_static_ip(ip_address, user, update_host_maps=False)
909930
910 matcher = MatchesStructure(931 matcher = MatchesStructure(
911 id=Not(Is(None)), # IP persisted in the DB.932 id=Not(Is(None)), # IP persisted in the DB.
@@ -927,7 +948,7 @@
927 dynamic_range = IPRange(ngi.ip_range_low, ngi.ip_range_high)948 dynamic_range = IPRange(ngi.ip_range_low, ngi.ip_range_high)
928 ip_address = factory.pick_ip_in_network(dynamic_range)949 ip_address = factory.pick_ip_in_network(dynamic_range)
929 with ExpectedException(StaticIPAddressForbidden):950 with ExpectedException(StaticIPAddressForbidden):
930 mac.set_static_ip(ip_address, user)951 mac.set_static_ip(ip_address, user, update_host_maps=False)
931952
932 def test_rejects_ip_if_ip_not_part_of_connected_network(self):953 def test_rejects_ip_if_ip_not_part_of_connected_network(self):
933 node = factory.make_Node(mac=True)954 node = factory.make_Node(mac=True)
@@ -945,15 +966,16 @@
945 other_network = factory.make_ipv4_network(disjoint_from=[network])966 other_network = factory.make_ipv4_network(disjoint_from=[network])
946 ip_address = factory.pick_ip_in_network(other_network)967 ip_address = factory.pick_ip_in_network(other_network)
947 with ExpectedException(StaticIPAddressConflict):968 with ExpectedException(StaticIPAddressConflict):
948 mac.set_static_ip(ip_address, user)969 mac.set_static_ip(ip_address, user, update_host_maps=False)
949970
950 def test_returns_existing_allocation_if_it_exists(self):971 def test_returns_existing_allocation_if_it_exists(self):
951 mac = factory.make_MACAddress_with_Node()972 mac = factory.make_MACAddress_with_Node()
952 user = factory.make_User()973 user = factory.make_User()
953 ip_address = factory.make_ip_address()974 ip_address = factory.make_ip_address()
954 static_ip = mac.set_static_ip(ip_address, user)975 static_ip = mac.set_static_ip(ip_address, user, update_host_maps=False)
955976
956 static_ip2 = mac.set_static_ip(ip_address, user)977 static_ip2 = mac.set_static_ip(
978 ip_address, user, update_host_maps=False)
957979
958 self.assertEquals(static_ip.id, static_ip2.id)980 self.assertEquals(static_ip.id, static_ip2.id)
959981
@@ -961,11 +983,11 @@
961 other_mac = factory.make_MACAddress_with_Node()983 other_mac = factory.make_MACAddress_with_Node()
962 user = factory.make_User()984 user = factory.make_User()
963 ip_address = factory.make_ip_address()985 ip_address = factory.make_ip_address()
964 other_mac.set_static_ip(ip_address, user)986 other_mac.set_static_ip(ip_address, user, update_host_maps=False)
965987
966 mac = factory.make_MACAddress_with_Node()988 mac = factory.make_MACAddress_with_Node()
967 with ExpectedException(StaticIPAddressUnavailable):989 with ExpectedException(StaticIPAddressUnavailable):
968 mac.set_static_ip(ip_address, user)990 mac.set_static_ip(ip_address, user, update_host_maps=False)
969991
970 def test_rejects_ip_if_allocation_with_other_type_already_exists(self):992 def test_rejects_ip_if_allocation_with_other_type_already_exists(self):
971 nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)993 nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
@@ -977,7 +999,7 @@
977 mac.cluster_interface = ngi999 mac.cluster_interface = ngi
978 mac.save()1000 mac.save()
979 # Create an AUTO IP allocation.1001 # Create an AUTO IP allocation.
980 static_ip = mac.claim_static_ips()[0]1002 static_ip = mac.claim_static_ips(update_host_maps=False)[0]
9811003
982 with ExpectedException(StaticIPAddressUnavailable):1004 with ExpectedException(StaticIPAddressUnavailable):
983 mac.set_static_ip(static_ip.ip, user)1005 mac.set_static_ip(static_ip.ip, user, update_host_maps=False)
9841006
=== modified file 'src/maasserver/models/tests/test_node.py'
--- src/maasserver/models/tests/test_node.py 2015-06-10 11:24:44 +0000
+++ src/maasserver/models/tests/test_node.py 2015-06-15 08:11:13 +0000
@@ -92,7 +92,6 @@
92)92)
93from maastesting.djangotestcase import count_queries93from maastesting.djangotestcase import count_queries
94from maastesting.matchers import (94from maastesting.matchers import (
95 MockAnyCall,
96 MockCalledOnceWith,95 MockCalledOnceWith,
97 MockNotCalled,96 MockNotCalled,
98)97)
@@ -351,7 +350,8 @@
351 primary_mac = node.get_primary_mac()350 primary_mac = node.get_primary_mac()
352 random_alloc_type = factory.pick_enum(351 random_alloc_type = factory.pick_enum(
353 IPADDRESS_TYPE, but_not=[IPADDRESS_TYPE.USER_RESERVED])352 IPADDRESS_TYPE, but_not=[IPADDRESS_TYPE.USER_RESERVED])
354 primary_mac.claim_static_ips(alloc_type=random_alloc_type)353 primary_mac.claim_static_ips(
354 alloc_type=random_alloc_type, update_host_maps=False)
355 node.delete()355 node.delete()
356 self.assertItemsEqual([], StaticIPAddress.objects.all())356 self.assertItemsEqual([], StaticIPAddress.objects.all())
357357
@@ -362,7 +362,8 @@
362 primary_mac = node.get_primary_mac()362 primary_mac = node.get_primary_mac()
363 static_ip_addresses = set(363 static_ip_addresses = set(
364 static_ip_address.ip for static_ip_address in364 static_ip_address.ip for static_ip_address in
365 primary_mac.claim_static_ips(alloc_type=IPADDRESS_TYPE.STICKY))365 primary_mac.claim_static_ips(
366 alloc_type=IPADDRESS_TYPE.STICKY, update_host_maps=False))
366 node.delete()367 node.delete()
367 self.assertThat(368 self.assertThat(
368 remove_host_maps, MockCalledOnceWith(369 remove_host_maps, MockCalledOnceWith(
@@ -1244,7 +1245,7 @@
1244 user = factory.make_User()1245 user = factory.make_User()
1245 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface(1246 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface(
1246 owner=user, status=NODE_STATUS.ALLOCATED)1247 owner=user, status=NODE_STATUS.ALLOCATED)
1247 sips = node.get_primary_mac().claim_static_ips()1248 sips = node.get_primary_mac().claim_static_ips(update_host_maps=False)
1248 node.deallocate_static_ip_addresses()1249 node.deallocate_static_ip_addresses()
1249 expected = {sip.ip.format() for sip in sips}1250 expected = {sip.ip.format() for sip in sips}
1250 self.assertThat(1251 self.assertThat(
@@ -1254,14 +1255,14 @@
1254 def test_deallocate_static_ip_updates_dns(self):1255 def test_deallocate_static_ip_updates_dns(self):
1255 # silence remove_host_maps1256 # silence remove_host_maps
1256 self.patch_autospec(node_module, "remove_host_maps")1257 self.patch_autospec(node_module, "remove_host_maps")
1257 dns_update_zones = self.patch(dns_config, 'dns_update_zones')1258 dns_update_zones = self.patch(dns_config.dns_update_zones)
1258 nodegroup = factory.make_NodeGroup(1259 nodegroup = factory.make_NodeGroup(
1259 management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS,1260 management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS,
1260 status=NODEGROUP_STATUS.ENABLED)1261 status=NODEGROUP_STATUS.ENABLED)
1261 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface(1262 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface(
1262 nodegroup=nodegroup, status=NODE_STATUS.ALLOCATED,1263 nodegroup=nodegroup, status=NODE_STATUS.ALLOCATED,
1263 owner=factory.make_User(), power_type='ether_wake')1264 owner=factory.make_User(), power_type='ether_wake')
1264 node.get_primary_mac().claim_static_ips()1265 node.get_primary_mac().claim_static_ips(update_host_maps=False)
1265 node.deallocate_static_ip_addresses()1266 node.deallocate_static_ip_addresses()
1266 self.assertThat(dns_update_zones, MockCalledOnceWith([node.nodegroup]))1267 self.assertThat(dns_update_zones, MockCalledOnceWith([node.nodegroup]))
12671268
@@ -2422,7 +2423,8 @@
24222423
2423 def test__returns_mapping_for_iface_on_managed_network(self):2424 def test__returns_mapping_for_iface_on_managed_network(self):
2424 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface()2425 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface()
2425 static_mappings = node.claim_static_ip_addresses()2426 static_mappings = node.claim_static_ip_addresses(
2427 update_host_maps=False)
2426 [static_ip] = node.static_ip_addresses()2428 [static_ip] = node.static_ip_addresses()
2427 [mac_address] = node.macaddress_set.all()2429 [mac_address] = node.macaddress_set.all()
2428 self.assertEqual(2430 self.assertEqual(
@@ -2436,7 +2438,8 @@
2436 [managed_interface] = node.nodegroup.get_managed_interfaces()2438 [managed_interface] = node.nodegroup.get_managed_interfaces()
2437 node.pxe_mac.cluster_interface = managed_interface2439 node.pxe_mac.cluster_interface = managed_interface
2438 node.pxe_mac.save()2440 node.pxe_mac.save()
2439 static_mappings = node.claim_static_ip_addresses()2441 static_mappings = node.claim_static_ip_addresses(
2442 update_host_maps=False)
2440 [static_ip] = node.static_ip_addresses()2443 [static_ip] = node.static_ip_addresses()
2441 mac_address = node.get_pxe_mac()2444 mac_address = node.get_pxe_mac()
2442 self.assertEqual(2445 self.assertEqual(
@@ -2446,7 +2449,8 @@
2446 def test__ignores_mac_address_with_non_auto_addresses(self):2449 def test__ignores_mac_address_with_non_auto_addresses(self):
2447 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface()2450 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface()
2448 mac_address = node.macaddress_set.first()2451 mac_address = node.macaddress_set.first()
2449 mac_address.claim_static_ips(IPADDRESS_TYPE.STICKY)2452 mac_address.claim_static_ips(
2453 IPADDRESS_TYPE.STICKY, update_host_maps=False)
2450 self.assertRaises(2454 self.assertRaises(
2451 StaticIPAddressTypeClash, mac_address.claim_static_ips)2455 StaticIPAddressTypeClash, mac_address.claim_static_ips)
2452 static_mappings = node.claim_static_ip_addresses()2456 static_mappings = node.claim_static_ip_addresses()
@@ -2464,7 +2468,7 @@
2464 static_range = ngi.get_static_ip_range()2468 static_range = ngi.get_static_ip_range()
24652469
2466 pxe_ip, pxe_mac = node.claim_static_ip_addresses(2470 pxe_ip, pxe_mac = node.claim_static_ip_addresses(
2467 alloc_type=IPADDRESS_TYPE.STICKY)[0]2471 alloc_type=IPADDRESS_TYPE.STICKY, update_host_maps=False)[0]
24682472
2469 self.expectThat(static_range, Contains(IPAddress(pxe_ip)))2473 self.expectThat(static_range, Contains(IPAddress(pxe_ip)))
24702474
@@ -2489,7 +2493,8 @@
2489 first_ip = ngi.get_static_ip_range()[0]2493 first_ip = ngi.get_static_ip_range()[0]
24902494
2491 pxe_ip, pxe_mac = node.claim_static_ip_addresses(2495 pxe_ip, pxe_mac = node.claim_static_ip_addresses(
2492 alloc_type=IPADDRESS_TYPE.STICKY, requested_address=first_ip)[0]2496 alloc_type=IPADDRESS_TYPE.STICKY, requested_address=first_ip,
2497 update_host_maps=False)[0]
2493 mac = MACAddress.objects.get(mac_address=pxe_mac)2498 mac = MACAddress.objects.get(mac_address=pxe_mac)
2494 ip = mac.ip_addresses.first()2499 ip = mac.ip_addresses.first()
2495 self.expectThat(IPAddress(ip.ip), Equals(first_ip))2500 self.expectThat(IPAddress(ip.ip), Equals(first_ip))
@@ -2507,10 +2512,12 @@
2507 first_ip = ngi.get_static_ip_range()[0]2512 first_ip = ngi.get_static_ip_range()[0]
25082513
2509 node.claim_static_ip_addresses(2514 node.claim_static_ip_addresses(
2510 alloc_type=IPADDRESS_TYPE.STICKY, requested_address=first_ip)2515 alloc_type=IPADDRESS_TYPE.STICKY, requested_address=first_ip,
2516 update_host_maps=False)
2511 with ExpectedException(StaticIPAddressUnavailable):2517 with ExpectedException(StaticIPAddressUnavailable):
2512 node2.claim_static_ip_addresses(2518 node2.claim_static_ip_addresses(
2513 alloc_type=IPADDRESS_TYPE.STICKY, requested_address=first_ip)2519 alloc_type=IPADDRESS_TYPE.STICKY, requested_address=first_ip,
2520 update_host_maps=False)
25142521
25152522
2516class TestAsyncDellocateStaticIPAddress(MAASTransactionServerTestCase):2523class TestAsyncDellocateStaticIPAddress(MAASTransactionServerTestCase):
@@ -2554,7 +2561,7 @@
25542561
2555 with transaction.atomic():2562 with transaction.atomic():
2556 pxe_ip, pxe_mac = node.claim_static_ip_addresses(2563 pxe_ip, pxe_mac = node.claim_static_ip_addresses(
2557 alloc_type=IPADDRESS_TYPE.STICKY)[0]2564 alloc_type=IPADDRESS_TYPE.STICKY, update_host_maps=False)[0]
25582565
2559 mac = MACAddress.objects.get(mac_address=pxe_mac)2566 mac = MACAddress.objects.get(mac_address=pxe_mac)
2560 ip = mac.ip_addresses.all()[0]2567 ip = mac.ip_addresses.all()[0]
@@ -2565,7 +2572,8 @@
2565 for mac in macs:2572 for mac in macs:
2566 with transaction.atomic():2573 with transaction.atomic():
2567 sips.append(node.claim_static_ip_addresses(2574 sips.append(node.claim_static_ip_addresses(
2568 mac=mac, alloc_type=IPADDRESS_TYPE.STICKY)[0])2575 mac=mac, alloc_type=IPADDRESS_TYPE.STICKY,
2576 update_host_maps=False)[0])
25692577
2570 # try removing just the IP on the PXE MAC first2578 # try removing just the IP on the PXE MAC first
2571 deallocated = node.deallocate_static_ip_addresses(2579 deallocated = node.deallocate_static_ip_addresses(
@@ -2639,7 +2647,7 @@
2639 user_data = factory.make_bytes()2647 user_data = factory.make_bytes()
26402648
2641 with post_commit_hooks:2649 with post_commit_hooks:
2642 node.start(user, user_data=user_data)2650 node.start(user, user_data=user_data, update_host_maps=False)
26432651
2644 nud = NodeUserData.objects.get(node=node)2652 nud = NodeUserData.objects.get(node=node)
2645 self.assertEqual(user_data, nud.data)2653 self.assertEqual(user_data, nud.data)
@@ -2653,7 +2661,7 @@
2653 NodeUserData.objects.set_user_data(node, user_data)2661 NodeUserData.objects.set_user_data(node, user_data)
26542662
2655 with post_commit_hooks:2663 with post_commit_hooks:
2656 node.start(user, user_data=None)2664 node.start(user, user_data=None, update_host_maps=False)
26572665
2658 self.assertFalse(NodeUserData.objects.filter(node=node).exists())2666 self.assertFalse(NodeUserData.objects.filter(node=node).exists())
26592667
@@ -2664,13 +2672,15 @@
2664 node = self.make_acquired_node_with_mac(user, nodegroup)2672 node = self.make_acquired_node_with_mac(user, nodegroup)
26652673
2666 claim_static_ip_addresses = self.patch_autospec(2674 claim_static_ip_addresses = self.patch_autospec(
2667 node, "claim_static_ip_addresses", spec_set=False)2675 node, "claim_static_ip_addresses")
2668 claim_static_ip_addresses.return_value = {}2676 claim_static_ip_addresses.return_value = {}
26692677
2670 with post_commit_hooks:2678 with post_commit_hooks:
2671 node.start(user)2679 node.start(user)
26722680
2673 self.expectThat(node.claim_static_ip_addresses, MockAnyCall())2681 self.expectThat(
2682 claim_static_ip_addresses, MockCalledOnceWith(
2683 update_host_maps=True))
26742684
2675 def test__only_claims_static_addresses_when_allocated(self):2685 def test__only_claims_static_addresses_when_allocated(self):
2676 user = factory.make_User()2686 user = factory.make_User()
@@ -2685,7 +2695,7 @@
2685 claim_static_ip_addresses.return_value = {}2695 claim_static_ip_addresses.return_value = {}
26862696
2687 with post_commit_hooks:2697 with post_commit_hooks:
2688 node.start(user)2698 node.start(user, update_host_maps=False)
26892699
2690 # No calls are made to claim_static_ip_addresses, since the node2700 # No calls are made to claim_static_ip_addresses, since the node
2691 # isn't ALLOCATED.2701 # isn't ALLOCATED.
@@ -2734,16 +2744,17 @@
2734 self.assertRaises(exception_type, node.start, user)2744 self.assertRaises(exception_type, node.start, user)
27352745
2736 def test__updates_dns(self):2746 def test__updates_dns(self):
2747 dns_update_zones = self.patch(dns_config.dns_update_zones)
2748 self.patch(Node, "update_host_maps")
2749
2737 user = factory.make_User()2750 user = factory.make_User()
2738 node = self.make_acquired_node_with_mac(user)2751 node = self.make_acquired_node_with_mac(user)
27392752
2740 dns_update_zones = self.patch(dns_config, "dns_update_zones")
2741
2742 with post_commit_hooks:2753 with post_commit_hooks:
2743 node.start(user)2754 node.start(user)
27442755
2745 self.assertThat(2756 self.assertThat(
2746 dns_update_zones, MockCalledOnceWith(node.nodegroup))2757 dns_update_zones, MockCalledOnceWith([node.nodegroup]))
27472758
2748 def test__set_zone(self):2759 def test__set_zone(self):
2749 """Verifies whether the set_zone sets the node's zone"""2760 """Verifies whether the set_zone sets the node's zone"""
@@ -2762,7 +2773,7 @@
2762 self.patch_autospec(node, '_start_transition_monitor_async')2773 self.patch_autospec(node, '_start_transition_monitor_async')
27632774
2764 with post_commit_hooks:2775 with post_commit_hooks:
2765 node.start(user)2776 node.start(user, update_host_maps=False)
27662777
2767 # If the following fails the diff is big, but it's useful.2778 # If the following fails the diff is big, but it's useful.
2768 self.maxDiff = None2779 self.maxDiff = None
@@ -2792,13 +2803,13 @@
27922803
2793 with ExpectedException(PraiseBeToJTVException):2804 with ExpectedException(PraiseBeToJTVException):
2794 with post_commit_hooks:2805 with post_commit_hooks:
2795 node.start(user)2806 node.start(user, update_host_maps=False)
27962807
2797 def test__marks_allocated_node_as_deploying(self):2808 def test__marks_allocated_node_as_deploying(self):
2798 user = factory.make_User()2809 user = factory.make_User()
2799 node = self.make_acquired_node_with_mac(user)2810 node = self.make_acquired_node_with_mac(user)
2800 with post_commit_hooks:2811 with post_commit_hooks:
2801 node.start(user)2812 node.start(user, update_host_maps=False)
2802 self.assertEqual(2813 self.assertEqual(
2803 NODE_STATUS.DEPLOYING, reload_object(node).status)2814 NODE_STATUS.DEPLOYING, reload_object(node).status)
28042815
@@ -2813,7 +2824,7 @@
2813 power_on_node = self.patch(node_module, "power_on_node")2824 power_on_node = self.patch(node_module, "power_on_node")
2814 power_on_node.return_value = defer.succeed(None)2825 power_on_node.return_value = defer.succeed(None)
2815 with post_commit_hooks:2826 with post_commit_hooks:
2816 node.start(user)2827 node.start(user, update_host_maps=False)
2817 self.assertEqual(2828 self.assertEqual(
2818 NODE_STATUS.DEPLOYED, reload_object(node).status)2829 NODE_STATUS.DEPLOYED, reload_object(node).status)
28192830
@@ -2829,7 +2840,7 @@
2829 )2840 )
2830 self.patch(node, 'get_effective_power_info').return_value = power_info2841 self.patch(node, 'get_effective_power_info').return_value = power_info
2831 power_on_node = self.patch(node_module, "power_on_node")2842 power_on_node = self.patch(node_module, "power_on_node")
2832 node.start(user)2843 node.start(user, update_host_maps=False)
2833 self.assertThat(power_on_node, MockNotCalled())2844 self.assertThat(power_on_node, MockNotCalled())
28342845
2835 def test__does_not_start_nodes_the_user_cannot_edit(self):2846 def test__does_not_start_nodes_the_user_cannot_edit(self):
@@ -2838,8 +2849,9 @@
2838 node = self.make_acquired_node_with_mac(owner)2849 node = self.make_acquired_node_with_mac(owner)
28392850
2840 user = factory.make_User()2851 user = factory.make_User()
2841 self.assertRaises(PermissionDenied, node.start, user)2852 with ExpectedException(PermissionDenied):
2842 self.assertThat(power_on_node, MockNotCalled())2853 node.start(user, update_host_maps=False)
2854 self.assertThat(power_on_node, MockNotCalled())
28432855
2844 def test__allows_admin_to_start_any_node(self):2856 def test__allows_admin_to_start_any_node(self):
2845 power_on_node = self.patch_autospec(node_module, "power_on_node")2857 power_on_node = self.patch_autospec(node_module, "power_on_node")
@@ -2848,7 +2860,7 @@
28482860
2849 admin = factory.make_admin()2861 admin = factory.make_admin()
2850 with post_commit_hooks:2862 with post_commit_hooks:
2851 node.start(admin)2863 node.start(admin, update_host_maps=False)
28522864
2853 self.expectThat(2865 self.expectThat(
2854 power_on_node, MockCalledOnceWith(2866 power_on_node, MockCalledOnceWith(
@@ -2870,7 +2882,7 @@
28702882
2871 with ExpectedException(exception_type):2883 with ExpectedException(exception_type):
2872 with post_commit_hooks:2884 with post_commit_hooks:
2873 node.start(user)2885 node.start(user, update_host_maps=False)
28742886
2875 self.assertThat(deallocate_ips, MockCalledOnceWith(node))2887 self.assertThat(deallocate_ips, MockCalledOnceWith(node))
28762888
@@ -2880,8 +2892,8 @@
2880 update_host_maps.return_value = [2892 update_host_maps.return_value = [
2881 Failure(exception_type("You steaming nit, you!"))2893 Failure(exception_type("You steaming nit, you!"))
2882 ]2894 ]
2883 deallocate_ips = self.patch(2895 deallocate = self.patch(
2884 node_module.StaticIPAddress.objects, 'deallocate_by_node')2896 node_module.StaticIPAddress, 'deallocate')
28852897
2886 user = factory.make_User()2898 user = factory.make_User()
2887 node = self.make_acquired_node_with_mac(user)2899 node = self.make_acquired_node_with_mac(user)
@@ -2890,7 +2902,8 @@
2890 with post_commit_hooks:2902 with post_commit_hooks:
2891 node.start(user)2903 node.start(user)
28922904
2893 self.assertThat(deallocate_ips, MockCalledOnceWith(node))2905 self.assertThat(
2906 deallocate, MockCalledOnceWith())
28942907
2895 def test_update_host_maps_updates_given_nodegroup_list(self):2908 def test_update_host_maps_updates_given_nodegroup_list(self):
2896 user = factory.make_User()2909 user = factory.make_User()
28972910
=== modified file 'src/maasserver/websockets/handlers/device.py'
--- src/maasserver/websockets/handlers/device.py 2015-05-27 16:17:55 +0000
+++ src/maasserver/websockets/handlers/device.py 2015-06-15 08:11:13 +0000
@@ -17,7 +17,6 @@
17 ]17 ]
1818
19from maasserver.clusterrpc import dhcp19from maasserver.clusterrpc import dhcp
20from maasserver.dns.config import dns_update_zones
21from maasserver.enum import (20from maasserver.enum import (
22 IPADDRESS_TYPE,21 IPADDRESS_TYPE,
23 NODE_PERMISSION,22 NODE_PERMISSION,
@@ -312,7 +311,7 @@
312 ip_assignment = nic["ip_assignment"]311 ip_assignment = nic["ip_assignment"]
313 if ip_assignment == DEVICE_IP_ASSIGNMENT.EXTERNAL:312 if ip_assignment == DEVICE_IP_ASSIGNMENT.EXTERNAL:
314 sticky_ip = mac_address.set_static_ip(313 sticky_ip = mac_address.set_static_ip(
315 nic["ip_address"], self.user)314 nic["ip_address"], self.user, update_host_maps=False)
316 external_static_ips.append(315 external_static_ips.append(
317 (sticky_ip, mac_address))316 (sticky_ip, mac_address))
318 elif ip_assignment == DEVICE_IP_ASSIGNMENT.DYNAMIC:317 elif ip_assignment == DEVICE_IP_ASSIGNMENT.DYNAMIC:
@@ -335,7 +334,7 @@
335 # Claim the static ip.334 # Claim the static ip.
336 sticky_ips = mac_address.claim_static_ips(335 sticky_ips = mac_address.claim_static_ips(
337 alloc_type=IPADDRESS_TYPE.STICKY,336 alloc_type=IPADDRESS_TYPE.STICKY,
338 requested_address=ip_address)337 requested_address=ip_address, update_host_maps=False)
339 assigned_sticky_ips.extend([338 assigned_sticky_ips.extend([
340 (static_ip, mac_address)339 (static_ip, mac_address)
341 for static_ip in sticky_ips340 for static_ip in sticky_ips
@@ -393,7 +392,8 @@
393 log_static_allocations(392 log_static_allocations(
394 device_obj, external_static_ips, assigned_sticky_ips)393 device_obj, external_static_ips, assigned_sticky_ips)
395 if len(external_static_ips) > 0 or len(assigned_sticky_ips) > 0:394 if len(external_static_ips) > 0 or len(assigned_sticky_ips) > 0:
396 dns_update_zones([NodeGroup.objects.ensure_master()])395 from maasserver.dns import config as dns_config
396 dns_config.dns_update_zones([NodeGroup.objects.ensure_master()])
397397
398 return self.full_dehydrate(device_obj)398 return self.full_dehydrate(device_obj)
399399
400400
=== modified file 'src/maasserver/websockets/handlers/tests/test_device.py'
--- src/maasserver/websockets/handlers/tests/test_device.py 2015-05-26 23:55:25 +0000
+++ src/maasserver/websockets/handlers/tests/test_device.py 2015-06-15 08:11:13 +0000
@@ -17,6 +17,7 @@
17import re17import re
1818
19from maasserver.clusterrpc import dhcp as dhcp_module19from maasserver.clusterrpc import dhcp as dhcp_module
20from maasserver.dns import config as dns_config
20from maasserver.exceptions import NodeActionError21from maasserver.exceptions import NodeActionError
21from maasserver.fields import MAC22from maasserver.fields import MAC
22from maasserver.forms import (23from maasserver.forms import (
@@ -155,7 +156,7 @@
155 interface = factory.make_NodeGroupInterface(nodegroup)156 interface = factory.make_NodeGroupInterface(nodegroup)
156 primary_mac.cluster_interface = interface157 primary_mac.cluster_interface = interface
157 primary_mac.save()158 primary_mac.save()
158 primary_mac.claim_static_ips()159 primary_mac.claim_static_ips(update_host_maps=False)
159 return device160 return device
160161
161 def make_devices(self, nodegroup, number, owner=None):162 def make_devices(self, nodegroup, number, owner=None):
@@ -190,6 +191,7 @@
190 handler.list({}))191 handler.list({}))
191192
192 def test_list_num_queries_is_independent_of_num_devices(self):193 def test_list_num_queries_is_independent_of_num_devices(self):
194 self.patch(Node, "update_host_maps")
193 owner = factory.make_User()195 owner = factory.make_User()
194 handler = DeviceHandler(owner, {})196 handler = DeviceHandler(owner, {})
195 nodegroup = factory.make_NodeGroup()197 nodegroup = factory.make_NodeGroup()
@@ -211,6 +213,7 @@
211 "Number of queries is not independent to the number of nodes.")213 "Number of queries is not independent to the number of nodes.")
212214
213 def test_list_returns_devices_only_viewable_by_user(self):215 def test_list_returns_devices_only_viewable_by_user(self):
216 self.patch(Node, "update_host_maps")
214 user = factory.make_User()217 user = factory.make_User()
215 # Create another user.218 # Create another user.
216 factory.make_User()219 factory.make_User()
@@ -327,7 +330,7 @@
327 hostname = factory.make_name("hostname")330 hostname = factory.make_name("hostname")
328 ip_address = factory.make_ipv4_address()331 ip_address = factory.make_ipv4_address()
329 self.patch(dhcp_module, "update_host_maps").return_value = []332 self.patch(dhcp_module, "update_host_maps").return_value = []
330 mock_dns_update_zones = self.patch(device_module, "dns_update_zones")333 mock_dns_update_zones = self.patch(dns_config.dns_update_zones)
331 handler.create({334 handler.create({
332 "hostname": hostname,335 "hostname": hostname,
333 "primary_mac": mac,336 "primary_mac": mac,
@@ -371,6 +374,7 @@
371 Equals(0), "Created StaticIPAddress was not deleted.")374 Equals(0), "Created StaticIPAddress was not deleted.")
372375
373 def test_create_creates_device_with_static_ip_assignment_implicit(self):376 def test_create_creates_device_with_static_ip_assignment_implicit(self):
377 self.patch(Node, "update_host_maps")
374 user = factory.make_User()378 user = factory.make_User()
375 handler = DeviceHandler(user, {})379 handler = DeviceHandler(user, {})
376 mac = factory.make_mac_address()380 mac = factory.make_mac_address()
@@ -400,6 +404,7 @@
400 Equals(1), "StaticIPAddress was not created.")404 Equals(1), "StaticIPAddress was not created.")
401405
402 def test_create_creates_device_with_static_ip_assignment_explicit(self):406 def test_create_creates_device_with_static_ip_assignment_explicit(self):
407 self.patch(Node, "update_host_maps")
403 user = factory.make_User()408 user = factory.make_User()
404 handler = DeviceHandler(user, {})409 handler = DeviceHandler(user, {})
405 mac = factory.make_mac_address()410 mac = factory.make_mac_address()
@@ -431,14 +436,15 @@
431 Equals(1), "StaticIPAddress was not created.")436 Equals(1), "StaticIPAddress was not created.")
432437
433 def test_create_with_static_ip_calls_dns_update_zones(self):438 def test_create_with_static_ip_calls_dns_update_zones(self):
439 self.patch(device_module.update_host_maps).return_value = []
440 # self.patch(Node.update_host_maps).return_value = []
434 user = factory.make_User()441 user = factory.make_User()
435 handler = DeviceHandler(user, {})442 handler = DeviceHandler(user, {})
436 mac = factory.make_mac_address()443 mac = factory.make_mac_address()
437 hostname = factory.make_name("hostname")444 hostname = factory.make_name("hostname")
438 nodegroup = factory.make_NodeGroup()445 nodegroup = factory.make_NodeGroup()
439 nodegroup_interface = factory.make_NodeGroupInterface(nodegroup)446 nodegroup_interface = factory.make_NodeGroupInterface(nodegroup)
440 self.patch(dhcp_module, "update_host_maps").return_value = []447 mock_dns_update_zones = self.patch(dns_config.dns_update_zones)
441 mock_dns_update_zones = self.patch(device_module, "dns_update_zones")
442 handler.create({448 handler.create({
443 "hostname": hostname,449 "hostname": hostname,
444 "primary_mac": mac,450 "primary_mac": mac,
@@ -453,6 +459,8 @@
453 MockCalledOnceWith([NodeGroup.objects.ensure_master()]))459 MockCalledOnceWith([NodeGroup.objects.ensure_master()]))
454460
455 def test_create_raises_failure_static_ip_update_hostmaps_fails(self):461 def test_create_raises_failure_static_ip_update_hostmaps_fails(self):
462 self.patch(Node, "update_host_maps")
463 mock_update_host_maps = self.patch(dhcp_module, "update_host_maps")
456 user = factory.make_User()464 user = factory.make_User()
457 handler = DeviceHandler(user, {})465 handler = DeviceHandler(user, {})
458 mac = factory.make_mac_address()466 mac = factory.make_mac_address()
@@ -460,7 +468,6 @@
460 nodegroup = factory.make_NodeGroup()468 nodegroup = factory.make_NodeGroup()
461 nodegroup_interface = factory.make_NodeGroupInterface(nodegroup)469 nodegroup_interface = factory.make_NodeGroupInterface(nodegroup)
462 ip_address = nodegroup_interface.static_ip_range_low470 ip_address = nodegroup_interface.static_ip_range_low
463 mock_update_host_maps = self.patch(dhcp_module, "update_host_maps")
464 mock_update_host_maps.return_value = [471 mock_update_host_maps.return_value = [
465 Failure(factory.make_exception()),472 Failure(factory.make_exception()),
466 ]473 ]
@@ -485,6 +492,7 @@
485 Equals(0), "Created StaticIPAddress was not deleted.")492 Equals(0), "Created StaticIPAddress was not deleted.")
486493
487 def test_create_creates_device_with_static_and_external_ip(self):494 def test_create_creates_device_with_static_and_external_ip(self):
495 self.patch(Node, "update_host_maps")
488 user = factory.make_User()496 user = factory.make_User()
489 handler = DeviceHandler(user, {})497 handler = DeviceHandler(user, {})
490 hostname = factory.make_name("hostname")498 hostname = factory.make_name("hostname")
@@ -539,18 +547,19 @@
539 Equals(1), "External StaticIPAddress was not created.")547 Equals(1), "External StaticIPAddress was not created.")
540548
541 def test_create_deletes_device_and_ips_when_only_one_errors(self):549 def test_create_deletes_device_and_ips_when_only_one_errors(self):
542 user = factory.make_User()550 self.patch(Node, "update_host_maps")
543 handler = DeviceHandler(user, {})
544 hostname = factory.make_name("hostname")
545 nodegroup = factory.make_NodeGroup()
546 nodegroup_interface = factory.make_NodeGroupInterface(nodegroup)
547 mac_static = factory.make_mac_address()
548 static_ip_address = nodegroup_interface.static_ip_range_low
549 mac_external = factory.make_mac_address()
550 external_ip_address = factory.make_ipv4_address()
551 self.patch(dhcp_module, "update_host_maps").side_effect = [551 self.patch(dhcp_module, "update_host_maps").side_effect = [
552 [], [Failure(factory.make_exception())],552 [], [Failure(factory.make_exception())],
553 ]553 ]
554 user = factory.make_User()
555 handler = DeviceHandler(user, {})
556 hostname = factory.make_name("hostname")
557 nodegroup = factory.make_NodeGroup()
558 nodegroup_interface = factory.make_NodeGroupInterface(nodegroup)
559 mac_static = factory.make_mac_address()
560 static_ip_address = nodegroup_interface.static_ip_range_low
561 mac_external = factory.make_mac_address()
562 external_ip_address = factory.make_ipv4_address()
554 self.assertRaises(HandlerError, handler.create, {563 self.assertRaises(HandlerError, handler.create, {
555 "hostname": hostname,564 "hostname": hostname,
556 "primary_mac": mac_static,565 "primary_mac": mac_static,
557566
=== modified file 'src/maastesting/testcase.py'
--- src/maastesting/testcase.py 2015-05-07 18:14:38 +0000
+++ src/maastesting/testcase.py 2015-06-15 08:11:13 +0000
@@ -19,6 +19,7 @@
1919
20from contextlib import contextmanager20from contextlib import contextmanager
21import doctest21import doctest
22from importlib import import_module
22import types23import types
23import unittest24import unittest
2425
@@ -177,7 +178,7 @@
177 with active_test(result, self):178 with active_test(result, self):
178 super(MAASTestCase, self).__call__(result)179 super(MAASTestCase, self).__call__(result)
179180
180 def patch(self, obj, attribute, value=mock.sentinel.unset):181 def patch(self, obj, attribute=None, value=mock.sentinel.unset):
181 """Patch `obj.attribute` with `value`.182 """Patch `obj.attribute` with `value`.
182183
183 If `value` is unspecified, a new `MagicMock` will be created and184 If `value` is unspecified, a new `MagicMock` will be created and
@@ -188,6 +189,14 @@
188189
189 :return: The patched-in object.190 :return: The patched-in object.
190 """191 """
192
193 # If 'attribute' is None, assume 'obj' is a 'fully-qualified' object,
194 # and assume that its __module__ is what we want to patch. For more
195 # complex use cases, the two-paramerter 'patch' will still need to
196 # be used.
197 if attribute is None:
198 attribute = obj.__name__
199 obj = import_module(obj.__module__)
191 if value is mock.sentinel.unset:200 if value is mock.sentinel.unset:
192 value = mock.MagicMock()201 value = mock.MagicMock()
193 super(MAASTestCase, self).patch(obj, attribute, value)202 super(MAASTestCase, self).patch(obj, attribute, value)