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
=== modified file 'src/maasserver/api.py'
--- src/maasserver/api.py 2014-08-04 14:29:05 +0000
+++ src/maasserver/api.py 2014-08-04 15:55:26 +0000
@@ -553,6 +553,10 @@
553 :param mac_address: Optional MAC address on the node on which to553 :param mac_address: Optional MAC address on the node on which to
554 assign the sticky IP address. If not passed, defaults to the554 assign the sticky IP address. If not passed, defaults to the
555 primary MAC for the node.555 primary MAC for the node.
556 :param requested_address: Optional IP address to claim. Must be in
557 the range defined on the cluster interface to which the context
558 MAC is related, or 403 Forbidden is returned. If the requested
559 address is unavailable for use, 404 Not Found is returned.
556560
557 A sticky IP is one which stays with the node until the IP is561 A sticky IP is one which stays with the node until the IP is
558 disassociated with the node, or the node is deleted. It allows562 disassociated with the node, or the node is deleted. It allows
@@ -575,9 +579,12 @@
575 except MACAddress.DoesNotExist:579 except MACAddress.DoesNotExist:
576 raise MAASAPIBadRequest(580 raise MAASAPIBadRequest(
577 "mac_address %s not found on the node" % raw_mac)581 "mac_address %s not found on the node" % raw_mac)
578 sip = mac_address.claim_static_ip(alloc_type=IPADDRESS_TYPE.STICKY)582 requested_address = request.POST.get('requested_address', None)
583 sticky_ip = mac_address.claim_static_ip(
584 alloc_type=IPADDRESS_TYPE.STICKY,
585 requested_address=requested_address)
579 maaslog.info(586 maaslog.info(
580 "%s: Sticky IP address %s allocated", node.hostname, sip.ip)587 "%s: Sticky IP address %s allocated", node.hostname, sticky_ip.ip)
581 return node588 return node
582589
583 @operation(idempotent=False)590 @operation(idempotent=False)
584591
=== modified file 'src/maasserver/exceptions.py'
--- src/maasserver/exceptions.py 2014-08-04 15:55:26 +0000
+++ src/maasserver/exceptions.py 2014-08-04 15:55:26 +0000
@@ -127,7 +127,7 @@
127 api_error = httplib.NOT_FOUND127 api_error = httplib.NOT_FOUND
128128
129129
130class StaticIPAddressOutOfRange(MAASException):130class StaticIPAddressOutOfRange(MAASAPIException):
131 """Raised when a requested IP is not in an acceptable range."""131 """Raised when a requested IP is not in an acceptable range."""
132 api_error = httplib.FORBIDDEN132 api_error = httplib.FORBIDDEN
133133
134134
=== modified file 'src/maasserver/models/macaddress.py'
--- src/maasserver/models/macaddress.py 2014-07-31 10:56:18 +0000
+++ src/maasserver/models/macaddress.py 2014-08-04 15:55:26 +0000
@@ -89,7 +89,8 @@
89 """Return networks to which this MAC is connected, sorted by name."""89 """Return networks to which this MAC is connected, sorted by name."""
90 return self.networks.all().order_by('name')90 return self.networks.all().order_by('name')
9191
92 def claim_static_ip(self, alloc_type=IPADDRESS_TYPE.AUTO):92 def claim_static_ip(self, alloc_type=IPADDRESS_TYPE.AUTO,
93 requested_address=None):
93 """Assign a static IP to this MAC.94 """Assign a static IP to this MAC.
9495
95 It is the caller's responsibility to create a celery Task that will96 It is the caller's responsibility to create a celery Task that will
@@ -99,6 +100,9 @@
99100
100 :param alloc_type: See :class:`StaticIPAddress`.alloc_type.101 :param alloc_type: See :class:`StaticIPAddress`.alloc_type.
101 This parameter musn't be IPADDRESS_TYPE.USER_RESERVED.102 This parameter musn't be IPADDRESS_TYPE.USER_RESERVED.
103 :param requested_address: Optional IP address to claim. Must be in
104 the range defined on the cluster interface to which this MACAddress
105 is related.
102 :return: A :class:`StaticIPAddress` object. Returns None if106 :return: A :class:`StaticIPAddress` object. Returns None if
103 the cluster_interface is not yet known, or the107 the cluster_interface is not yet known, or the
104 static_ip_range_low/high values values are not set on the108 static_ip_range_low/high values values are not set on the
@@ -107,6 +111,10 @@
107 :raises: StaticIPAddressExhaustion if there are not enough IPs left.111 :raises: StaticIPAddressExhaustion if there are not enough IPs left.
108 :raises: StaticIPAddressTypeClash if an IP already exists with a112 :raises: StaticIPAddressTypeClash if an IP already exists with a
109 different type.113 different type.
114 :raises: StaticIPAddressOutOfRange if the requested_address is not in
115 the cluster interface's defined range.
116 :raises: StaticIPAddressUnavailable if the requested_address is already
117 allocated.
110 """118 """
111 # Avoid circular import.119 # Avoid circular import.
112 from maasserver.models.staticipaddress import StaticIPAddress120 from maasserver.models.staticipaddress import StaticIPAddress
@@ -141,6 +149,7 @@
141 MACStaticIPAddressLink,149 MACStaticIPAddressLink,
142 StaticIPAddress,150 StaticIPAddress,
143 )151 )
144 sip = StaticIPAddress.objects.allocate_new(low, high, alloc_type)152 sip = StaticIPAddress.objects.allocate_new(
153 low, high, alloc_type, requested_address=requested_address)
145 MACStaticIPAddressLink(mac_address=self, ip_address=sip).save()154 MACStaticIPAddressLink(mac_address=self, ip_address=sip).save()
146 return sip155 return sip
147156
=== modified file 'src/maasserver/models/tests/test_macaddress.py'
--- src/maasserver/models/tests/test_macaddress.py 2014-07-16 12:33:31 +0000
+++ src/maasserver/models/tests/test_macaddress.py 2014-08-04 15:55:26 +0000
@@ -131,3 +131,9 @@
131 ip = mac.claim_static_ip(alloc_type=iptype)131 ip = mac.claim_static_ip(alloc_type=iptype)
132 self.assertEqual(132 self.assertEqual(
133 ip, mac.claim_static_ip(alloc_type=iptype))133 ip, mac.claim_static_ip(alloc_type=iptype))
134
135 def test_passes_requested_ip(self):
136 node = factory.make_node_with_mac_attached_to_nodegroupinterface()
137 mac = node.get_primary_mac()
138 ip = node.get_primary_mac().cluster_interface.static_ip_range_high
139 self.assertEqual(ip, mac.claim_static_ip(requested_address=ip).ip)
134140
=== modified file 'src/maasserver/tests/test_api_node.py'
--- src/maasserver/tests/test_api_node.py 2014-07-31 11:47:10 +0000
+++ src/maasserver/tests/test_api_node.py 2014-08-04 15:55:26 +0000
@@ -18,6 +18,7 @@
18from cStringIO import StringIO18from cStringIO import StringIO
19import httplib19import httplib
20import json20import json
21from netaddr import IPAddress
21import sys22import sys
2223
23import bson24import bson
@@ -1040,6 +1041,63 @@
1040 "mac_address %s not found on the node" % random_mac,1041 "mac_address %s not found on the node" % random_mac,
1041 response.content)1042 response.content)
10421043
1044 def test_claim_sticky_ip_allows_requested_ip(self):
1045 self.become_admin()
1046 node = factory.make_node_with_mac_attached_to_nodegroupinterface()
1047 ngi = node.get_primary_mac().cluster_interface
1048 requested_address = ngi.static_ip_range_low
1049
1050 response = self.client.post(
1051 self.get_node_uri(node),
1052 {
1053 'op': 'claim_sticky_ip_address',
1054 'requested_address': requested_address,
1055 })
1056 self.assertEqual(httplib.OK, response.status_code, response.content)
1057 [observed_static_ip] = StaticIPAddress.objects.all()
1058 self.assertEqual(observed_static_ip.ip, requested_address)
1059
1060 def test_claim_sticky_ip_address_detects_out_of_range_requested_ip(self):
1061 self.become_admin()
1062 node = factory.make_node_with_mac_attached_to_nodegroupinterface()
1063 ngi = node.get_primary_mac().cluster_interface
1064 requested_address = IPAddress(ngi.static_ip_range_low) - 1
1065
1066 response = self.client.post(
1067 self.get_node_uri(node),
1068 {
1069 'op': 'claim_sticky_ip_address',
1070 'requested_address': requested_address.format(),
1071 })
1072 self.assertEqual(
1073 httplib.FORBIDDEN, response.status_code, response.content)
1074
1075 def test_claim_sticky_ip_address_detects_unavailable_requested_ip(self):
1076 self.become_admin()
1077 # Create 2 nodes on the same nodegroup and interface.
1078 node = factory.make_node_with_mac_attached_to_nodegroupinterface()
1079 ngi = node.get_primary_mac().cluster_interface
1080 other_node = factory.make_node(mac=True, nodegroup=ngi.nodegroup)
1081 other_mac = other_node.get_primary_mac()
1082 other_mac.cluster_interface = ngi
1083 other_mac.save()
1084
1085 # Allocate an IP to one of the nodes.
1086 requested_address = IPAddress(ngi.static_ip_range_low) + 1
1087 requested_address = requested_address.format()
1088 other_node.get_primary_mac().claim_static_ip(
1089 requested_address=requested_address)
1090
1091 # Use the API to try to duplicate the same IP on the other node.
1092 response = self.client.post(
1093 self.get_node_uri(node),
1094 {
1095 'op': 'claim_sticky_ip_address',
1096 'requested_address': requested_address,
1097 })
1098 self.assertEqual(
1099 httplib.NOT_FOUND, response.status_code, response.content)
1100
10431101
1044class TestGetDetails(APITestCase):1102class TestGetDetails(APITestCase):
1045 """Tests for /api/1.0/nodes/<node>/?op=details."""1103 """Tests for /api/1.0/nodes/<node>/?op=details."""