Admin network address pool robustness
Normally, a platform network's address pool is created at bootstrap and is unable to be deleted after the initial configuration is complete. An exception to this is the admin network / address pool which is able to be deleted and re-configured by a user at any time. This facilitates the ability for a subcloud to be updated or re-homed to adapt to network changes that may occur between itself and the desired system controller. Currently, most platform network address pools are created at bootstrap by only specifying the range of addresses based on the start-end addresses. When the address pool is associated with a network, the appropriate floating, controller-0, and controller-1 addresses are allocated. In the case of the admin network, a user may decide to delete the network/address-pool and re-create it with the (valid) specification of a floating-address, controller-0-address, and controller-1-address. ie: system addrpool-add \ --floating-address 192.168.103.2 \ --controller0-address 192.168.103.3 \ --controller1-address 192.168.103.4 \ --gateway-address 192.168.103.1 \ admin 192.168.103.0 24 Currently, this would cause the floating IP to be allocated as 192.168.103.5, controller-0 IP to be allocated as 192.168.103.6 and controller-1 IP to be allocated as 192.168.103.7 when the address pool is associated with a network. This would not be what the user expects. In addition, it was found that if a user does use the options for floating-ip, controller0/1 ip, the current code has some significant bugs: 1. Possible to have mixed IPv4/IPv6 addresses 2. Possible to specify floating/controller addresses as the network (subnet) address or the broadcast address 3. Possible to specify the floating/controller addresses completely outside the specified subnet. This commit ensures that any user specification for the floating or controller addresses are preferred over auto-allocation. Test Plan: The addition of the addrpool unit tests provide over 40 new cases across IPv4/IPv6 Pass: Bootstrap a subcloud using the admin network. Pass: On the subcloud, delete the admin addrpool and re-create it with various combinations of subnet/floating/controller-0/1 IP. These combos are also covered by the unit tests. Story: 2010319 Task: 46910 Change-Id: Ie59c26ae2a57b9cb570c7b20e3b40aa0f14fd95d
This commit is contained in:
parent
54b40751fb
commit
c5a84464c8
|
@ -338,6 +338,12 @@ class AddressPoolController(rest.RestController):
|
|||
self._check_valid_range(network, start, end, ipset)
|
||||
ipset.update(netaddr.IPRange(start, end))
|
||||
|
||||
def _check_valid_address(self, addrpool_dict, address):
|
||||
subnet = netaddr.IPNetwork(
|
||||
addrpool_dict['network'] + "/" + str(addrpool_dict['prefix']))
|
||||
addr = netaddr.IPAddress(address)
|
||||
utils.is_valid_address_within_subnet(addr, subnet)
|
||||
|
||||
def _check_pool_readonly(self, addrpool):
|
||||
# The admin and system controller address pools which exist on the
|
||||
# subcloud are expected for re-home a subcloud to new system controllers.
|
||||
|
@ -487,18 +493,22 @@ class AddressPoolController(rest.RestController):
|
|||
|
||||
# Create addresses if specified
|
||||
if floating_address:
|
||||
self._check_valid_address(addrpool_dict, floating_address)
|
||||
f_addr = self._address_create(addrpool_dict, floating_address)
|
||||
addrpool_dict[ADDRPOOL_FLOATING_ADDRESS_ID] = f_addr.id
|
||||
|
||||
if controller0_address:
|
||||
self._check_valid_address(addrpool_dict, controller0_address)
|
||||
c0_addr = self._address_create(addrpool_dict, controller0_address)
|
||||
addrpool_dict[ADDRPOOL_CONTROLLER0_ADDRESS_ID] = c0_addr.id
|
||||
|
||||
if controller1_address:
|
||||
self._check_valid_address(addrpool_dict, controller1_address)
|
||||
c1_addr = self._address_create(addrpool_dict, controller1_address)
|
||||
addrpool_dict[ADDRPOOL_CONTROLLER1_ADDRESS_ID] = c1_addr.id
|
||||
|
||||
if gateway_address:
|
||||
self._check_valid_address(addrpool_dict, gateway_address)
|
||||
g_addr = self._address_create(addrpool_dict, gateway_address)
|
||||
addrpool_dict[ADDRPOOL_GATEWAY_ADDRESS_ID] = g_addr.id
|
||||
|
||||
|
|
|
@ -163,7 +163,8 @@ class InterfaceNetworkController(rest.RestController):
|
|||
# Update address mode based on network type
|
||||
if network_type in [constants.NETWORK_TYPE_MGMT,
|
||||
constants.NETWORK_TYPE_OAM,
|
||||
constants.NETWORK_TYPE_CLUSTER_HOST]:
|
||||
constants.NETWORK_TYPE_CLUSTER_HOST,
|
||||
constants.NETWORK_TYPE_ADMIN]:
|
||||
pool_uuid = pecan.request.dbapi.network_get_by_type(network_type).pool_uuid
|
||||
pool = pecan.request.dbapi.address_pool_get(pool_uuid)
|
||||
if pool.family == constants.IPV4_FAMILY:
|
||||
|
|
|
@ -221,10 +221,24 @@ class NetworkController(rest.RestController):
|
|||
return addresses
|
||||
|
||||
def _create_admin_network_address(self, pool):
|
||||
addresses = collections.OrderedDict()
|
||||
addresses[constants.CONTROLLER_HOSTNAME] = None
|
||||
addresses[constants.CONTROLLER_0_HOSTNAME] = None
|
||||
addresses[constants.CONTROLLER_1_HOSTNAME] = None
|
||||
addresses = {}
|
||||
if pool.floating_address:
|
||||
addresses.update(
|
||||
{constants.CONTROLLER_HOSTNAME: pool.floating_address})
|
||||
else:
|
||||
addresses.update({constants.CONTROLLER_HOSTNAME: None})
|
||||
|
||||
if pool.controller0_address:
|
||||
addresses.update(
|
||||
{constants.CONTROLLER_0_HOSTNAME: pool.controller0_address})
|
||||
else:
|
||||
addresses.update({constants.CONTROLLER_0_HOSTNAME: None})
|
||||
|
||||
if pool.controller1_address:
|
||||
addresses.update(
|
||||
{constants.CONTROLLER_1_HOSTNAME: pool.controller1_address})
|
||||
else:
|
||||
addresses.update({constants.CONTROLLER_1_HOSTNAME: None})
|
||||
|
||||
if pool.gateway_address is not None:
|
||||
if utils.get_distributed_cloud_role() == \
|
||||
|
|
|
@ -771,7 +771,7 @@ class AddressesStillExist(Conflict):
|
|||
|
||||
|
||||
class AddressPoolAlreadyExists(Conflict):
|
||||
message = _("Address pool %(uuid)s already exists")
|
||||
message = _("Address pool %(name)s already exists")
|
||||
|
||||
|
||||
class AddressPoolFamilyMismatch(Conflict):
|
||||
|
|
|
@ -5688,7 +5688,7 @@ class Connection(api.Connection):
|
|||
session.add(address_pool)
|
||||
session.flush()
|
||||
except db_exc.DBDuplicateEntry:
|
||||
raise exception.AddressPoolAlreadyExists(uuid=values['uuid'])
|
||||
raise exception.AddressPoolAlreadyExists(name=values['name'])
|
||||
return self._address_pool_get(values['uuid'])
|
||||
|
||||
def _address_pool_range_update(self, session, address_pool, ranges):
|
||||
|
|
|
@ -0,0 +1,366 @@
|
|||
#
|
||||
# Copyright (c) 2023 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
"""
|
||||
Tests for the API / address pool / methods.
|
||||
"""
|
||||
|
||||
import netaddr
|
||||
from six.moves import http_client
|
||||
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from sysinv.tests.api import base
|
||||
from sysinv.tests.db import base as dbbase
|
||||
from sysinv.tests.db import utils as dbutils
|
||||
|
||||
|
||||
class AddressPoolTestCase(base.FunctionalTest, dbbase.BaseHostTestCase):
|
||||
|
||||
# API_HEADERS are a generic header passed to most API calls
|
||||
API_HEADERS = {'User-Agent': 'sysinv-test'}
|
||||
|
||||
# API_PREFIX is the prefix for the URL
|
||||
API_PREFIX = '/addrpools'
|
||||
|
||||
# RESULT_KEY is the python table key for the list of results
|
||||
RESULT_KEY = 'addrpools'
|
||||
|
||||
# COMMON_FIELD is a field that is known to exist for inputs and outputs
|
||||
COMMON_FIELD = 'network'
|
||||
|
||||
# expected_api_fields are attributes that should be populated by
|
||||
# an API query
|
||||
expected_api_fields = ['id',
|
||||
'uuid',
|
||||
'name',
|
||||
'network',
|
||||
'prefix',
|
||||
'order',
|
||||
'controller0_address',
|
||||
'controller0_address_id',
|
||||
'controller1_address',
|
||||
'controller1_address_id',
|
||||
'floating_address',
|
||||
'floating_address_id',
|
||||
'gateway_address',
|
||||
'gateway_address_id',
|
||||
'ranges',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super(AddressPoolTestCase, self).setUp()
|
||||
|
||||
def get_single_url(self, uuid):
|
||||
return '%s/%s' % (self.API_PREFIX, uuid)
|
||||
|
||||
def assert_fields(self, api_object):
|
||||
# check the uuid is a uuid
|
||||
assert(uuidutils.is_uuid_like(api_object['uuid']))
|
||||
|
||||
# Verify that expected attributes are returned
|
||||
for field in self.expected_api_fields:
|
||||
self.assertIn(field, api_object)
|
||||
|
||||
def get_post_object(self, name, network, prefix):
|
||||
pool_db = dbutils.get_test_address_pool(
|
||||
name=name,
|
||||
network=network,
|
||||
prefix=prefix
|
||||
)
|
||||
|
||||
del pool_db['family']
|
||||
|
||||
return pool_db
|
||||
|
||||
def _create_db_object(self, name='testpool'):
|
||||
return self._create_test_address_pool(
|
||||
name=name,
|
||||
subnet=self.mgmt_subnet
|
||||
)
|
||||
|
||||
|
||||
class TestPostMixin(AddressPoolTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestPostMixin, self).setUp()
|
||||
|
||||
def _test_create_address_pool_success(self, name, network, prefix):
|
||||
# Test creation of object
|
||||
|
||||
ndict = self.get_post_object(name, network, prefix)
|
||||
response = self.post_json(self.API_PREFIX,
|
||||
ndict,
|
||||
headers=self.API_HEADERS)
|
||||
|
||||
# Check HTTP response is successful
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(response.status_code, http_client.OK)
|
||||
|
||||
# Check that an expected field matches.
|
||||
self.assertEqual(response.json[self.COMMON_FIELD],
|
||||
ndict[self.COMMON_FIELD])
|
||||
|
||||
uuid = response.json['uuid']
|
||||
|
||||
# Verify that the object was created and some basic attribute matches
|
||||
response = self.get_json(self.get_single_url(uuid))
|
||||
self.assertEqual(response[self.COMMON_FIELD],
|
||||
ndict[self.COMMON_FIELD])
|
||||
|
||||
def _test_create_address_pool_fail_duplicate(self, name, network, prefix):
|
||||
ndict = self.get_post_object(name, network, prefix)
|
||||
response = self.post_json(self.API_PREFIX,
|
||||
ndict,
|
||||
headers=self.API_HEADERS)
|
||||
|
||||
# Check HTTP response is successful
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(response.status_code, http_client.OK)
|
||||
|
||||
response = self.post_json(self.API_PREFIX,
|
||||
ndict,
|
||||
headers=self.API_HEADERS,
|
||||
expect_errors=True)
|
||||
|
||||
# Check HTTP response is failed
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(response.status_code, http_client.CONFLICT)
|
||||
self.assertIn("Address pool %s already exists" % name,
|
||||
response.json['error_message'])
|
||||
|
||||
def _test_create_address_pool_address_not_in_subnet(self, addr_type):
|
||||
address = str(self.oam_subnet[1])
|
||||
network = str(self.mgmt_subnet.network)
|
||||
prefix = self.mgmt_subnet.prefixlen
|
||||
|
||||
ndict = self.get_post_object('test', network, prefix)
|
||||
ndict['%s_address' % addr_type] = address
|
||||
|
||||
response = self.post_json(self.API_PREFIX,
|
||||
ndict,
|
||||
headers=self.API_HEADERS,
|
||||
expect_errors=True)
|
||||
|
||||
# Check HTTP response is failed
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(response.status_code, http_client.BAD_REQUEST)
|
||||
self.assertIn("IP Address %s is not in subnet: %s/%d" % (address, network, prefix),
|
||||
response.json['error_message'])
|
||||
|
||||
def _test_create_address_pool_invalid_address_family(self, addr_type):
|
||||
if self.mgmt_subnet.version == 6:
|
||||
address = netaddr.IPAddress('1.1.1.1')
|
||||
else:
|
||||
address = netaddr.IPAddress('1111::1')
|
||||
network = str(self.mgmt_subnet.network)
|
||||
prefix = self.mgmt_subnet.prefixlen
|
||||
|
||||
ndict = self.get_post_object('test', network, prefix)
|
||||
ndict['%s_address' % addr_type] = str(address)
|
||||
|
||||
response = self.post_json(self.API_PREFIX,
|
||||
ndict,
|
||||
headers=self.API_HEADERS,
|
||||
expect_errors=True)
|
||||
|
||||
# Check HTTP response is failed
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(response.status_code, http_client.BAD_REQUEST)
|
||||
self.assertIn("Invalid IP version %s %s" % (address.version, network),
|
||||
response.json['error_message'])
|
||||
|
||||
def _test_create_address_pool_invalid_address_network(self, addr_type):
|
||||
|
||||
# network address ie. 192.168.101.0
|
||||
address = str(self.mgmt_subnet[0])
|
||||
|
||||
network = str(self.mgmt_subnet.network)
|
||||
prefix = self.mgmt_subnet.prefixlen
|
||||
|
||||
ndict = self.get_post_object('test', network, prefix)
|
||||
ndict['%s_address' % addr_type] = str(address)
|
||||
|
||||
response = self.post_json(self.API_PREFIX,
|
||||
ndict,
|
||||
headers=self.API_HEADERS,
|
||||
expect_errors=True)
|
||||
|
||||
# Check HTTP response is failed
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(response.status_code, http_client.BAD_REQUEST)
|
||||
self.assertIn("Invalid IP address: %s. Cannot use network address: %s." % (address, network),
|
||||
response.json['error_message'])
|
||||
|
||||
def _test_create_address_pool_invalid_address_broadcast(self, addr_type):
|
||||
|
||||
# broadcast address ie. 192.168.101.255
|
||||
address = str(self.mgmt_subnet[-1])
|
||||
|
||||
network = str(self.mgmt_subnet.network)
|
||||
prefix = self.mgmt_subnet.prefixlen
|
||||
|
||||
ndict = self.get_post_object('test', network, prefix)
|
||||
ndict['%s_address' % addr_type] = str(address)
|
||||
|
||||
response = self.post_json(self.API_PREFIX,
|
||||
ndict,
|
||||
headers=self.API_HEADERS,
|
||||
expect_errors=True)
|
||||
|
||||
# Check HTTP response is failed
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(response.status_code, http_client.BAD_REQUEST)
|
||||
self.assertIn("Cannot use broadcast address: %s." % address,
|
||||
response.json['error_message'])
|
||||
|
||||
def test_address_pool_create_success(self):
|
||||
self._test_create_address_pool_success(
|
||||
'test', str(self.mgmt_subnet.network), self.mgmt_subnet.prefixlen)
|
||||
|
||||
def test_address_pool_create_fail_duplicate(self):
|
||||
self._test_create_address_pool_fail_duplicate(
|
||||
'test', str(self.mgmt_subnet.network), self.mgmt_subnet.prefixlen)
|
||||
|
||||
def test_address_pool_create_reversed_ranges(self):
|
||||
start = str(self.mgmt_subnet[-2])
|
||||
end = str(self.mgmt_subnet[1])
|
||||
network = str(self.mgmt_subnet.network)
|
||||
prefix = self.mgmt_subnet.prefixlen
|
||||
|
||||
ndict = self.get_post_object('test', network, prefix)
|
||||
ndict['ranges'] = [[start, end]]
|
||||
|
||||
response = self.post_json(self.API_PREFIX,
|
||||
ndict,
|
||||
headers=self.API_HEADERS,
|
||||
expect_errors=True)
|
||||
|
||||
# Check HTTP response is failed
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(response.status_code, http_client.CONFLICT)
|
||||
self.assertIn("start address must be less than end address",
|
||||
response.json['error_message'])
|
||||
|
||||
def test_address_pool_create_invalid_ranges(self):
|
||||
start = str(self.mgmt_subnet[1])
|
||||
end = str(self.oam_subnet[-2])
|
||||
network = str(self.mgmt_subnet.network)
|
||||
prefix = self.mgmt_subnet.prefixlen
|
||||
|
||||
ndict = self.get_post_object('test', network, prefix)
|
||||
ndict['ranges'] = [[start, end]]
|
||||
|
||||
response = self.post_json(self.API_PREFIX,
|
||||
ndict,
|
||||
headers=self.API_HEADERS,
|
||||
expect_errors=True)
|
||||
|
||||
# Check HTTP response is failed
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(response.status_code, http_client.CONFLICT)
|
||||
self.assertIn("Address %s is not within network %s/%d" % (end, network, prefix),
|
||||
response.json['error_message'])
|
||||
|
||||
def test_address_pool_create_floating_ip_not_in_subnet(self):
|
||||
self._test_create_address_pool_address_not_in_subnet('floating')
|
||||
|
||||
def test_address_pool_create_floating_ip_has_invalid_family(self):
|
||||
self._test_create_address_pool_invalid_address_family('floating')
|
||||
|
||||
def test_address_pool_create_floating_ip_is_network_address(self):
|
||||
self._test_create_address_pool_invalid_address_network('floating')
|
||||
|
||||
def test_address_pool_create_floating_ip_is_broadcast(self):
|
||||
self._test_create_address_pool_invalid_address_broadcast('floating')
|
||||
|
||||
def test_address_pool_create_controller0_ip_not_in_subnet(self):
|
||||
self._test_create_address_pool_address_not_in_subnet('controller0')
|
||||
|
||||
def test_address_pool_create_controller0_ip_has_invalid_family(self):
|
||||
self._test_create_address_pool_invalid_address_family('controller0')
|
||||
|
||||
def test_address_pool_create_controller0_ip_is_network_address(self):
|
||||
self._test_create_address_pool_invalid_address_network('controller0')
|
||||
|
||||
def test_address_pool_create_controller0_ip_is_broadcast(self):
|
||||
self._test_create_address_pool_invalid_address_broadcast('controller0')
|
||||
|
||||
def test_address_pool_create_controller1_ip_not_in_subnet(self):
|
||||
self._test_create_address_pool_address_not_in_subnet('controller1')
|
||||
|
||||
def test_address_pool_create_controller1_ip_has_invalid_family(self):
|
||||
self._test_create_address_pool_invalid_address_family('controller1')
|
||||
|
||||
def test_address_pool_create_controller1_ip_is_network_address(self):
|
||||
self._test_create_address_pool_invalid_address_network('controller1')
|
||||
|
||||
def test_address_pool_create_controller1_ip_is_broadcast(self):
|
||||
self._test_create_address_pool_invalid_address_broadcast('controller1')
|
||||
|
||||
def test_address_pool_create_gateway_ip_not_in_subnet(self):
|
||||
self._test_create_address_pool_address_not_in_subnet('gateway')
|
||||
|
||||
def test_address_pool_create_gateway_ip_has_invalid_family(self):
|
||||
self._test_create_address_pool_invalid_address_family('gateway')
|
||||
|
||||
def test_address_pool_create_gateway_ip_is_network_address(self):
|
||||
self._test_create_address_pool_invalid_address_network('gateway')
|
||||
|
||||
def test_address_pool_create_gateway_ip_is_broadcast(self):
|
||||
self._test_create_address_pool_invalid_address_broadcast('gateway')
|
||||
|
||||
|
||||
class TestDelete(AddressPoolTestCase):
|
||||
""" Tests deletion.
|
||||
Typically delete APIs return NO CONTENT.
|
||||
python2 and python3 libraries may return different
|
||||
content_type (None, or empty json) when NO_CONTENT returned.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestDelete, self).setUp()
|
||||
|
||||
def test_address_pool_delete(self):
|
||||
# Delete the API object
|
||||
self.delete_object = self._create_db_object()
|
||||
uuid = self.delete_object.uuid
|
||||
response = self.delete(self.get_single_url(uuid),
|
||||
headers=self.API_HEADERS)
|
||||
|
||||
# Verify the expected API response for the delete
|
||||
self.assertEqual(response.status_code, http_client.NO_CONTENT)
|
||||
|
||||
|
||||
class TestList(AddressPoolTestCase):
|
||||
""" Address Pool list operations
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestList, self).setUp()
|
||||
|
||||
def test_address_pool_list(self):
|
||||
current_pools = self.get_json(self.API_PREFIX)
|
||||
num = len(current_pools[self.RESULT_KEY]) + 1
|
||||
|
||||
# create a single object
|
||||
self.single_object = self._create_db_object()
|
||||
response = self.get_json(self.API_PREFIX)
|
||||
|
||||
self.assertEqual(
|
||||
num,
|
||||
len(response[self.RESULT_KEY]))
|
||||
|
||||
|
||||
class IPv4TestPost(TestPostMixin,
|
||||
AddressPoolTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class IPv6TestPost(TestPostMixin,
|
||||
dbbase.BaseIPv6Mixin,
|
||||
AddressPoolTestCase):
|
||||
pass
|
|
@ -668,7 +668,11 @@ def get_test_address_pool(**kw):
|
|||
'ranges': kw.get('ranges'),
|
||||
'prefix': kw.get('prefix'),
|
||||
'order': kw.get('order', 'random'),
|
||||
'uuid': kw.get('uuid')
|
||||
'uuid': kw.get('uuid'),
|
||||
'controller0_address': kw.get('controller0_address'),
|
||||
'controller1_address': kw.get('controller1_address'),
|
||||
'floating_address': kw.get('floating_address'),
|
||||
'gateway_address': kw.get('gateway_address'),
|
||||
}
|
||||
return inv
|
||||
|
||||
|
|
Loading…
Reference in New Issue