Merge lp:~julian-edwards/maas/request-user-ip into lp:~maas-committers/maas/trunk

Proposed by Julian Edwards
Status: Merged
Approved by: Julian Edwards
Approved revision: no longer in the source branch.
Merged at revision: 2953
Proposed branch: lp:~julian-edwards/maas/request-user-ip
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 145 lines (+57/-5)
2 files modified
src/maasserver/api/ip_addresses.py (+18/-4)
src/maasserver/api/tests/test_ipaddresses.py (+39/-1)
To merge this branch: bzr merge lp:~julian-edwards/maas/request-user-ip
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+234248@code.launchpad.net

Commit message

Allow API users to optionally request specific IP addresses when requesting USER_RESERVED static IP addresses. Previously a random one was always allocated.

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

I can see why you might return an HTTP status code of Forbidden for an address that wasn't in the right range, but Not Found for an address that's already taken seems strange. Isn't this a classic case for the Conflict error?

review: Approve
Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Thursday 11 Sep 2014 05:42:49 you wrote:
> Review: Approve
>
> I can see why you might return an HTTP status code of Forbidden for an
> address that wasn't in the right range, but Not Found for an address that's
> already taken seems strange. Isn't this a classic case for the Conflict
> error?

I am following what I did in the similar change in the claim_sticky_ip stuff.
There is a good reason, conflict is wrong IMO. If I can find the original MP
I'll show you later.

Thanks for the review!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/maasserver/api/ip_addresses.py'
--- src/maasserver/api/ip_addresses.py 2014-08-19 02:14:31 +0000
+++ src/maasserver/api/ip_addresses.py 2014-09-11 03:26:49 +0000
@@ -21,7 +21,10 @@
21 operation,21 operation,
22 OperationsHandler,22 OperationsHandler,
23 )23 )
24from maasserver.api.utils import get_mandatory_param24from maasserver.api.utils import (
25 get_mandatory_param,
26 get_optional_param,
27 )
25from maasserver.enum import (28from maasserver.enum import (
26 IPADDRESS_TYPE,29 IPADDRESS_TYPE,
27 NODEGROUP_STATUS,30 NODEGROUP_STATUS,
@@ -32,6 +35,10 @@
32 StaticIPAddress,35 StaticIPAddress,
33 )36 )
34import netaddr37import netaddr
38from provisioningserver.logger import get_maas_logger
39
40
41maaslog = get_maas_logger("ip_addresses")
3542
3643
37class IPAddressesHandler(OperationsHandler):44class IPAddressesHandler(OperationsHandler):
@@ -46,17 +53,20 @@
46 def resource_uri(cls, *args, **kwargs):53 def resource_uri(cls, *args, **kwargs):
47 return ('ipaddresses_handler', [])54 return ('ipaddresses_handler', [])
4855
49 def claim_ip(self, user, interface):56 def claim_ip(self, user, interface, requested_address):
50 """Attempt to get a USER_RESERVED StaticIPAddress for `user` on57 """Attempt to get a USER_RESERVED StaticIPAddress for `user` on
51 `interface`.58 `interface`.
5259
53 :raises StaticIPAddressExhaustion: If no IPs available.60 :raises StaticIPAddressExhaustion: If no IPs available.
54 """61 """
55 return StaticIPAddress.objects.allocate_new(62 sip = StaticIPAddress.objects.allocate_new(
56 range_low=interface.static_ip_range_low,63 range_low=interface.static_ip_range_low,
57 range_high=interface.static_ip_range_high,64 range_high=interface.static_ip_range_high,
58 alloc_type=IPADDRESS_TYPE.USER_RESERVED,65 alloc_type=IPADDRESS_TYPE.USER_RESERVED,
66 requested_address=requested_address,
59 user=user)67 user=user)
68 maaslog.info("User %s was allocated IP %s", user.username, sip.ip)
69 return sip
6070
61 @operation(idempotent=False)71 @operation(idempotent=False)
62 def reserve(self, request):72 def reserve(self, request):
@@ -71,6 +81,8 @@
71 :type network: unicode81 :type network: unicode
72 """82 """
73 network = get_mandatory_param(request.POST, "network")83 network = get_mandatory_param(request.POST, "network")
84 requested_address = get_optional_param(
85 request.POST, "requested_address")
74 # Validate the passed network.86 # Validate the passed network.
75 try:87 try:
76 valid_network = netaddr.IPNetwork(network)88 valid_network = netaddr.IPNetwork(network)
@@ -87,7 +99,8 @@
87 for interface in interfaces:99 for interface in interfaces:
88 if valid_network == interface.network:100 if valid_network == interface.network:
89 # Winner winner chicken dinner.101 # Winner winner chicken dinner.
90 return self.claim_ip(request.user, interface)102 return self.claim_ip(
103 request.user, interface, requested_address)
91 raise MAASAPIBadRequest("No network found matching %s" % network)104 raise MAASAPIBadRequest("No network found matching %s" % network)
92105
93 @operation(idempotent=False)106 @operation(idempotent=False)
@@ -101,6 +114,7 @@
101 staticaddress = get_object_or_404(114 staticaddress = get_object_or_404(
102 StaticIPAddress, ip=ip, user=request.user)115 StaticIPAddress, ip=ip, user=request.user)
103 staticaddress.deallocate()116 staticaddress.deallocate()
117 maaslog.info("User %s released IP %s", request.user.username, ip)
104118
105 def read(self, request):119 def read(self, request):
106 """List IPAddresses.120 """List IPAddresses.
107121
=== modified file 'src/maasserver/api/tests/test_ipaddresses.py'
--- src/maasserver/api/tests/test_ipaddresses.py 2014-09-05 16:38:32 +0000
+++ src/maasserver/api/tests/test_ipaddresses.py 2014-09-11 03:26:49 +0000
@@ -26,6 +26,8 @@
26from maasserver.testing.api import APITestCase26from maasserver.testing.api import APITestCase
27from maasserver.testing.factory import factory27from maasserver.testing.factory import factory
28from maasserver.testing.orm import reload_object28from maasserver.testing.orm import reload_object
29from netaddr import IPAddress
30from testtools.matchers import Equals
2931
3032
31class TestNetworksAPI(APITestCase):33class TestNetworksAPI(APITestCase):
@@ -34,11 +36,13 @@
34 cluster = factory.make_NodeGroup(status=status, **kwargs)36 cluster = factory.make_NodeGroup(status=status, **kwargs)
35 return factory.make_NodeGroupInterface(cluster)37 return factory.make_NodeGroupInterface(cluster)
3638
37 def post_reservation_request(self, net):39 def post_reservation_request(self, net, requested_address=None):
38 params = {40 params = {
39 'op': 'reserve',41 'op': 'reserve',
40 'network': unicode(net),42 'network': unicode(net),
41 }43 }
44 if requested_address is not None:
45 params["requested_address"] = requested_address
42 return self.client.post(reverse('ipaddresses_handler'), params)46 return self.client.post(reverse('ipaddresses_handler'), params)
4347
44 def post_release_request(self, ip):48 def post_release_request(self, ip):
@@ -103,6 +107,40 @@
103 "Invalid network parameter %s" % net,107 "Invalid network parameter %s" % net,
104 response.content)108 response.content)
105109
110 def test_POST_reserve_creates_requested_address(self):
111 interface = self.make_interface()
112 net = interface.network
113 ip_in_network = interface.static_ip_range_low
114 response = self.post_reservation_request(net, ip_in_network)
115 self.assertEqual(httplib.OK, response.status_code, response.content)
116 returned_address = json.loads(response.content)
117 [staticipaddress] = StaticIPAddress.objects.all()
118 self.expectThat(
119 returned_address["alloc_type"],
120 Equals(IPADDRESS_TYPE.USER_RESERVED))
121 self.expectThat(returned_address["ip"], Equals(ip_in_network))
122 self.expectThat(staticipaddress.ip, Equals(ip_in_network))
123
124 def test_POST_reserve_requested_address_detects_in_use_address(self):
125 interface = self.make_interface()
126 net = interface.network
127 ip_in_network = interface.static_ip_range_low
128 response = self.post_reservation_request(net, ip_in_network)
129 self.assertEqual(httplib.OK, response.status_code, response.content)
130 # Do same request again and check it is rejected.
131 response = self.post_reservation_request(net, ip_in_network)
132 self.assertEqual(
133 httplib.NOT_FOUND, response.status_code, response.content)
134
135 def test_POST_reserve_requested_address_detects_out_of_range_addr(self):
136 interface = self.make_interface()
137 net = interface.network
138 ip_not_in_network = unicode(
139 IPAddress(interface.static_ip_range_low) - 1)
140 response = self.post_reservation_request(net, ip_not_in_network)
141 self.assertEqual(
142 httplib.FORBIDDEN, response.status_code, response.content)
143
106 def test_GET_returns_ipaddresses(self):144 def test_GET_returns_ipaddresses(self):
107 original_ipaddress = factory.make_StaticIPAddress(145 original_ipaddress = factory.make_StaticIPAddress(
108 user=self.logged_in_user)146 user=self.logged_in_user)