IpPools support

Adding support for IP pool create/delete/get actions,
and also allocate & release IPs from the pool

Change-Id: Ieac0aad2268cffa9d4fb5b521ebec268f2b408f3
This commit is contained in:
Adit Sarfaty 2016-12-11 10:14:29 +02:00
parent c52244b8b9
commit ea8eb2a59a
6 changed files with 317 additions and 5 deletions

View File

@ -160,3 +160,23 @@ FAKE_QOS_PROFILE = {
"_create_user": "admin",
"_revision": 0
}
FAKE_IP_POOL_UUID = uuidutils.generate_uuid()
FAKE_IP_POOL = {
"_revision": 0,
"id": FAKE_IP_POOL_UUID,
"display_name": "IPPool-IPV6-1",
"description": "IPPool-IPV6-1 Description",
"resource_type": "IpPool",
"subnets": [{
"dns_nameservers": [
"2002:a70:cbfa:1:1:1:1:1"
],
"allocation_ranges": [{
"start": "2002:a70:cbfa:0:0:0:0:1",
"end": "2002:a70:cbfa:0:0:0:0:5"
}],
"gateway_ip": "2002:a80:cbfa:0:0:0:0:255",
"cidr": "2002:a70:cbfa:0:0:0:0:0/24"
}],
}

View File

@ -540,3 +540,189 @@ class LogicalRouterPortTestCase(nsxlib_testcase.NsxClientTestCase):
'get', lrport,
'https://1.2.3.4/api/v1/logical-router-ports/?'
'logical_switch_id=%s' % switch_id)
class IpPoolTestCase(nsxlib_testcase.NsxClientTestCase):
def _mocked_pool(self, session_response=None):
return self.mocked_resource(
resources.IpPool, session_response=session_response)
def test_create_ip_pool_all_args(self):
"""Test creating an IP pool
returns the correct response and 201 status
"""
pool = self._mocked_pool()
display_name = 'dummy'
gateway_ip = '1.1.1.1'
ranges = [{'start': '2.2.2.0', 'end': '2.2.2.255'},
{'start': '3.2.2.0', 'end': '3.2.2.255'}]
cidr = '2.2.2.0/24'
description = 'desc'
dns_nameserver = '7.7.7.7'
pool.create(cidr, ranges=ranges,
display_name=display_name,
gateway_ip=gateway_ip,
description=description,
dns_nameservers=[dns_nameserver])
data = {
'display_name': display_name,
'description': description,
'subnets': [{
'gateway_ip': gateway_ip,
'allocation_ranges': ranges,
'cidr': cidr,
'dns_nameservers': [dns_nameserver]
}]
}
test_client.assert_json_call(
'post', pool,
'https://1.2.3.4/api/v1/pools/ip-pools',
data=jsonutils.dumps(data, sort_keys=True))
def test_create_ip_pool_minimal_args(self):
pool = self._mocked_pool()
ranges = [{'start': '2.2.2.0', 'end': '2.2.2.255'},
{'start': '3.2.2.0', 'end': '3.2.2.255'}]
cidr = '2.2.2.0/24'
pool.create(cidr, ranges=ranges)
data = {
'subnets': [{
'allocation_ranges': ranges,
'cidr': cidr,
}]
}
test_client.assert_json_call(
'post', pool,
'https://1.2.3.4/api/v1/pools/ip-pools',
data=jsonutils.dumps(data, sort_keys=True))
def test_create_ip_pool_no_ranges_with_gateway(self):
pool = self._mocked_pool()
cidr = '2.2.2.0/30'
gateway_ip = '2.2.2.1'
pool.create(cidr, ranges=None, gateway_ip=gateway_ip)
exp_ranges = [{'start': '2.2.2.0', 'end': '2.2.2.0'},
{'start': '2.2.2.2', 'end': '2.2.2.3'}]
data = {
'subnets': [{
'gateway_ip': gateway_ip,
'allocation_ranges': exp_ranges,
'cidr': cidr,
}]
}
test_client.assert_json_call(
'post', pool,
'https://1.2.3.4/api/v1/pools/ip-pools',
data=jsonutils.dumps(data, sort_keys=True))
def test_create_ip_pool_no_ranges_no_gateway(self):
pool = self._mocked_pool()
cidr = '2.2.2.0/30'
pool.create(cidr, ranges=None)
exp_ranges = [{'start': '2.2.2.0', 'end': '2.2.2.3'}]
data = {
'subnets': [{
'allocation_ranges': exp_ranges,
'cidr': cidr,
}]
}
test_client.assert_json_call(
'post', pool,
'https://1.2.3.4/api/v1/pools/ip-pools',
data=jsonutils.dumps(data, sort_keys=True))
def test_create_ip_pool_no_cidr(self):
pool = self._mocked_pool()
gateway_ip = '1.1.1.1'
ranges = [{'start': '2.2.2.0', 'end': '2.2.2.255'},
{'start': '3.2.2.0', 'end': '3.2.2.255'}]
cidr = None
try:
pool.create(cidr, ranges=ranges,
gateway_ip=gateway_ip)
except exceptions.InvalidInput:
# This call should fail
pass
else:
self.fail("shouldn't happen")
def test_get_ip_pool(self):
"""Test getting a router port by router id"""
fake_ip_pool = test_constants.FAKE_IP_POOL.copy()
resp_resources = fake_ip_pool
pool = self._mocked_pool(
session_response=mocks.MockRequestsResponse(
200, jsonutils.dumps(resp_resources)))
uuid = fake_ip_pool['id']
result = pool.get(uuid)
self.assertEqual(fake_ip_pool, result)
test_client.assert_json_call(
'get', pool,
'https://1.2.3.4/api/v1/pools/ip-pools/%s' % uuid)
def test_delete_ip_pool(self):
"""Test deleting router port"""
pool = self._mocked_pool()
uuid = test_constants.FAKE_IP_POOL['id']
pool.delete(uuid)
test_client.assert_json_call(
'delete', pool,
'https://1.2.3.4/api/v1/pools/ip-pools/%s' % uuid)
def test_allocate_ip_from_pool(self):
pool = self._mocked_pool()
uuid = test_constants.FAKE_IP_POOL['id']
addr = '1.1.1.1'
pool.allocate(uuid, ip_addr=addr)
data = {'allocation_id': addr}
test_client.assert_json_call(
'post', pool,
'https://1.2.3.4/api/v1/pools/ip-pools/%s?action=ALLOCATE' % uuid,
data=jsonutils.dumps(data, sort_keys=True))
def test_release_ip_to_pool(self):
pool = self._mocked_pool()
uuid = test_constants.FAKE_IP_POOL['id']
addr = '1.1.1.1'
pool.release(uuid, addr)
data = {'allocation_id': addr}
test_client.assert_json_call(
'post', pool,
'https://1.2.3.4/api/v1/pools/ip-pools/%s?action=RELEASE' % uuid,
data=jsonutils.dumps(data, sort_keys=True))
def test_get_ip_pool_allocations(self):
"""Test getting a router port by router id"""
fake_ip_pool = test_constants.FAKE_IP_POOL.copy()
resp_resources = fake_ip_pool
pool = self._mocked_pool(
session_response=mocks.MockRequestsResponse(
200, jsonutils.dumps(resp_resources)))
uuid = fake_ip_pool['id']
result = pool.get_allocations(uuid)
self.assertEqual(fake_ip_pool, result)
test_client.assert_json_call(
'get', pool,
'https://1.2.3.4/api/v1/pools/ip-pools/%s/allocations' % uuid)

View File

@ -94,9 +94,11 @@ class RESTClient(object):
def url_post(self, url, body, headers=None):
return self._rest_call(url, method='POST', body=body, headers=headers)
def _raise_error(self, status_code, operation, result_msg):
def _raise_error(self, status_code, operation, result_msg,
error_code=None):
error = ERRORS.get(status_code, DEFAULT_ERROR)
raise error(manager='', operation=operation, details=result_msg)
raise error(manager='', operation=operation, details=result_msg,
error_code=error_code)
def _validate_result(self, result, expected, operation):
if result.status_code not in expected:
@ -109,14 +111,17 @@ class RESTClient(object):
for code in expected]),
'body': result_msg})
error_code = None
if isinstance(result_msg, dict) and 'error_message' in result_msg:
error_code = result_msg.get('error_code')
related_errors = [error['error_message'] for error in
result_msg.get('related_errors', [])]
result_msg = result_msg['error_message']
if related_errors:
result_msg += " relatedErrors: %s" % ' '.join(
related_errors)
self._raise_error(result.status_code, operation, result_msg)
self._raise_error(result.status_code, operation, result_msg,
error_code=error_code)
@classmethod
def merge_headers(cls, *headers):
@ -215,9 +220,11 @@ class NSX3Client(JSONRESTClient):
default_headers=default_headers,
client_obj=client_obj)
def _raise_error(self, status_code, operation, result_msg):
def _raise_error(self, status_code, operation, result_msg,
error_code=None):
"""Override the Rest client errors to add the manager IPs"""
error = ERRORS.get(status_code, DEFAULT_ERROR)
raise error(manager=self.nsx_api_managers,
operation=operation,
details=result_msg)
details=result_msg,
error_code=error_code)

View File

@ -63,6 +63,7 @@ class ManagerError(NsxLibException):
self.msg = self.message % kwargs
except KeyError:
self.msg = details
self.error_code = kwargs.get('error_code')
class ResourceNotFound(ManagerError):
@ -70,6 +71,11 @@ class ResourceNotFound(ManagerError):
"%(operation)s")
class InvalidInput(ManagerError):
message = _("%(operation)s failed: Invalid input %(arg_val)s "
"for %(arg_name)s")
class StaleRevision(ManagerError):
pass

View File

@ -101,3 +101,9 @@ EGRESS = 'egress'
INGRESS = 'ingress'
EGRESS_SHAPING = 'EgressRateShaper'
INGRESS_SHAPING = 'IngressRateShaper'
# Error codes returned by the backend
ERR_CODE_OBJECT_NOT_FOUND = 202
ERR_CODE_IPAM_POOL_EXHAUSTED = 5109
ERR_CODE_IPAM_SPECIFIC_IP = 5123
ERR_CODE_IPAM_IP_NOT_IN_POOL = 5110

View File

@ -15,6 +15,7 @@
#
import abc
import collections
import netaddr
import six
from vmware_nsxlib._i18n import _
@ -575,3 +576,89 @@ class LogicalDhcpServer(AbstractRESTResource):
def delete_binding(self, server_uuid, binding_uuid):
url = "%s/static-bindings/%s" % (server_uuid, binding_uuid)
return self._client.url_delete(url)
class IpPool(AbstractRESTResource):
#TODO(asarfaty): Check the DK api - could be different
@property
def uri_segment(self):
return 'pools/ip-pools'
def _generate_ranges(self, cidr, gateway_ip):
"""Create list of ranges from the given cidr.
Ignore the gateway_ip, if defined
"""
ip_set = netaddr.IPSet(netaddr.IPNetwork(cidr))
if gateway_ip:
ip_set.remove(gateway_ip)
return [{"start": str(r[0]),
"end": str(r[-1])} for r in ip_set.iter_ipranges()]
def create(self, cidr, ranges=None, display_name=None, description=None,
gateway_ip=None, dns_nameservers=None):
"""Create an IpPool.
Arguments:
cidr: (required)
ranges: (optional) a list of dictionaries, each with 'start'
and 'end' keys, and IP values.
If None: the cidr will be used to create the ranges,
excluding the gateway.
display_name: (optional)
description: (optional)
gateway_ip: (optional)
dns_nameservers: (optional) list of addresses
"""
if not cidr:
raise exceptions.InvalidInput(operation="IP Pool create",
arg_name="cidr", arg_val=cidr)
if not ranges:
# generate ranges from (cidr - gateway)
ranges = self._generate_ranges(cidr, gateway_ip)
subnet = {"allocation_ranges": ranges,
"cidr": cidr}
if gateway_ip:
subnet["gateway_ip"] = gateway_ip
if dns_nameservers:
subnet["dns_nameservers"] = dns_nameservers
body = {"subnets": [subnet]}
if description:
body['description'] = description
if display_name:
body['display_name'] = display_name
return self._client.create(body=body)
def delete(self, pool_id):
"""Delete an IPPool by its ID."""
return self._client.delete(pool_id)
def update(self, uuid, *args, **kwargs):
# Not supported yet
pass
def get(self, pool_id):
return self._client.get(pool_id)
def allocate(self, pool_id, ip_addr=None):
"""Allocate an IP from a pool."""
# Note: Currently the backend does not support allocation of a
# specific IP, so an exception will be raised by the backend.
# Depending on the backend version, this may be allowed in the future
url = "%s?action=ALLOCATE" % pool_id
body = {"allocation_id": ip_addr}
return self._client.url_post(url, body=body)
def release(self, pool_id, ip_addr):
"""Release an IP back to a pool."""
url = "%s?action=RELEASE" % pool_id
body = {"allocation_id": ip_addr}
return self._client.url_post(url, body=body)
def get_allocations(self, pool_id):
"""Return information about the allocated IPs in the pool."""
url = "%s/allocations" % pool_id
return self._client.url_get(url)