Merge lp:~gmb/maas/add-api-reserve-sip-for-mac-bug-1387239 into lp:~maas-committers/maas/trunk

Proposed by Graham Binns
Status: Merged
Approved by: Graham Binns
Approved revision: no longer in the source branch.
Merged at revision: 3391
Proposed branch: lp:~gmb/maas/add-api-reserve-sip-for-mac-bug-1387239
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 479 lines (+270/-21)
6 files modified
src/maasserver/api/ip_addresses.py (+90/-10)
src/maasserver/api/tests/test_ipaddresses.py (+151/-3)
src/maasserver/exceptions.py (+6/-0)
src/maasserver/models/macaddress.py (+13/-6)
src/maasserver/models/tests/test_macaddress.py (+10/-0)
src/maasserver/testing/factory.py (+0/-2)
To merge this branch: bzr merge lp:~gmb/maas/add-api-reserve-sip-for-mac-bug-1387239
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Raphaël Badin Pending
Andres Rodriguez Pending
Review via email: mp+242788@code.launchpad.net

Commit message

Add an optional "mac" parameter to the static IP address reservation API. Specifying a MAC address here will link the new static IP to that MAC address. A DHCP host map will be created for the MAC address. No other IPs may be reserved for that MAC address until the current one is released.

Description of the change

Based loosely on the old branch, but significantly updated to make use of MACAddress.node being null-able now.

We don't do the work of linking MACAddress and StaticIPAddress in the StaticIPAddressManager class because MACAddress.claim_static_ips() gives it us for free! It also gives us protection against trying to create > 1 Static IP for a MAC on a given cluster interface, though I've added an exception to explain that to the user (because otherwise, this would happen):

>>> claim_ip(requested_ip="123.123.0.1", mac="aa:bb:cc:dd:ee:ff")
123.123.0.1
>>> claim_ip(requested_ip="234.234.0.1", mac="aa:bb:cc:dd:ee:ff")
123.123.0.1

Which is confusing.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

LGTM.

review: Approve
Revision history for this message
Graham Binns (gmb) wrote :
Download full text (22.6 KiB)

On 26 November 2014 at 11:09, Gavin Panella <email address hidden>
wrote:

> Review: Approve
>
> LGTM.
>
> Diff comments:
>
> > === modified file 'src/maasserver/api/ip_addresses.py'
> > --- src/maasserver/api/ip_addresses.py 2014-11-24 17:55:55 +0000
> > +++ src/maasserver/api/ip_addresses.py 2014-11-25 14:41:46 +0000
> > @@ -16,6 +16,7 @@
> > 'IPAddressesHandler',
> > ]
> >
> > +from django.db import transaction
> > from django.shortcuts import get_object_or_404
> > from maasserver.api.support import (
> > operation,
> > @@ -25,15 +26,23 @@
> > get_mandatory_param,
> > get_optional_param,
> > )
> > +from maasserver.clusterrpc.dhcp import (
> > + remove_host_maps,
> > + update_host_maps,
> > + )
> > from maasserver.enum import (
> > IPADDRESS_TYPE,
> > NODEGROUP_STATUS,
> > )
> > -from maasserver.exceptions import MAASAPIBadRequest
> > +from maasserver.exceptions import (
> > + MAASAPIBadRequest,
> > + StaticIPAlreadyExistsForMACAddress,
> > + )
> > from maasserver.models import (
> > NodeGroupInterface,
> > StaticIPAddress,
> > )
> > +from maasserver.models.macaddress import MACAddress
> > import netaddr
> > from provisioningserver.logger import get_maas_logger
> >
> > @@ -53,19 +62,53 @@
> > def resource_uri(cls, *args, **kwargs):
> > return ('ipaddresses_handler', [])
> >
> > - def claim_ip(self, user, interface, requested_address):
> > + @transaction.atomic
> > + def claim_ip(self, user, interface, requested_address, mac=None):
> > """Attempt to get a USER_RESERVED StaticIPAddress for `user` on
> > `interface`.
> >
> > :raises StaticIPAddressExhaustion: If no IPs available.
> > """
> > - sip = StaticIPAddress.objects.allocate_new(
> > - range_low=interface.static_ip_range_low,
> > - range_high=interface.static_ip_range_high,
> > - alloc_type=IPADDRESS_TYPE.USER_RESERVED,
> > - requested_address=requested_address,
> > - user=user)
> > - maaslog.info("User %s was allocated IP %s", user.username,
> sip.ip)
> > + if mac is not None:
>
> Might be worth switching around this conditional to remove the negative.
>

Yep.

>
> > + mac_address, _ = MACAddress.objects.get_or_create(
> > + mac_address=mac, cluster_interface=interface)
> > + ips_on_interface = (
> > + addr.ip for addr in mac_address.ip_addresses.all()
> > + if netaddr.IPAddress(addr.ip) in interface.network)
> > + if any(ips_on_interface):
> > + # If this MAC already has static IPs on the interface in
> > + # question we raise an error, since we can't sanely
> > + # allocate more addresses for the MAC here.
> > + raise StaticIPAlreadyExistsForMACAddress(
> > + "MAC address %s already has the IP address(es) %s."
> %
> > + (mac, ips_on_interface))
> > +
> > + [sip] = mac_address.claim_static_ips(
> > + alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=user...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/ip_addresses.py'
2--- src/maasserver/api/ip_addresses.py 2014-11-24 17:55:55 +0000
3+++ src/maasserver/api/ip_addresses.py 2014-11-26 12:14:08 +0000
4@@ -16,6 +16,7 @@
5 'IPAddressesHandler',
6 ]
7
8+from django.db import transaction
9 from django.shortcuts import get_object_or_404
10 from maasserver.api.support import (
11 operation,
12@@ -25,15 +26,24 @@
13 get_mandatory_param,
14 get_optional_param,
15 )
16+from maasserver.clusterrpc.dhcp import (
17+ remove_host_maps,
18+ update_host_maps,
19+ )
20 from maasserver.enum import (
21 IPADDRESS_TYPE,
22 NODEGROUP_STATUS,
23 )
24-from maasserver.exceptions import MAASAPIBadRequest
25+from maasserver.exceptions import (
26+ MAASAPIBadRequest,
27+ StaticIPAlreadyExistsForMACAddress,
28+ )
29 from maasserver.models import (
30 NodeGroupInterface,
31 StaticIPAddress,
32 )
33+from maasserver.models.macaddress import MACAddress
34+from maasserver.utils.orm import commit_within_atomic_block
35 import netaddr
36 from provisioningserver.logger import get_maas_logger
37
38@@ -53,19 +63,65 @@
39 def resource_uri(cls, *args, **kwargs):
40 return ('ipaddresses_handler', [])
41
42- def claim_ip(self, user, interface, requested_address):
43+ @transaction.atomic
44+ def claim_ip(self, user, interface, requested_address, mac=None):
45 """Attempt to get a USER_RESERVED StaticIPAddress for `user` on
46 `interface`.
47
48 :raises StaticIPAddressExhaustion: If no IPs available.
49 """
50- sip = StaticIPAddress.objects.allocate_new(
51- range_low=interface.static_ip_range_low,
52- range_high=interface.static_ip_range_high,
53- alloc_type=IPADDRESS_TYPE.USER_RESERVED,
54- requested_address=requested_address,
55- user=user)
56- maaslog.info("User %s was allocated IP %s", user.username, sip.ip)
57+ if mac is None:
58+ sip = StaticIPAddress.objects.allocate_new(
59+ range_low=interface.static_ip_range_low,
60+ range_high=interface.static_ip_range_high,
61+ alloc_type=IPADDRESS_TYPE.USER_RESERVED,
62+ requested_address=requested_address,
63+ user=user)
64+ commit_within_atomic_block()
65+ maaslog.info("User %s was allocated IP %s", user.username, sip.ip)
66+ else:
67+ # The user has requested a static IP linked to a MAC
68+ # address, so we set that up via the MACAddress model.
69+ mac_address, _ = MACAddress.objects.get_or_create(
70+ mac_address=mac, cluster_interface=interface)
71+ ips_on_interface = (
72+ addr.ip for addr in mac_address.ip_addresses.all()
73+ if netaddr.IPAddress(addr.ip) in interface.network)
74+ if any(ips_on_interface):
75+ # If this MAC already has static IPs on the interface in
76+ # question we raise an error, since we can't sanely
77+ # allocate more addresses for the MAC here.
78+ raise StaticIPAlreadyExistsForMACAddress(
79+ "MAC address %s already has the IP address(es) %s." %
80+ (mac, ips_on_interface))
81+
82+ [sip] = mac_address.claim_static_ips(
83+ alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=user,
84+ requested_address=requested_address)
85+ # Update the DHCP host maps for the cluster so that this MAC
86+ # gets an entry with this static IP.
87+ host_map_updates = {
88+ interface.nodegroup: {
89+ sip.ip: mac_address.mac_address,
90+ }
91+ }
92+ # Commit the DB changes before we do RPC calls.
93+ commit_within_atomic_block()
94+ update_host_maps_failures = list(
95+ update_host_maps(host_map_updates))
96+ if len(update_host_maps_failures) > 0:
97+ # Deallocate the static IPs and delete the MAC address
98+ # if it doesn't have a Node attached.
99+ if mac_address.node is None:
100+ mac_address.delete()
101+ sip.deallocate()
102+ commit_within_atomic_block()
103+
104+ # There will only ever be one error, so raise that.
105+ raise update_host_maps_failures[0].value
106+ maaslog.info(
107+ "User %s was allocated IP %s for MAC address %s",
108+ user.username, sip.ip, mac_address.mac_address)
109 return sip
110
111 @operation(idempotent=False)
112@@ -86,6 +142,7 @@
113 network = get_mandatory_param(request.POST, "network")
114 requested_address = get_optional_param(
115 request.POST, "requested_address")
116+ mac_address = get_optional_param(request.POST, "mac")
117 # Validate the passed network.
118 try:
119 valid_network = netaddr.IPNetwork(network)
120@@ -103,7 +160,7 @@
121 if valid_network == interface.network:
122 # Winner winner chicken dinner.
123 return self.claim_ip(
124- request.user, interface, requested_address)
125+ request.user, interface, requested_address, mac_address)
126 raise MAASAPIBadRequest(
127 "No network found matching %s; you may be requesting an IP "
128 "on a network with no static IP range defined." % network)
129@@ -120,7 +177,30 @@
130 ip = get_mandatory_param(request.POST, "ip")
131 staticaddress = get_object_or_404(
132 StaticIPAddress, ip=ip, user=request.user)
133+
134+ linked_mac_addresses = staticaddress.macaddress_set
135+ linked_mac_address_interfaces = set(
136+ mac_address.cluster_interface
137+ for mac_address in linked_mac_addresses.all())
138+
139+ # Remove any hostmaps for this IP.
140+ host_maps_to_remove = {
141+ interface.nodegroup: [staticaddress.ip]
142+ for interface in linked_mac_address_interfaces
143+ }
144+ remove_host_maps_failures = list(
145+ remove_host_maps(host_maps_to_remove))
146+ if len(remove_host_maps_failures) > 0:
147+ # There's only going to be one failure, so raise that.
148+ raise remove_host_maps_failures[0].value
149+
150+ # Delete any MACAddress entries that are attached to this static
151+ # IP but that *aren't* attached to a Node. With the DB isolation
152+ # at SERIALIZABLE there will be no race here, and it's better to
153+ # keep cruft out of the DB.
154+ linked_mac_addresses.filter(node=None).delete()
155 staticaddress.deallocate()
156+
157 maaslog.info("User %s released IP %s", request.user.username, ip)
158
159 def read(self, request):
160
161=== modified file 'src/maasserver/api/tests/test_ipaddresses.py'
162--- src/maasserver/api/tests/test_ipaddresses.py 2014-10-22 10:35:09 +0000
163+++ src/maasserver/api/tests/test_ipaddresses.py 2014-11-26 12:14:08 +0000
164@@ -18,16 +18,26 @@
165 import json
166
167 from django.core.urlresolvers import reverse
168+from maasserver.api import ip_addresses as ip_addresses_module
169 from maasserver.enum import (
170 IPADDRESS_TYPE,
171 NODEGROUP_STATUS,
172 )
173 from maasserver.models import StaticIPAddress
174+from maasserver.models.macaddress import MACAddress
175 from maasserver.testing.api import APITestCase
176 from maasserver.testing.factory import factory
177 from maasserver.testing.orm import reload_object
178+from maastesting.matchers import MockCalledOnceWith
179 from netaddr import IPAddress
180-from testtools.matchers import Equals
181+from provisioningserver.rpc.exceptions import NoConnectionsAvailable
182+from testtools.matchers import (
183+ Equals,
184+ HasLength,
185+ Is,
186+ Not,
187+ )
188+from twisted.python.failure import Failure
189
190
191 class TestNetworksAPI(APITestCase):
192@@ -36,19 +46,22 @@
193 cluster = factory.make_NodeGroup(status=status, **kwargs)
194 return factory.make_NodeGroupInterface(cluster)
195
196- def post_reservation_request(self, net, requested_address=None):
197+ def post_reservation_request(self, net, requested_address=None, mac=None):
198 params = {
199 'op': 'reserve',
200 'network': unicode(net),
201 }
202 if requested_address is not None:
203 params["requested_address"] = requested_address
204+ if mac is not None:
205+ params["mac"] = mac
206 return self.client.post(reverse('ipaddresses_handler'), params)
207
208- def post_release_request(self, ip):
209+ def post_release_request(self, ip, mac=None):
210 params = {
211 'op': 'release',
212 'ip': ip,
213+ 'mac': mac,
214 }
215 return self.client.post(reverse('ipaddresses_handler'), params)
216
217@@ -84,6 +97,77 @@
218 IPADDRESS_TYPE.USER_RESERVED, staticipaddress.alloc_type)
219 self.assertEqual(self.logged_in_user, staticipaddress.user)
220
221+ def test_POST_reserve_with_MAC_links_MAC_to_ip_address(self):
222+ update_host_maps = self.patch(ip_addresses_module, 'update_host_maps')
223+ interface = self.make_interface()
224+ net = interface.network
225+ mac = factory.make_mac_address()
226+
227+ response = self.post_reservation_request(net, mac=mac)
228+ self.assertEqual(httplib.OK, response.status_code)
229+ returned_address = json.loads(response.content)
230+ [staticipaddress] = StaticIPAddress.objects.all()
231+ self.expectThat(
232+ staticipaddress.macaddress_set.first().mac_address,
233+ Equals(mac))
234+
235+ # DHCP Host maps have been updated.
236+ self.expectThat(
237+ update_host_maps,
238+ MockCalledOnceWith(
239+ {interface.nodegroup: {returned_address['ip']: mac}}))
240+
241+ def test_POST_reserve_with_MAC_returns_503_if_hostmap_update_fails(self):
242+ update_host_maps = self.patch(ip_addresses_module, 'update_host_maps')
243+ # We a specific exception here because update_host_maps() will
244+ # fail with RPC-specific errors.
245+ update_host_maps.return_value = [
246+ Failure(
247+ NoConnectionsAvailable(
248+ "Are you sure you're not Elvis?"))
249+ ]
250+ interface = self.make_interface()
251+ net = interface.network
252+ mac = factory.make_mac_address()
253+
254+ response = self.post_reservation_request(net, mac=mac)
255+ self.expectThat(
256+ response.status_code, Equals(httplib.SERVICE_UNAVAILABLE))
257+ # No static IP has been created.
258+ self.expectThat(
259+ StaticIPAddress.objects.all(), HasLength(0))
260+ # No MAC address has been created, either.
261+ self.expectThat(
262+ MACAddress.objects.all(), HasLength(0))
263+
264+ def test_POST_returns_CONFLICT_when_static_ip_for_MAC_already_exists(self):
265+ interface = self.make_interface()
266+ mac = factory.make_MACAddress(cluster_interface=interface)
267+ mac.claim_static_ips()
268+ net = interface.network
269+
270+ response = self.post_reservation_request(net, mac=mac.mac_address)
271+ self.expectThat(
272+ response.status_code, Equals(httplib.CONFLICT))
273+ # No new static IP has been created.
274+ self.expectThat(
275+ StaticIPAddress.objects.all().count(),
276+ Equals(1))
277+
278+ def test_POST_allows_claiming_of_new_static_ips_for_existing_MAC(self):
279+ self.patch(ip_addresses_module, 'update_host_maps')
280+
281+ interface = self.make_interface()
282+ net = interface.network
283+ mac = factory.make_MACAddress(cluster_interface=interface)
284+
285+ response = self.post_reservation_request(net, mac=mac.mac_address)
286+ self.expectThat(response.status_code, Equals(httplib.OK))
287+ [staticipaddress] = StaticIPAddress.objects.all()
288+ self.assertEqual(
289+ staticipaddress.macaddress_set.first().mac_address,
290+ mac.mac_address)
291+
292 def test_POST_reserve_errors_for_no_matching_interface(self):
293 interface = self.make_interface()
294 net = factory.make_ipv4_network(but_not=[interface.network])
295@@ -202,6 +286,70 @@
296 self.assertEqual(httplib.OK, response.status_code, response.content)
297 self.assertIsNone(reload_object(ipaddress))
298
299+ def test_POST_release_deletes_floating_MAC_address(self):
300+ self.patch(ip_addresses_module, 'remove_host_maps')
301+
302+ interface = self.make_interface()
303+ floating_mac = factory.make_MACAddress(cluster_interface=interface)
304+ [ipaddress] = floating_mac.claim_static_ips(
305+ alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user)
306+
307+ self.post_release_request(ipaddress.ip)
308+ self.assertIsNone(reload_object(floating_mac))
309+
310+ def test_POST_release_does_not_delete_MACs_linked_to_nodes(self):
311+ self.patch(ip_addresses_module, 'remove_host_maps')
312+
313+ interface = self.make_interface()
314+ node = factory.make_Node(nodegroup=interface.nodegroup)
315+ attached_mac = factory.make_MACAddress(
316+ node=node, cluster_interface=interface)
317+ [ipaddress] = attached_mac.claim_static_ips(
318+ alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user)
319+
320+ self.post_release_request(ipaddress.ip)
321+ self.assertEqual(attached_mac, reload_object(attached_mac))
322+
323+ def test_POST_release_updates_DNS_and_DHCP(self):
324+ remove_host_maps = self.patch(ip_addresses_module, 'remove_host_maps')
325+
326+ interface = self.make_interface()
327+ floating_mac = factory.make_MACAddress(cluster_interface=interface)
328+ [ipaddress] = floating_mac.claim_static_ips(
329+ alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user)
330+
331+ self.post_release_request(ipaddress.ip)
332+ self.expectThat(
333+ remove_host_maps, MockCalledOnceWith(
334+ {interface.nodegroup: [ipaddress.ip]}))
335+
336+ def test_POST_release_raises_503_if_removing_host_maps_errors(self):
337+ remove_host_maps = self.patch(ip_addresses_module, 'remove_host_maps')
338+ # Failures in remove_host_maps() will be RPC-related exceptions,
339+ # so we use one of those explicitly.
340+ remove_host_maps.return_value = [
341+ Failure(
342+ NoConnectionsAvailable(
343+ "The wizard's staff has a knob on the end."))
344+ ]
345+
346+ interface = self.make_interface()
347+ floating_mac = factory.make_MACAddress(cluster_interface=interface)
348+ [ipaddress] = floating_mac.claim_static_ips(
349+ alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user)
350+
351+ response = self.post_release_request(ipaddress.ip)
352+ self.expectThat(
353+ response.status_code, Equals(httplib.SERVICE_UNAVAILABLE))
354+
355+ # The static IP hasn't been deleted.
356+ self.expectThat(
357+ reload_object(ipaddress), Not(Is(None)))
358+
359+ # Neither has the DHCPHost.
360+ self.expectThat(
361+ reload_object(floating_mac), Not(Is(None)))
362+
363 def test_POST_release_does_not_delete_IP_that_I_dont_own(self):
364 ipaddress = factory.make_StaticIPAddress(user=factory.make_User())
365 response = self.post_release_request(ipaddress.ip)
366
367=== modified file 'src/maasserver/exceptions.py'
368--- src/maasserver/exceptions.py 2014-10-08 09:43:51 +0000
369+++ src/maasserver/exceptions.py 2014-11-26 12:14:08 +0000
370@@ -140,6 +140,12 @@
371 api_error = httplib.CONFLICT
372
373
374+class StaticIPAlreadyExistsForMACAddress(MAASAPIException):
375+ """Raised when trying to allocate a static IP for a non-node MAC
376+ where a node with that MAC already exists."""
377+ api_error = httplib.CONFLICT
378+
379+
380 class NodeActionError(MAASException):
381 """Raised when there is an error performing a NodeAction."""
382
383
384=== modified file 'src/maasserver/models/macaddress.py'
385--- src/maasserver/models/macaddress.py 2014-11-21 20:25:54 +0000
386+++ src/maasserver/models/macaddress.py 2014-11-26 12:14:08 +0000
387@@ -204,7 +204,7 @@
388 return allocations
389
390 def _allocate_static_address(self, cluster_interface, alloc_type,
391- requested_address=None):
392+ requested_address=None, user=None):
393 """Allocate a `StaticIPAddress` for this MAC."""
394 # Avoid circular imports.
395 from maasserver.models import (
396@@ -215,12 +215,13 @@
397 new_sip = StaticIPAddress.objects.allocate_new(
398 cluster_interface.static_ip_range_low,
399 cluster_interface.static_ip_range_high,
400- alloc_type, requested_address=requested_address)
401+ alloc_type, requested_address=requested_address,
402+ user=user)
403 MACStaticIPAddressLink(mac_address=self, ip_address=new_sip).save()
404 return new_sip
405
406 def claim_static_ips(self, alloc_type=IPADDRESS_TYPE.AUTO,
407- requested_address=None):
408+ requested_address=None, user=None):
409 """Assign static IP addresses to this MAC.
410
411 Allocates one address per managed cluster interface connected to this
412@@ -235,6 +236,8 @@
413 the range defined on some cluster interface to which this
414 MACAddress is related. If given, no allocations will be made on
415 any other cluster interfaces the MAC may be connected to.
416+ :param user: Optional User who will be given ownership of any
417+ `StaticIPAddress`es claimed.
418 :return: A list of :class:`StaticIPAddress`. Returns empty if
419 the cluster_interface is not yet known, or the
420 static_ip_range_low/high values values are not set on the
421@@ -257,9 +260,13 @@
422 # different representations for "none" values in IP addresses.
423 if self.cluster_interface is None:
424 # No known cluster interface. Nothing we can do.
425+ if self.node is not None:
426+ hostname_string = "%s: " % self.node.hostname
427+ else:
428+ hostname_string = ""
429 maaslog.error(
430- "%s: Tried to allocate an IP to MAC %s but its cluster "
431- "interface is not known", self.node.hostname, self)
432+ "%sTried to allocate an IP to MAC %s but its cluster "
433+ "interface is not known", hostname_string, self)
434 return []
435 cluster_interfaces = [
436 interface
437@@ -301,7 +308,7 @@
438 if allocations[interface] is None:
439 # No IP address yet on this cluster interface. Get one.
440 allocations[interface] = self._allocate_static_address(
441- interface, alloc_type, requested_address)
442+ interface, alloc_type, requested_address, user=user)
443
444 # We now have a static IP allocated to each of our cluster interfaces.
445 # Ignore the clashes. Return the ones that have the right type: those
446
447=== modified file 'src/maasserver/models/tests/test_macaddress.py'
448--- src/maasserver/models/tests/test_macaddress.py 2014-11-25 14:23:55 +0000
449+++ src/maasserver/models/tests/test_macaddress.py 2014-11-26 12:14:08 +0000
450@@ -635,6 +635,16 @@
451 [sip] = allocation
452 self.assertEqual(IPAddress(requested_ip), IPAddress(sip.ip))
453
454+ def test__links_static_ip_to_user_if_passed(self):
455+ cluster = factory.make_NodeGroup()
456+ cluster_interface = factory.make_NodeGroupInterface(cluster)
457+ mac_address = factory.make_MACAddress(
458+ cluster_interface=cluster_interface)
459+ user = factory.make_User()
460+ [sip] = mac_address.claim_static_ips(
461+ user=user, alloc_type=IPADDRESS_TYPE.USER_RESERVED)
462+ self.assertEqual(sip.user, user)
463+
464
465 class TestGetClusterInterfaces(MAASServerTestCase):
466 """Tests for `MACAddress.get_cluster_interfaces`."""
467
468=== modified file 'src/maasserver/testing/factory.py'
469--- src/maasserver/testing/factory.py 2014-11-25 14:23:55 +0000
470+++ src/maasserver/testing/factory.py 2014-11-26 12:14:08 +0000
471@@ -461,8 +461,6 @@
472 def make_MACAddress(self, address=None, node=None, networks=None,
473 **kwargs):
474 """Create a `MACAddress` model object."""
475- if node is None:
476- node = self.make_Node()
477 if address is None:
478 address = self.make_mac_address()
479 mac = MACAddress(mac_address=MAC(address), node=node, **kwargs)