Merge lp:~julian-edwards/maas/optional-ip-api 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: 2650
Proposed branch: lp:~julian-edwards/maas/optional-ip-api
Merge into: lp:~maas-committers/maas/trunk
Prerequisite: lp:~julian-edwards/maas/optional-ip
Diff against target: 176 lines (+85/-5)
5 files modified
src/maasserver/api.py (+9/-2)
src/maasserver/exceptions.py (+1/-1)
src/maasserver/models/macaddress.py (+11/-2)
src/maasserver/models/tests/test_macaddress.py (+6/-0)
src/maasserver/tests/test_api_node.py (+58/-0)
To merge this branch: bzr merge lp:~julian-edwards/maas/optional-ip-api
Reviewer Review Type Date Requested Status
Graham Binns (community) Approve
Review via email: mp+229458@code.launchpad.net

Commit message

Add functionality to the API to request specific IPs for STICKY addresses on nodes.

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

Couple of comments, no blockers. Nice work :)

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

Thanks for the review! (this and the other)

On Monday 04 Aug 2014 15:21:50 you wrote:
> If claim_static_ip_address is mapped to 'claim_sticky_ip_address' as far as
> the world is concerned, we should do a rename of static_ip -> sticky_ip for
> the sake of not having another Cluster/NodeGroup-ish situation. That can be
> a tech-debt bug for now, of course.

Sticky is a *type* of static IP. See IPADDRESS_TYPE.

The API call is called claim_sticky_ip_address because it's node-specific and
only works with sticky types.

I have no inclination to rename static IP at all and will resist it unless
someone comes up with some better arguments.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api.py'
2--- src/maasserver/api.py 2014-08-04 14:29:05 +0000
3+++ src/maasserver/api.py 2014-08-04 15:55:26 +0000
4@@ -553,6 +553,10 @@
5 :param mac_address: Optional MAC address on the node on which to
6 assign the sticky IP address. If not passed, defaults to the
7 primary MAC for the node.
8+ :param requested_address: Optional IP address to claim. Must be in
9+ the range defined on the cluster interface to which the context
10+ MAC is related, or 403 Forbidden is returned. If the requested
11+ address is unavailable for use, 404 Not Found is returned.
12
13 A sticky IP is one which stays with the node until the IP is
14 disassociated with the node, or the node is deleted. It allows
15@@ -575,9 +579,12 @@
16 except MACAddress.DoesNotExist:
17 raise MAASAPIBadRequest(
18 "mac_address %s not found on the node" % raw_mac)
19- sip = mac_address.claim_static_ip(alloc_type=IPADDRESS_TYPE.STICKY)
20+ requested_address = request.POST.get('requested_address', None)
21+ sticky_ip = mac_address.claim_static_ip(
22+ alloc_type=IPADDRESS_TYPE.STICKY,
23+ requested_address=requested_address)
24 maaslog.info(
25- "%s: Sticky IP address %s allocated", node.hostname, sip.ip)
26+ "%s: Sticky IP address %s allocated", node.hostname, sticky_ip.ip)
27 return node
28
29 @operation(idempotent=False)
30
31=== modified file 'src/maasserver/exceptions.py'
32--- src/maasserver/exceptions.py 2014-08-04 15:55:26 +0000
33+++ src/maasserver/exceptions.py 2014-08-04 15:55:26 +0000
34@@ -127,7 +127,7 @@
35 api_error = httplib.NOT_FOUND
36
37
38-class StaticIPAddressOutOfRange(MAASException):
39+class StaticIPAddressOutOfRange(MAASAPIException):
40 """Raised when a requested IP is not in an acceptable range."""
41 api_error = httplib.FORBIDDEN
42
43
44=== modified file 'src/maasserver/models/macaddress.py'
45--- src/maasserver/models/macaddress.py 2014-07-31 10:56:18 +0000
46+++ src/maasserver/models/macaddress.py 2014-08-04 15:55:26 +0000
47@@ -89,7 +89,8 @@
48 """Return networks to which this MAC is connected, sorted by name."""
49 return self.networks.all().order_by('name')
50
51- def claim_static_ip(self, alloc_type=IPADDRESS_TYPE.AUTO):
52+ def claim_static_ip(self, alloc_type=IPADDRESS_TYPE.AUTO,
53+ requested_address=None):
54 """Assign a static IP to this MAC.
55
56 It is the caller's responsibility to create a celery Task that will
57@@ -99,6 +100,9 @@
58
59 :param alloc_type: See :class:`StaticIPAddress`.alloc_type.
60 This parameter musn't be IPADDRESS_TYPE.USER_RESERVED.
61+ :param requested_address: Optional IP address to claim. Must be in
62+ the range defined on the cluster interface to which this MACAddress
63+ is related.
64 :return: A :class:`StaticIPAddress` object. Returns None if
65 the cluster_interface is not yet known, or the
66 static_ip_range_low/high values values are not set on the
67@@ -107,6 +111,10 @@
68 :raises: StaticIPAddressExhaustion if there are not enough IPs left.
69 :raises: StaticIPAddressTypeClash if an IP already exists with a
70 different type.
71+ :raises: StaticIPAddressOutOfRange if the requested_address is not in
72+ the cluster interface's defined range.
73+ :raises: StaticIPAddressUnavailable if the requested_address is already
74+ allocated.
75 """
76 # Avoid circular import.
77 from maasserver.models.staticipaddress import StaticIPAddress
78@@ -141,6 +149,7 @@
79 MACStaticIPAddressLink,
80 StaticIPAddress,
81 )
82- sip = StaticIPAddress.objects.allocate_new(low, high, alloc_type)
83+ sip = StaticIPAddress.objects.allocate_new(
84+ low, high, alloc_type, requested_address=requested_address)
85 MACStaticIPAddressLink(mac_address=self, ip_address=sip).save()
86 return sip
87
88=== modified file 'src/maasserver/models/tests/test_macaddress.py'
89--- src/maasserver/models/tests/test_macaddress.py 2014-07-16 12:33:31 +0000
90+++ src/maasserver/models/tests/test_macaddress.py 2014-08-04 15:55:26 +0000
91@@ -131,3 +131,9 @@
92 ip = mac.claim_static_ip(alloc_type=iptype)
93 self.assertEqual(
94 ip, mac.claim_static_ip(alloc_type=iptype))
95+
96+ def test_passes_requested_ip(self):
97+ node = factory.make_node_with_mac_attached_to_nodegroupinterface()
98+ mac = node.get_primary_mac()
99+ ip = node.get_primary_mac().cluster_interface.static_ip_range_high
100+ self.assertEqual(ip, mac.claim_static_ip(requested_address=ip).ip)
101
102=== modified file 'src/maasserver/tests/test_api_node.py'
103--- src/maasserver/tests/test_api_node.py 2014-07-31 11:47:10 +0000
104+++ src/maasserver/tests/test_api_node.py 2014-08-04 15:55:26 +0000
105@@ -18,6 +18,7 @@
106 from cStringIO import StringIO
107 import httplib
108 import json
109+from netaddr import IPAddress
110 import sys
111
112 import bson
113@@ -1040,6 +1041,63 @@
114 "mac_address %s not found on the node" % random_mac,
115 response.content)
116
117+ def test_claim_sticky_ip_allows_requested_ip(self):
118+ self.become_admin()
119+ node = factory.make_node_with_mac_attached_to_nodegroupinterface()
120+ ngi = node.get_primary_mac().cluster_interface
121+ requested_address = ngi.static_ip_range_low
122+
123+ response = self.client.post(
124+ self.get_node_uri(node),
125+ {
126+ 'op': 'claim_sticky_ip_address',
127+ 'requested_address': requested_address,
128+ })
129+ self.assertEqual(httplib.OK, response.status_code, response.content)
130+ [observed_static_ip] = StaticIPAddress.objects.all()
131+ self.assertEqual(observed_static_ip.ip, requested_address)
132+
133+ def test_claim_sticky_ip_address_detects_out_of_range_requested_ip(self):
134+ self.become_admin()
135+ node = factory.make_node_with_mac_attached_to_nodegroupinterface()
136+ ngi = node.get_primary_mac().cluster_interface
137+ requested_address = IPAddress(ngi.static_ip_range_low) - 1
138+
139+ response = self.client.post(
140+ self.get_node_uri(node),
141+ {
142+ 'op': 'claim_sticky_ip_address',
143+ 'requested_address': requested_address.format(),
144+ })
145+ self.assertEqual(
146+ httplib.FORBIDDEN, response.status_code, response.content)
147+
148+ def test_claim_sticky_ip_address_detects_unavailable_requested_ip(self):
149+ self.become_admin()
150+ # Create 2 nodes on the same nodegroup and interface.
151+ node = factory.make_node_with_mac_attached_to_nodegroupinterface()
152+ ngi = node.get_primary_mac().cluster_interface
153+ other_node = factory.make_node(mac=True, nodegroup=ngi.nodegroup)
154+ other_mac = other_node.get_primary_mac()
155+ other_mac.cluster_interface = ngi
156+ other_mac.save()
157+
158+ # Allocate an IP to one of the nodes.
159+ requested_address = IPAddress(ngi.static_ip_range_low) + 1
160+ requested_address = requested_address.format()
161+ other_node.get_primary_mac().claim_static_ip(
162+ requested_address=requested_address)
163+
164+ # Use the API to try to duplicate the same IP on the other node.
165+ response = self.client.post(
166+ self.get_node_uri(node),
167+ {
168+ 'op': 'claim_sticky_ip_address',
169+ 'requested_address': requested_address,
170+ })
171+ self.assertEqual(
172+ httplib.NOT_FOUND, response.status_code, response.content)
173+
174
175 class TestGetDetails(APITestCase):
176 """Tests for /api/1.0/nodes/<node>/?op=details."""