Support searching existing port with macaddress

Kuryr currently supports running container with existing neutron
port with matched IP address(es). This patch adds support for
searching existing neutron port with matching MAC address as
alternative.

End-users have two options to pass existing neutron port to kuryr:
* Use --ip and/or --ip6 to specify the IP address(es) of the
  existing neutron port
* Use --mac-address to specify the MAC address of the existing port.

If both IP and MAC address are specified, kuryr will search with IP
first, then search with MAC. If both searches are missed, kuryr
will create a new port with the specified IP and MAC address.

Implements: blueprint existing-port-by-mac
Change-Id: I1bca7d25aa951d218e579e160a4f18b00b7c14f6
This commit is contained in:
Hongbin Lu 2017-09-19 23:55:03 +00:00
parent 51ccefc3a2
commit 38a32b03e4
3 changed files with 182 additions and 35 deletions

View File

@ -33,6 +33,7 @@ NEUTRON_ID_LH_OPTION = 'kuryr.net.uuid.lh'
NEUTRON_ID_UH_OPTION = 'kuryr.net.uuid.uh'
DOCKER_EXPOSED_PORTS_OPTION = 'com.docker.network.endpoint.exposedports'
DOCKER_MAC_ADDRESS_OPTION = 'com.docker.network.endpoint.macaddress'
KURYR_EXISTING_NEUTRON_NET = 'kuryr.net.existing'
KURYR_EXISTING_NEUTRON_SUBNETPOOL = 'kuryr.subnetpool.existing'
KURYR_EXISTING_NEUTRON_PORT = 'kuryr.port.existing'

View File

@ -487,7 +487,7 @@ def _get_cidr_from_subnetpool(**kwargs):
.format(kwargs))
def _update_existing_port(existing_port, fixed_ip):
def _update_existing_port(existing_port, fixed_ip, mac_address):
host = existing_port.get('binding:host_id')
vif_type = existing_port.get('binding:vif_type')
if not host and vif_type == 'unbound':
@ -496,6 +496,8 @@ def _update_existing_port(existing_port, fixed_ip):
'admin_state_up': True,
'binding:host_id': lib_utils.get_hostname(),
}
if mac_address:
updated_port['mac_address'] = mac_address
updated_port_resp = app.neutron.update_port(
existing_port['id'],
{'port': updated_port})
@ -771,14 +773,14 @@ def network_driver_create_network():
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)
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)
@ -1408,7 +1410,7 @@ def ipam_get_capabilities():
https://github.com/docker/libnetwork/blob/master/docs/ipam.md#getcapabilities # noqa
"""
LOG.debug("Received /IpamDriver.GetCapabilities")
capabilities = {'RequiresMACAddress': False}
capabilities = {'RequiresMACAddress': True}
return flask.jsonify(capabilities)
@ -1565,12 +1567,14 @@ def ipam_request_address():
jsonschema.validate(json_data, schemata.REQUEST_ADDRESS_SCHEMA)
pool_id = json_data['PoolID']
req_address = json_data['Address']
req_mac_address = ''
is_gateway = False
allocated_address = ''
subnet = {}
# Check if the port is gateway
options = json_data.get('Options')
if options:
req_mac_address = options.get(const.DOCKER_MAC_ADDRESS_OPTION)
request_address_type = options.get(const.REQUEST_ADDRESS_TYPE)
if request_address_type == const.NETWORK_GATEWAY_OPTIONS:
is_gateway = True
@ -1632,23 +1636,29 @@ def ipam_request_address():
'admin_state_up': True,
'network_id': neutron_network_id,
}
if req_mac_address:
port['mac_address'] = req_mac_address
fixed_ips = port['fixed_ips'] = []
fixed_ip = {'subnet_id': subnet['id']}
num_ports = 0
filtered_ports = []
if req_address:
fixed_ip['ip_address'] = req_address
fixed_ip_existing = [('subnet_id=%s' % subnet['id'])]
fixed_ip_existing.append('ip_address='
'%s' % str(req_address))
filtered_ports = app.neutron.list_ports(
fixed_ips=fixed_ip_existing)
num_ports = len(filtered_ports.get('ports', []))
fixed_ips=fixed_ip_existing).get('ports', [])
if not filtered_ports:
filtered_ports = app.neutron.list_ports(
mac_address=req_mac_address).get('ports', [])
num_ports = len(filtered_ports)
fixed_ips.append(fixed_ip)
if num_ports:
existing_port = filtered_ports['ports'][0]
created_port = _update_existing_port(existing_port,
fixed_ip)
existing_port = filtered_ports[0]
created_port = _update_existing_port(
existing_port, fixed_ip, req_mac_address)
# REVISIT(yedongcan) For tag-ext extension not
# supported, the Neutron existing port still can not
# be deleted in ipam_release_address.
@ -1664,9 +1674,11 @@ 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'])
fixed_ips = created_port['fixed_ips']
fixed_ips = [ip for ip in fixed_ips
if ip['subnet_id'] == subnet['id']]
allocated_address = (req_address or
fixed_ips[0]['ip_address'])
allocated_address = '{}/{}'.format(allocated_address,
subnet_cidr.prefixlen)
except n_exceptions.NeutronClientException as ex:

View File

@ -53,7 +53,7 @@ class TestKuryrIpam(base.TestKuryrBase):
"GlobalDefaultAddressSpace":
config.CONF.global_default_address_space}),
('/IpamDriver.GetCapabilities',
{"RequiresMACAddress": False}))
{"RequiresMACAddress": True}))
@ddt.unpack
def test_remote_ipam_driver_endpoint(self, endpoint, expected):
response = self.app.post(endpoint)
@ -595,15 +595,18 @@ class TestKuryrIpam(base.TestKuryrBase):
# faking create_port
fake_neutron_port_id = uuidutils.generate_uuid()
fake_mac_address = 'fa:16:3e:ca:59:88'
fake_port = base.TestKuryrBase._get_fake_port(
docker_endpoint_id, neutron_network_id,
fake_neutron_port_id, lib_const.PORT_STATUS_ACTIVE,
subnet_v4_id,
neutron_subnet_v4_address="10.0.0.5")
neutron_subnet_v4_address="10.0.0.5",
neutron_mac_address=fake_mac_address)
port_request = {
'name': const.KURYR_UNBOUND_PORT,
'admin_state_up': True,
'network_id': neutron_network_id,
'mac_address': fake_mac_address,
}
fixed_ips = port_request['fixed_ips'] = []
fixed_ip = {'subnet_id': subnet_v4_id}
@ -614,7 +617,7 @@ class TestKuryrIpam(base.TestKuryrBase):
fake_request = {
'PoolID': fake_kuryr_subnetpool_id,
'Address': '', # Querying for container address
'Options': {}
'Options': {const.DOCKER_MAC_ADDRESS_OPTION: fake_mac_address}
}
mock_port_add_tag.return_value = None
response = self.app.post('/IpamDriver.RequestAddress',
@ -637,6 +640,7 @@ class TestKuryrIpam(base.TestKuryrBase):
def test_ipam_driver_request_address_when_subnet_not_exist(self,
mock_list_subnetpools, mock_list_subnets):
requested_address = '10.0.0.5'
fake_mac_address = 'fa:16:3e:ca:59:88'
fake_kuryr_subnetpool_id = uuidutils.generate_uuid()
fake_name = lib_utils.get_neutron_subnetpool_name(FAKE_IP4_CIDR)
kuryr_subnetpools = self._get_fake_v4_subnetpools(
@ -652,7 +656,7 @@ class TestKuryrIpam(base.TestKuryrBase):
fake_request = {
'PoolID': fake_kuryr_subnetpool_id,
'Address': requested_address,
'Options': {}
'Options': {const.DOCKER_MAC_ADDRESS_OPTION: fake_mac_address}
}
response = self.app.post('/IpamDriver.RequestAddress',
content_type='application/json',
@ -692,12 +696,14 @@ class TestKuryrIpam(base.TestKuryrBase):
# faking update_port or create_port
requested_address = '10.0.0.5'
fake_mac_address = 'fa:16:3e:ca:59:88'
fake_neutron_port_id = uuidutils.generate_uuid()
fake_port = base.TestKuryrBase._get_fake_port(
docker_endpoint_id, neutron_network_id,
fake_neutron_port_id, lib_const.PORT_STATUS_ACTIVE,
subnet_v4_id,
neutron_subnet_v4_address=requested_address)
neutron_subnet_v4_address=requested_address,
neutron_mac_address=fake_mac_address)
fixed_ip_existing = [('subnet_id=%s' % subnet_v4_id)]
fixed_ip_existing.append('ip_address=%s' % requested_address)
@ -708,6 +714,7 @@ class TestKuryrIpam(base.TestKuryrBase):
'name': const.KURYR_UNBOUND_PORT,
'admin_state_up': True,
'network_id': neutron_network_id,
'mac_address': fake_mac_address,
}
fixed_ips = port_request['fixed_ips'] = []
fixed_ip = {'subnet_id': subnet_v4_id,
@ -719,7 +726,7 @@ class TestKuryrIpam(base.TestKuryrBase):
fake_request = {
'PoolID': fake_kuryr_subnetpool_id,
'Address': requested_address,
'Options': {}
'Options': {const.DOCKER_MAC_ADDRESS_OPTION: fake_mac_address}
}
mock_port_add_tag.return_value = None
response = self.app.post('/IpamDriver.RequestAddress',
@ -729,8 +736,9 @@ class TestKuryrIpam(base.TestKuryrBase):
self.assertEqual(200, response.status_code)
mock_list_subnets.assert_called_with(
subnetpool_id=fake_kuryr_subnetpool_id)
mock_list_ports.assert_called_with(
fixed_ips=fixed_ip_existing)
mock_list_ports.assert_has_calls([
mock.call(fixed_ips=fixed_ip_existing),
mock.call(mac_address=fake_mac_address)])
mock_create_port.assert_called_with({'port': port_request})
if mock_app.tag_ext:
mock_port_add_tag.assert_called()
@ -781,13 +789,15 @@ class TestKuryrIpam(base.TestKuryrBase):
# faking update_port or create_port
requested_address = '10.0.0.5'
requested_address_v6 = 'fe80::6'
requested_mac_address = 'fa:16:3e:86:a0:fe'
fake_neutron_port_id = uuidutils.generate_uuid()
fake_port = base.TestKuryrBase._get_fake_port(
docker_endpoint_id, neutron_network_id,
fake_neutron_port_id, lib_const.PORT_STATUS_ACTIVE,
subnet_v4_id, subnet_v6_id,
neutron_subnet_v4_address=requested_address,
neutron_subnet_v6_address=requested_address_v6)
neutron_subnet_v6_address=requested_address_v6,
neutron_mac_address=requested_mac_address)
fixed_ip_existing = [('subnet_id=%s' % subnet_v4_id)]
fixed_ipv6_existing = [('subnet_id=%s' % subnet_v6_id)]
@ -808,6 +818,7 @@ class TestKuryrIpam(base.TestKuryrBase):
'name': const.NEUTRON_UNBOUND_PORT,
'admin_state_up': True,
'binding:host_id': lib_utils.get_hostname(),
'mac_address': requested_mac_address,
}
mock_update_port.return_value = fake_port
@ -815,7 +826,7 @@ class TestKuryrIpam(base.TestKuryrBase):
fake_request = {
'PoolID': fake_kuryr_subnetpool_id,
'Address': requested_address,
'Options': {}
'Options': {const.DOCKER_MAC_ADDRESS_OPTION: requested_mac_address}
}
mock_port_add_tag.return_value = None
response = self.app.post('/IpamDriver.RequestAddress',
@ -829,7 +840,7 @@ class TestKuryrIpam(base.TestKuryrBase):
fake_request_2 = {
'PoolID': fake_kuryr_subnetpool_v6_id,
'Address': requested_address_v6,
'Options': {}
'Options': {const.DOCKER_MAC_ADDRESS_OPTION: requested_mac_address}
}
response = self.app.post('/IpamDriver.RequestAddress',
content_type='application/json',
@ -852,6 +863,122 @@ class TestKuryrIpam(base.TestKuryrBase):
else:
self.assertEqual(0, mock_port_add_tag.call_count)
@mock.patch('kuryr_libnetwork.controllers._neutron_port_add_tag')
@mock.patch('kuryr_libnetwork.controllers.app.neutron.create_port')
@mock.patch('kuryr_libnetwork.controllers.app.neutron.update_port')
@mock.patch('kuryr_libnetwork.controllers.app.neutron.list_ports')
@mock.patch('kuryr_libnetwork.controllers.app.neutron.list_subnets')
@mock.patch('kuryr_libnetwork.controllers.app')
@ddt.data((False), (True))
def test_ipam_driver_request_specific_mac_address_existing_port(self,
use_tag_ext, mock_app, mock_list_subnets, mock_list_ports,
mock_update_port, mock_create_port, mock_port_add_tag):
mock_app.tag_ext = use_tag_ext
# faking list_subnets
neutron_network_id = uuidutils.generate_uuid()
docker_endpoint_id = lib_utils.get_hash()
subnet_v4_id = uuidutils.generate_uuid()
subnet_v6_id = uuidutils.generate_uuid()
fake_kuryr_subnetpool_id = uuidutils.generate_uuid()
fake_kuryr_subnetpool_v6_id = uuidutils.generate_uuid()
fake_v4_subnet = self._get_fake_v4_subnet(
neutron_network_id, docker_endpoint_id, subnet_v4_id,
subnetpool_id=fake_kuryr_subnetpool_id,
cidr=FAKE_IP4_CIDR)
fake_v6_subnet = self._get_fake_v6_subnet(
neutron_network_id, docker_endpoint_id, subnet_v6_id,
subnetpool_id=fake_kuryr_subnetpool_v6_id,
cidr=FAKE_IP6_CIDR)
fake_subnet_response = {
'subnets': [
fake_v4_subnet['subnet']
]
}
fake_subnet_response_v6 = {
'subnets': [
fake_v6_subnet['subnet']
]
}
mock_list_subnets.side_effect = [
fake_subnet_response, fake_subnet_response_v6]
# faking update_port or create_port
fake_address = '10.0.0.5'
fake_address_v6 = 'fe80::6'
requested_mac_address = 'fa:16:3e:86:a0:fe'
fake_neutron_port_id = uuidutils.generate_uuid()
fake_port = base.TestKuryrBase._get_fake_port(
docker_endpoint_id, neutron_network_id,
fake_neutron_port_id, lib_const.PORT_STATUS_ACTIVE,
subnet_v4_id, subnet_v6_id,
neutron_subnet_v4_address=fake_address,
neutron_subnet_v6_address=fake_address_v6,
neutron_mac_address=requested_mac_address)
fixed_ip_existing = [('subnet_id=%s' % subnet_v4_id)]
fixed_ipv6_existing = [('subnet_id=%s' % subnet_v6_id)]
fixed_ip_existing.append('ip_address=%s' % fake_address)
fixed_ipv6_existing.append('ip_address=%s' % fake_address_v6)
fake_existing_port = dict(fake_port['port'])
fake_existing_port['binding:host_id'] = ''
fake_existing_port['binding:vif_type'] = 'unbound'
fake_ports_response = {'ports': [fake_existing_port]}
fake_existing_port_2 = dict(fake_port['port'])
fake_existing_port_2['name'] = const.NEUTRON_UNBOUND_PORT
fake_existing_port_2['binding:host_id'] = lib_utils.get_hostname()
fake_ports_response_2 = {'ports': [fake_existing_port_2]}
mock_list_ports.side_effect = [
fake_ports_response, fake_ports_response_2]
update_port = {
'name': const.NEUTRON_UNBOUND_PORT,
'admin_state_up': True,
'binding:host_id': lib_utils.get_hostname(),
'mac_address': requested_mac_address,
}
mock_update_port.return_value = fake_port
# Testing container ip allocation
fake_request = {
'PoolID': fake_kuryr_subnetpool_id,
'Address': '',
'Options': {const.DOCKER_MAC_ADDRESS_OPTION: requested_mac_address}
}
mock_port_add_tag.return_value = None
response = self.app.post('/IpamDriver.RequestAddress',
content_type='application/json',
data=jsonutils.dumps(fake_request))
self.assertEqual(200, response.status_code)
decoded_json = jsonutils.loads(response.data)
self.assertEqual(fake_address + '/16', decoded_json['Address'])
fake_request_2 = {
'PoolID': fake_kuryr_subnetpool_v6_id,
'Address': '',
'Options': {const.DOCKER_MAC_ADDRESS_OPTION: requested_mac_address}
}
response = self.app.post('/IpamDriver.RequestAddress',
content_type='application/json',
data=jsonutils.dumps(fake_request_2))
self.assertEqual(200, response.status_code)
decoded_json = jsonutils.loads(response.data)
self.assertEqual(fake_address_v6 + '/64', decoded_json['Address'])
mock_list_subnets.assert_has_calls([
mock.call(subnetpool_id=fake_kuryr_subnetpool_id),
mock.call(subnetpool_id=fake_kuryr_subnetpool_v6_id)])
mock_list_ports.assert_has_calls([
mock.call(mac_address=requested_mac_address),
mock.call(mac_address=requested_mac_address)])
mock_update_port.assert_called_with(fake_neutron_port_id,
{'port': update_port})
if mock_app.tag_ext:
self.assertEqual(2, mock_port_add_tag.call_count)
else:
self.assertEqual(0, mock_port_add_tag.call_count)
@mock.patch('kuryr_libnetwork.controllers._neutron_port_add_tag')
@mock.patch('kuryr_libnetwork.controllers.app.neutron.create_port')
@mock.patch('kuryr_libnetwork.controllers.app.neutron.list_subnets')
@ -903,16 +1030,19 @@ class TestKuryrIpam(base.TestKuryrBase):
fake_subnet_response, fake_subnet_response2]
# faking create_port
fake_neutron_port_id = uuidutils.generate_uuid()
fake_mac_address = 'fa:16:3e:86:a0:fe'
fake_port = self._get_fake_port(
docker_endpoint_id, neutron_network_id,
fake_neutron_port_id,
neutron_subnet_v4_id=neutron_subnet_v4_id,
neutron_subnet_v4_address="10.0.0.5")
neutron_subnet_v4_address="10.0.0.5",
neutron_mac_address=fake_mac_address)
mock_create_port.return_value = fake_port
port_request = {
'name': const.KURYR_UNBOUND_PORT,
'admin_state_up': True,
'network_id': neutron_network_id,
'mac_address': fake_mac_address,
}
port_request['fixed_ips'] = []
fixed_ip = {'subnet_id': neutron_subnet_v4_id}
@ -922,7 +1052,7 @@ class TestKuryrIpam(base.TestKuryrBase):
fake_request = {
'PoolID': fake_kuryr_subnetpool_id,
'Address': '', # Querying for container address
'Options': {}
'Options': {const.DOCKER_MAC_ADDRESS_OPTION: fake_mac_address}
}
mock_port_add_tag.return_value = None
response = self.app.post('/IpamDriver.RequestAddress',
@ -993,10 +1123,11 @@ class TestKuryrIpam(base.TestKuryrBase):
fake_subnet_response, fake_subnet_response2]
# Testing container ip allocation
fake_mac_address = 'fa:16:3e:86:a0:fe'
fake_request = {
'PoolID': fake_kuryr_subnetpool_id,
'Address': '', # Querying for container address
'Options': {}
'Options': {const.DOCKER_MAC_ADDRESS_OPTION: fake_mac_address}
}
response = self.app.post('/IpamDriver.RequestAddress',
content_type='application/json',
@ -1057,16 +1188,19 @@ class TestKuryrIpam(base.TestKuryrBase):
mock_list_subnets.return_value = fake_subnet_response
# faking create_port
fake_neutron_port_id = uuidutils.generate_uuid()
fake_mac_address = 'fa:16:3e:86:a0:fe'
fake_port = self._get_fake_port(
docker_endpoint_id, neutron_network_id,
fake_neutron_port_id,
neutron_subnet_v4_id=neutron_subnet_v4_id,
neutron_subnet_v4_address="10.0.0.5")
neutron_subnet_v4_address="10.0.0.5",
neutron_mac_address=fake_mac_address)
mock_create_port.return_value = fake_port
port_request = {
'name': const.KURYR_UNBOUND_PORT,
'admin_state_up': True,
'network_id': neutron_network_id,
'mac_address': fake_mac_address,
}
port_request['fixed_ips'] = []
fixed_ip = {'subnet_id': neutron_subnet_v4_id}
@ -1076,7 +1210,7 @@ class TestKuryrIpam(base.TestKuryrBase):
fake_request = {
'PoolID': fake_kuryr_subnetpool_id,
'Address': '', # Querying for container address
'Options': {}
'Options': {const.DOCKER_MAC_ADDRESS_OPTION: fake_mac_address}
}
mock_port_add_tag.return_value = None
response = self.app.post('/IpamDriver.RequestAddress',