Merge lp:~gmb/maas/add-api-reserve-sip-for-mac-bug-1387239 into lp:~maas-committers/maas/trunk
- add-api-reserve-sip-for-mac-bug-1387239
- Merge into trunk
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 |
Related bugs: |
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 StaticIPAddress
>>> claim_ip(
123.123.0.1
>>> claim_ip(
123.123.0.1
Which is confusing.
Graham Binns (gmb) wrote : | # |
On 26 November 2014 at 11:09, Gavin Panella <email address hidden>
wrote:
> Review: Approve
>
> LGTM.
>
> Diff comments:
>
> > === modified file 'src/maasserver
> > --- src/maasserver/
> > +++ src/maasserver/
> > @@ -16,6 +16,7 @@
> > 'IPAddressesHan
> > ]
> >
> > +from django.db import transaction
> > from django.shortcuts import get_object_or_404
> > from maasserver.
> > operation,
> > @@ -25,15 +26,23 @@
> > get_mandatory_
> > get_optional_param,
> > )
> > +from maasserver.
> > + remove_host_maps,
> > + update_host_maps,
> > + )
> > from maasserver.enum import (
> > IPADDRESS_TYPE,
> > NODEGROUP_STATUS,
> > )
> > -from maasserver.
> > +from maasserver.
> > + MAASAPIBadRequest,
> > + StaticIPAlready
> > + )
> > from maasserver.models import (
> > NodeGroupInterface,
> > StaticIPAddress,
> > )
> > +from maasserver.
> > import netaddr
> > from provisioningser
> >
> > @@ -53,19 +62,53 @@
> > def resource_uri(cls, *args, **kwargs):
> > return ('ipaddresses_
> >
> > - 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 StaticIPAddress
> > """
> > - sip = StaticIPAddress
> > - range_low=
> > - range_high=
> > - alloc_type=
> > - requested_
> > - 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.
> > + mac_address=mac, cluster_
> > + ips_on_interface = (
> > + addr.ip for addr in mac_address.
> > + if netaddr.
> > + if any(ips_
> > + # 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 StaticIPAlready
> > + "MAC address %s already has the IP address(es) %s."
> %
> > + (mac, ips_on_interface))
> > +
> > + [sip] = mac_address.
> > + alloc_type=
Preview Diff
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) |
LGTM.