Support specifying existing subnet uuid/name

Currently, we rely on user-provided cidr to select the subnet from which the
port creates. This approach has some limitations, i.e. overlapping cidr. This
BP proposes to introduce driver-specific options (e.g. neutron.subnet.uuid) to
allow users to specify a specific subnet. To identify the right subnetpools
has the same prefixes. The approach is to use tagging, tag subnetpool with the
uuid of the specified subnet.

For e.x - Need both ipam-option *and* option:
    docker network create ... --ipam-opt neutron.subnet.uuid=xxx -o
    neutron.subnet.uuid=xxx (User can pass neutron.subnet.name also)

Change-Id: I01223e9c7668ba083a90d86e007cc5ff16af84e3
Implement: blueprint existing-subnet
This commit is contained in:
Kien Nguyen 2017-08-31 14:48:55 +07:00 committed by Kien Nguyen
parent 2490c368e3
commit fa290eafcf
4 changed files with 228 additions and 33 deletions

View File

@ -39,6 +39,10 @@ KURYR_EXISTING_NEUTRON_PORT = 'kuryr.port.existing'
NETWORK_GATEWAY_OPTIONS = 'com.docker.network.gateway'
NETWORK_GENERIC_OPTIONS = 'com.docker.network.generic'
NEUTRON_NAME_OPTION = 'neutron.net.name'
NEUTRON_SUBNET_NAME_OPTION = 'neutron.subnet.name'
NEUTRON_SUBNET_UUID_OPTION = 'neutron.subnet.uuid'
NEUTRON_V6_SUBNET_NAME_OPTION = 'neutron.subnet.v6.name'
NEUTRON_V6_SUBNET_UUID_OPTION = 'neutron.subnet.v6.uuid'
NEUTRON_POOL_NAME_OPTION = 'neutron.pool.name'
NEUTRON_POOL_UUID_OPTION = 'neutron.pool.uuid'
NEUTRON_V6_POOL_NAME_OPTION = 'neutron.pool.v6.name'

View File

@ -39,7 +39,6 @@ from kuryr_libnetwork import utils
LOG = log.getLogger(__name__)
MANDATORY_NEUTRON_EXTENSION = "subnet_allocation"
TAG_NEUTRON_EXTENSION = "tag"
TAG_EXT_NEUTRON_EXTENSION = "tag-ext"
@ -166,6 +165,19 @@ def _get_subnets_by_interface_cidr(neutron_network_id,
return subnets
def _get_subnet_by_name(subnet_name):
subnets_by_name = _get_subnets_by_attrs(name=subnet_name)
if not subnets_by_name:
raise exceptions.NoResourceException(
"The subnet doesn't exist for the name {0}"
.format(subnets_by_name))
elif len(subnets_by_name) > 1:
raise exceptions.DuplicatedResourceException(
"Multiple Neutron subnets exist for name {0}"
.format(subnet_name))
return subnets_by_name[0]['id']
def _get_neutron_port_from_docker_endpoint(endpoint_id):
port_name = utils.get_neutron_port_name(endpoint_id)
filtered_ports = app.neutron.list_ports(name=port_name)
@ -229,7 +241,7 @@ def _get_fixed_ips_by_interface_cidr(subnets, interface_cidrv4,
def _create_or_update_port(neutron_network_id, endpoint_id,
interface_cidrv4, interface_cidrv6, interface_mac):
interface_cidrv4, interface_cidrv6, interface_mac):
subnets = []
fixed_ips = []
response_port = []
@ -252,14 +264,14 @@ def _create_or_update_port(neutron_network_id, endpoint_id,
.format(interface_cidrv4, interface_cidrv6))
_get_fixed_ips_by_interface_cidr(subnets, interface_cidrv4,
interface_cidrv6, fixed_ips)
interface_cidrv6, fixed_ips)
filtered_ports = app.neutron.list_ports(fixed_ips=fixed_ips)
num_port = len(filtered_ports.get('ports', []))
if not num_port:
fixed_ips = (
lib_utils.get_dict_format_fixed_ips_from_kv_format(fixed_ips))
response_port = _create_port(endpoint_id, neutron_network_id,
interface_mac, fixed_ips)
interface_mac, fixed_ips)
elif num_port == 1:
port = filtered_ports['ports'][0]
response_port = app.driver.update_port(port, endpoint_id,
@ -275,7 +287,7 @@ def _create_or_update_port(neutron_network_id, endpoint_id,
fixed_ips = (
lib_utils.get_dict_format_fixed_ips_from_kv_format(fixed_ips))
response_port = _create_port(endpoint_id, neutron_network_id,
interface_mac, fixed_ips)
interface_mac, fixed_ips)
else:
raise exceptions.DuplicatedResourceException(
"Multiple ports exist for the cidrs {0} and {1}"
@ -496,7 +508,7 @@ def _update_existing_port(existing_port, fixed_ip):
raise exceptions.AddressInUseException(
"Requested ip address {0} already belongs to "
"a bound Neutron port: {1}".format(fixed_ip,
existing_port['id']))
existing_port['id']))
return existing_port
@ -549,9 +561,13 @@ def _create_kuryr_subnet(pool_cidr, subnet_cidr, pool_id, network_id, gateway):
LOG.debug("Created kuryr subnet %s", new_kuryr_subnet)
def _create_kuryr_subnetpool(pool_cidr):
def _create_kuryr_subnetpool(pool_cidr, pool_tag):
pool_name = lib_utils.get_neutron_subnetpool_name(pool_cidr)
pools = _get_subnetpools_by_attrs(name=pool_name)
if pool_tag:
kwargs = {'tags': [pool_tag]}
else:
kwargs = {'name': pool_name}
pools = _get_subnetpools_by_attrs(**kwargs)
if len(pools):
raise exceptions.KuryrException(
"Another pool with same cidr exist. ipam and network"
@ -566,6 +582,8 @@ def _create_kuryr_subnetpool(pool_cidr):
created_subnetpool_response = app.neutron.create_subnetpool(
{'subnetpool': new_subnetpool})
pool = created_subnetpool_response['subnetpool']
if pool_tag:
_neutron_subnetpool_add_tag(pool['id'], pool_tag)
return pool
@ -745,10 +763,22 @@ def network_driver_create_network():
v4_pool_id = ''
v6_pool_name = ''
v6_pool_id = ''
v4_subnet_name = ''
v4_subnet_id = ''
v6_subnet_name = ''
v6_subnet_id = ''
options = json_data.get('Options')
if options:
generic_options = options.get(const.NETWORK_GENERIC_OPTIONS)
if generic_options:
v4_subnet_id = \
generic_options.get(const.NEUTRON_SUBNET_UUID_OPTION)
v4_subnet_name = \
generic_options.get(const.NEUTRON_SUBNET_NAME_OPTION)
v6_subnet_id = \
generic_options.get(const.NEUTRON_V6_SUBNET_UUID_OPTION)
v6_subnet_name = \
generic_options.get(const.NEUTRON_V6_SUBNET_NAME_OPTION)
neutron_uuid = generic_options.get(const.NEUTRON_UUID_OPTION)
neutron_name = generic_options.get(const.NEUTRON_NAME_OPTION)
v4_pool_name = generic_options.get(const.NEUTRON_POOL_NAME_OPTION)
@ -759,12 +789,16 @@ def network_driver_create_network():
v6_pool_id = generic_options.get(
const.NEUTRON_V6_POOL_UUID_OPTION)
def _get_pool_id(pool_name, pool_cidr):
def _get_pool_id(pool_name, pool_cidr, pool_tags):
pool_id = ''
kwargs = {}
if pool_tags:
kwargs['tags'] = pool_tags
if not pool_name and pool_cidr:
pool_name = lib_utils.get_neutron_subnetpool_name(pool_cidr)
if pool_name:
pools = _get_subnetpools_by_attrs(name=pool_name)
kwargs['name'] = pool_name
pools = _get_subnetpools_by_attrs(**kwargs)
if pools:
pool_id = pools[0]['id']
else:
@ -779,15 +813,21 @@ def network_driver_create_network():
raise exceptions.KuryrException(
("Specified pool id({0}) does not "
"exist.").format(pool_id))
if v4_subnet_name and not v4_subnet_id:
v4_subnet_id = _get_subnet_by_name(v4_subnet_name)
if v4_pool_id:
_verify_pool_id(v4_pool_id)
else:
v4_pool_id = _get_pool_id(v4_pool_name, v4_pool_cidr)
v4_pool_tags = [v4_subnet_id] if v4_subnet_id else None
v4_pool_id = _get_pool_id(v4_pool_name, v4_pool_cidr, v4_pool_tags)
if v6_subnet_name and not v6_subnet_id:
v6_subnet_id = _get_subnet_by_name(v6_subnet_name)
if v6_pool_id:
_verify_pool_id(v6_pool_id)
else:
v6_pool_id = _get_pool_id(v6_pool_name, v6_pool_cidr)
v6_pool_tags = [v6_subnet_id] if v6_subnet_id else None
v6_pool_id = _get_pool_id(v6_pool_name, v6_pool_cidr, v6_pool_tags)
# let the user override the driver default
if not neutron_uuid and not neutron_name:
@ -1428,12 +1468,18 @@ def ipam_request_pool():
subnet_cidr = ''
pool_name = ''
pool_id = ''
subnet_id = ''
subnet_name = ''
options = json_data.get('Options')
if options:
if v6:
subnet_name = options.get(const.NEUTRON_V6_SUBNET_NAME_OPTION)
subnet_id = options.get(const.NEUTRON_V6_SUBNET_UUID_OPTION)
pool_name = options.get(const.NEUTRON_V6_POOL_NAME_OPTION)
pool_id = options.get(const.NEUTRON_V6_POOL_UUID_OPTION)
else:
subnet_name = options.get(const.NEUTRON_SUBNET_NAME_OPTION)
subnet_id = options.get(const.NEUTRON_SUBNET_UUID_OPTION)
pool_name = options.get(const.NEUTRON_POOL_NAME_OPTION)
pool_id = options.get(const.NEUTRON_POOL_UUID_OPTION)
if requested_pool:
@ -1442,13 +1488,17 @@ def ipam_request_pool():
else:
cidr = ipaddress.ip_network(six.text_type(requested_pool))
subnet_cidr = six.text_type(cidr)
subnets_by_cidr = _get_subnets_by_attrs(cidr=subnet_cidr)
if len(subnets_by_cidr):
LOG.warning("There is already existing subnet for the "
"same cidr. Please check and specify pool name "
"in Options.")
if not subnet_id and subnet_name:
subnet_id = _get_subnet_by_name(subnet_name)
elif not subnet_id and not subnet_name:
subnets_by_cidr = _get_subnets_by_attrs(cidr=subnet_cidr)
if len(subnets_by_cidr):
LOG.warning("There is already existing subnet for the "
"same cidr. Please check and specify pool name "
"in Options.")
if not pool_name and not pool_id:
pool_id = _create_kuryr_subnetpool(subnet_cidr)['id']
pool_id = _create_kuryr_subnetpool(subnet_cidr,
subnet_id)['id']
else:
if pool_id:
existing_pools = _get_subnetpools_by_attrs(id=pool_id)
@ -1457,7 +1507,7 @@ def ipam_request_pool():
if not existing_pools:
raise exceptions.KuryrException(
("Specified subnetpool id/name({0}) does not "
"exist.").format(pool_id or pool_name))
"exist.").format(pool_id or pool_name))
pool_id = existing_pools[0]['id']
if app.tag_ext:
@ -1469,7 +1519,8 @@ def ipam_request_pool():
LOG.info("Using existing Neutron subnetpool %s successfully",
pool_id)
else:
pool_id = _create_kuryr_subnetpool(subnet_cidr)['id']
pool_id = _create_kuryr_subnetpool(subnet_cidr,
subnet_id)['id']
else:
if v6:
default_pool_list = SUBNET_POOLS_V6
@ -1613,8 +1664,9 @@ def ipam_request_address():
lib_const.DEVICE_OWNER)
LOG.debug("created port %s", created_port)
allocated_address = (req_address or
created_port['fixed_ips'][0]['ip_address'])
allocated_address = \
(req_address or
created_port['fixed_ips'][0]['ip_address'])
allocated_address = '{}/{}'.format(allocated_address,
subnet_cidr.prefixlen)
except n_exceptions.NeutronClientException as ex:
@ -1741,8 +1793,8 @@ def ipam_release_address():
for port in all_ports['ports']:
tags = port.get('tags', [])
if ((tags and lib_const.DEVICE_OWNER in tags) or
(not tags and port['name'] ==
utils.get_neutron_port_name(port['device_id']))):
(not tags and port['name'] ==
utils.get_neutron_port_name(port['device_id']))):
for tmp_subnet in subnets:
if (port['fixed_ips'][0]['subnet_id'] == tmp_subnet['id']):
app.neutron.delete_port(port['id'])

View File

@ -61,6 +61,137 @@ class TestKuryrIpam(base.TestKuryrBase):
decoded_json = jsonutils.loads(response.data)
self.assertEqual(expected, decoded_json)
@mock.patch('kuryr_libnetwork.controllers.app.neutron.add_tag')
@mock.patch('kuryr_libnetwork.controllers.app.neutron.create_subnetpool')
@mock.patch('kuryr_libnetwork.controllers.app.neutron.list_subnetpools')
@ddt.data((FAKE_IP4_CIDR), (FAKE_IP6_CIDR))
def test_ipam_driver_request_pool_with_existing_subnet_id(self,
pool_cidr, mock_list_subnetpools,
mock_create_subnetpool, mock_add_tag):
neutron_subnet_v4_id = uuidutils.generate_uuid()
pool_name = lib_utils.get_neutron_subnetpool_name(pool_cidr)
prefixlen = ipaddress.ip_network(six.text_type(pool_cidr)).prefixlen
new_subnetpool = {
'name': pool_name,
'default_prefixlen': prefixlen,
'prefixes': [pool_cidr]}
fake_kuryr_subnetpool_id = uuidutils.generate_uuid()
fake_name = pool_name
if pool_cidr == FAKE_IP4_CIDR:
kuryr_subnetpools = self._get_fake_v4_subnetpools(
fake_kuryr_subnetpool_id, prefixes=[pool_cidr],
name=fake_name)
else:
kuryr_subnetpools = self._get_fake_v6_subnetpools(
fake_kuryr_subnetpool_id, prefixes=[pool_cidr],
name=fake_name)
mock_list_subnetpools.return_value = {'subnetpools': []}
fake_subnetpool_response = {
'subnetpool': kuryr_subnetpools['subnetpools'][0]
}
mock_create_subnetpool.return_value = fake_subnetpool_response
fake_request = {
'AddressSpace': '',
'Pool': pool_cidr,
'SubPool': '', # In the case --ip-range is not given
'Options': {
'neutron.subnet.uuid': neutron_subnet_v4_id
},
'V6': False
}
response = self.app.post('/IpamDriver.RequestPool',
content_type='application/json',
data=jsonutils.dumps(fake_request))
self.assertEqual(200, response.status_code)
mock_list_subnetpools.assert_called_with(
tags=[str(neutron_subnet_v4_id)])
mock_create_subnetpool.assert_called_with(
{'subnetpool': new_subnetpool})
mock_add_tag.assert_called_once_with(
'subnetpools', fake_kuryr_subnetpool_id, neutron_subnet_v4_id)
decoded_json = jsonutils.loads(response.data)
self.assertEqual(fake_kuryr_subnetpool_id, decoded_json['PoolID'])
@mock.patch('kuryr_libnetwork.controllers.app.neutron.add_tag')
@mock.patch('kuryr_libnetwork.controllers.app.neutron.create_subnetpool')
@mock.patch('kuryr_libnetwork.controllers.app.neutron.list_subnetpools')
@mock.patch('kuryr_libnetwork.controllers.app.neutron.list_subnets')
@ddt.data((FAKE_IP4_CIDR), (FAKE_IP6_CIDR))
def test_ipam_driver_request_pool_with_existing_subnet_name(self,
pool_cidr, mock_list_subnets, mock_list_subnetpools,
mock_create_subnetpool, mock_add_tag):
# faking list_subnets
docker_endpoint_id = lib_utils.get_hash()
neutron_network_id = uuidutils.generate_uuid()
neutron_subnet_v4_id = uuidutils.generate_uuid()
neutron_subnet_v4_name = utils.make_subnet_name(FAKE_IP4_CIDR)
# Faking existing Neutron subnets
fake_v4_subnet = self._get_fake_v4_subnet(
neutron_network_id, docker_endpoint_id,
subnet_v4_id=neutron_subnet_v4_id,
cidr=FAKE_IP4_CIDR, name=neutron_subnet_v4_name)
fake_subnets = {
'subnets': [
fake_v4_subnet['subnet'],
]
}
mock_list_subnets.return_value = fake_subnets
pool_name = lib_utils.get_neutron_subnetpool_name(pool_cidr)
prefixlen = ipaddress.ip_network(six.text_type(pool_cidr)).prefixlen
new_subnetpool = {
'name': pool_name,
'default_prefixlen': prefixlen,
'prefixes': [pool_cidr]}
fake_kuryr_subnetpool_id = uuidutils.generate_uuid()
fake_name = pool_name
if pool_cidr == FAKE_IP4_CIDR:
kuryr_subnetpools = self._get_fake_v4_subnetpools(
fake_kuryr_subnetpool_id, prefixes=[pool_cidr],
name=fake_name)
else:
kuryr_subnetpools = self._get_fake_v6_subnetpools(
fake_kuryr_subnetpool_id, prefixes=[pool_cidr],
name=fake_name)
mock_list_subnetpools.return_value = {'subnetpools': []}
fake_subnetpool_response = {
'subnetpool': kuryr_subnetpools['subnetpools'][0]
}
mock_create_subnetpool.return_value = fake_subnetpool_response
fake_request = {
'AddressSpace': '',
'Pool': pool_cidr,
'SubPool': '', # In the case --ip-range is not given
'Options': {
'neutron.subnet.name': neutron_subnet_v4_name
},
'V6': False
}
response = self.app.post('/IpamDriver.RequestPool',
content_type='application/json',
data=jsonutils.dumps(fake_request))
self.assertEqual(200, response.status_code)
mock_list_subnets.assert_called_with(name=neutron_subnet_v4_name)
mock_list_subnetpools.assert_called_with(
tags=[str(neutron_subnet_v4_id)])
mock_create_subnetpool.assert_called_with(
{'subnetpool': new_subnetpool})
mock_add_tag.assert_called_once_with(
'subnetpools', fake_kuryr_subnetpool_id, neutron_subnet_v4_id)
decoded_json = jsonutils.loads(response.data)
self.assertEqual(fake_kuryr_subnetpool_id, decoded_json['PoolID'])
@mock.patch('kuryr_libnetwork.controllers.app.neutron.create_subnetpool')
@mock.patch('kuryr_libnetwork.controllers.app.neutron.list_subnetpools')
@mock.patch('kuryr_libnetwork.controllers.app.neutron.list_subnets')

View File

@ -71,6 +71,8 @@ class TestKuryrNetworkCreateFailures(base.TestKuryrFailures):
network_request['IPv6Data'][0]['Pool'])
kuryr_v6_subnetpools = self._get_fake_v6_subnetpools(
fake_kuryr_v6_subnetpool_id, name=fake_v6_pool_name)
fake_v4_pool_attrs = {'name': fake_v4_pool_name}
fake_v6_pool_attrs = {'name': fake_v6_pool_name}
mock_list_subnetpools.side_effect = [
{'subnetpools': kuryr_v4_subnetpools['subnetpools']},
{'subnetpools': kuryr_v6_subnetpools['subnetpools']}
@ -86,8 +88,8 @@ class TestKuryrNetworkCreateFailures(base.TestKuryrFailures):
response = self._invoke_create_request(network_request)
self.assertEqual(401, response.status_code)
decoded_json = jsonutils.loads(response.data)
mock_list_subnetpools.assert_any_call(name=fake_v4_pool_name)
mock_list_subnetpools.assert_any_call(name=fake_v6_pool_name)
mock_list_subnetpools.assert_any_call(**fake_v4_pool_attrs)
mock_list_subnetpools.assert_any_call(**fake_v6_pool_attrs)
mock_create_network.assert_called_with(fake_request)
self.assertIn('Err', decoded_json)
self.assertEqual(
@ -127,6 +129,8 @@ class TestKuryrNetworkCreateFailures(base.TestKuryrFailures):
network_request['IPv6Data'][0]['Pool'])
kuryr_v6_subnetpools = self._get_fake_v6_subnetpools(
fake_kuryr_v6_subnetpool_id, name=fake_v6_pool_name)
fake_v4_pool_attrs = {'name': fake_v4_pool_name}
fake_v6_pool_attrs = {'name': fake_v6_pool_name}
mock_list_subnetpools.side_effect = [
{'subnetpools': kuryr_v4_subnetpools['subnetpools']},
{'subnetpools': kuryr_v6_subnetpools['subnetpools']}
@ -136,8 +140,8 @@ class TestKuryrNetworkCreateFailures(base.TestKuryrFailures):
response = self._invoke_create_request(network_request)
self.assertEqual(401, response.status_code)
decoded_json = jsonutils.loads(response.data)
mock_list_subnetpools.assert_any_call(name=fake_v4_pool_name)
mock_list_subnetpools.assert_any_call(name=fake_v6_pool_name)
mock_list_subnetpools.assert_any_call(**fake_v4_pool_attrs)
mock_list_subnetpools.assert_any_call(**fake_v6_pool_attrs)
mock_get_default_network_id.assert_called()
self.assertIn('Err', decoded_json)
self.assertEqual(
@ -240,8 +244,10 @@ class TestKuryrNetworkDeleteFailures(base.TestKuryrFailures):
self.assertEqual(const.SCHEMA['SUCCESS'], decoded_json)
else:
self.assertEqual(GivenException.status_code, response.status_code)
mock_list_subnetpools.assert_any_call(name='kuryr6')
mock_list_subnetpools.assert_any_call(name='kuryr')
fake_v4_pool_attrs = {'name': 'kuryr'}
fake_v6_pool_attrs = {'name': 'kuryr6'}
mock_list_subnetpools.assert_any_call(**fake_v6_pool_attrs)
mock_list_subnetpools.assert_any_call(**fake_v4_pool_attrs)
mock_delete_subnet.assert_any_call(subnet_v4_id)
mock_delete_subnet.assert_any_call(subnet_v6_id)
mock_delete_network.assert_called_with(fake_neutron_network_id)
@ -311,8 +317,10 @@ class TestKuryrNetworkDeleteFailures(base.TestKuryrFailures):
mock_list_networks.assert_any_call(tags=te)
mock_list_subnets.assert_called_with(
network_id=fake_neutron_network_id)
mock_list_subnetpools.assert_any_call(name='kuryr6')
mock_list_subnetpools.assert_any_call(name='kuryr')
fake_v4_pool_attrs = {'name': 'kuryr'}
fake_v6_pool_attrs = {'name': 'kuryr6'}
mock_list_subnetpools.assert_any_call(**fake_v6_pool_attrs)
mock_list_subnetpools.assert_any_call(**fake_v4_pool_attrs)
mock_delete_subnet.assert_called_with(subnet_v4_id)
decoded_json = jsonutils.loads(response.data)