Implement /NetworkDriver.DeleteEndpoint

This patch replaces the mocked version of /NetworkDriver.DeleteEndpoint
with the actual Neutron call. The unit test for the endpoint is also
implemented.

When the requst to delete the Endpoint is invoked, Kuryr deletes the
two or three resources corresponds to the Endpoint in the process of the
endpoint creation, i.e., Neutron port, Neutron subnet ofthe IPv4 address
and/or Neutron subnet of the IPv6 address. Kuryr searches the resources
with the given NetworkID and deletes them all based on the assumption
there's no conflict with the NetworkIDs.

Kuryr tries to delete all the Neutron subnets which IDs are contained in
the Neutron port, however if the multiple Docker endpoints have the same
subnet CIDR, the subntes can be referred from the multiple ports created
with the different Docker endpoint IDs and they can't be deleted unless
they're not referred from any port. In this case we'll get 409 Conflict
as the response status code and the corresponding exceptions. Kuryr
catches the exceptions, log them and continue to proceed because it is
totally the normal case.

The operation should be atomic but in this patch they're not because
there's no transaction mechanism in python-neutronclien and the better
design is required in the future.

Change-Id: I4c94428c613094e758bd228f586cb73f507b5775
Signed-off-by: Taku Fukushima <f.tac.mac@gmail.com>
This commit is contained in:
Taku Fukushima 2015-08-20 01:50:35 +02:00
parent daaa97fbda
commit c7e49bb878
3 changed files with 284 additions and 21 deletions

View File

@ -311,7 +311,77 @@ def network_driver_endpoint_operational_info():
@app.route('/NetworkDriver.DeleteEndpoint', methods=['POST'])
def network_driver_delete_endpoint():
return jsonify(SCHEMA['SUCCESS'])
"""Deletes Neutron Subnets and a Port with the given EndpointID.
This function takes the following JSON data and delegates the actual
endpoint deletion to the Neutron client mapping it into Subnet and Port. ::
{
"NetworkID": string,
"EndpointID": string
}
See the following link for more details about the spec:
https://github.com/docker/libnetwork/blob/master/docs/remote.md#delete-endpoint # noqa
"""
json_data = request.get_json(force=True)
# TODO(tfukushima): Add a validation of the JSON data for the subnet.
app.logger.debug("Received JSON data {0} for /NetworkDriver.DeleteEndpoint"
.format(json_data))
neutron_network_name = json_data['NetworkID']
endpoint_id = json_data['EndpointID']
filtered_networks = app.neutron.list_networks(name=neutron_network_name)
if not filtered_networks:
return jsonify({
'Err': "Neutron network associated with ID {0} doesn't exit."
.format(neutron_network_name)
})
elif len(filtered_networks) > 1:
raise exceptions.DuplicatedResourceException(
"Multiple Neutron Networks exist for NetworkID {0}"
.format(neutron_network_name))
else:
neutron_network_id = filtered_networks['networks'][0]['id']
filtered_ports = []
concerned_subnet_ids = []
try:
filtered_ports = app.neutron.list_ports(
network_id=neutron_network_id)
filtered_ports = [port for port in filtered_ports['ports']
if endpoint_id in port['name']]
for port in filtered_ports:
fixed_ips = port.get('fixed_ips', [])
for fixed_ip in fixed_ips:
concerned_subnet_ids.append(fixed_ip['subnet_id'])
app.neutron.delete_port(port['id'])
except n_exceptions.NeutronClientException as ex:
app.logger.error("Error happend during deleting a "
"Neutron ports: {0}".format(ex))
raise
for subnet_id in concerned_subnet_ids:
# If the subnet to be deleted has any port, when some ports are
# referring to the subnets in other words, delete_subnet throws an
# exception, SubnetInUse that extends Conflict. This can happen
# when the multiple Docker endpoints are created with the same
# subnet CIDR and it's totally the normal case. So we'd just log
# that and continue to proceed.
try:
app.neutron.delete_subnet(subnet_id)
except n_exceptions.Conflict as ex:
app.logger.info("The subnet with ID {0} is still referred "
"from other ports and it can't be deleted for "
"now.".format(subnet_id))
except n_exceptions.NeutronClientException as ex:
app.logger.error("Error happend during deleting a "
"Neutron subnets: {0}".format(ex))
raise
return jsonify(SCHEMA['SUCCESS'])
@app.route('/NetworkDriver.Join', methods=['POST'])

View File

@ -42,7 +42,6 @@ class TestKuryr(TestKuryrBase):
"""
@data(('/Plugin.Activate', SCHEMA['PLUGIN_ACTIVATE']),
('/NetworkDriver.EndpointOperInfo', SCHEMA['ENDPOINT_OPER_INFO']),
('/NetworkDriver.DeleteEndpoint', SCHEMA['SUCCESS']),
('/NetworkDriver.Join', SCHEMA['JOIN']),
('/NetworkDriver.Leave', SCHEMA['SUCCESS']))
@unpack
@ -251,3 +250,62 @@ class TestKuryr(TestKuryrBase):
decoded_json = jsonutils.loads(response.data)
expected = {'Interfaces': data['Interfaces']}
self.assertEqual(expected, decoded_json)
def test_network_driver_delete_endpoint(self):
docker_network_id = hashlib.sha256(
str(random.getrandbits(256))).hexdigest()
docker_endpoint_id = hashlib.sha256(
str(random.getrandbits(256))).hexdigest()
fake_neutron_network_id = str(uuid.uuid4())
self._mock_out_network(fake_neutron_network_id, docker_network_id)
fake_subnet_v4_id = "9436e561-47bf-436a-b1f1-fe23a926e031"
fake_subnet_v6_id = "64dd4a98-3d7a-4bfd-acf4-91137a8d2f51"
self.mox.StubOutWithMock(app.neutron, 'delete_subnet')
app.neutron.delete_subnet(fake_subnet_v4_id).AndReturn(None)
app.neutron.delete_subnet(fake_subnet_v6_id).AndReturn(None)
fake_neutron_port_id = '65c0ee9f-d634-4522-8954-51021b570b0d'
# The following fake response is retrieved from the Neutron doc:
# http://developer.openstack.org/api-ref-networking-v2.html#createPort # noqa
fake_ports = {
"ports": [{
"status": "DOWN",
"name": '-'.join([docker_endpoint_id, '0', 'port']),
"allowed_address_pairs": [],
"admin_state_up": True,
"network_id": fake_neutron_network_id,
"tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa",
"device_owner": "",
"mac_address": "fa:16:3e:20:57:c3",
"fixed_ips": [{
"subnet_id": fake_subnet_v4_id,
"ip_address": "192.168.1.2"
}, {
"subnet_id": fake_subnet_v6_id,
"ip_address": "fe80::f816:3eff:fe20:57c4"
}],
"id": fake_neutron_port_id,
"security_groups": [],
"device_id": ""
}]
}
self.mox.StubOutWithMock(app.neutron, 'list_ports')
app.neutron.list_ports(
network_id=fake_neutron_network_id).AndReturn(fake_ports)
self.mox.StubOutWithMock(app.neutron, 'delete_port')
app.neutron.delete_port(fake_neutron_port_id).AndReturn(None)
self.mox.ReplayAll()
data = {
'NetworkID': docker_network_id,
'EndpointID': docker_endpoint_id,
}
response = self.app.post('/NetworkDriver.DeleteEndpoint',
content_type='application/json',
data=jsonutils.dumps(data))
self.assertEqual(200, response.status_code)
decoded_json = jsonutils.loads(response.data)
self.assertEqual(SCHEMA['SUCCESS'], decoded_json)

View File

@ -20,20 +20,19 @@ from neutronclient.common import exceptions
from oslo_serialization import jsonutils
from kuryr import app
from kuryr import constants
from kuryr.tests.base import TestKuryrFailures
@ddt
class TestKuryrEndpointCreateFailures(TestKuryrFailures):
"""Unittests for the failures for creating endpoints.
class TestKuryrEndpointFailures(TestKuryrFailures):
"""Base class that has the methods commonly shared among endpoint tests.
This test covers error responses listed in the spec:
http://developer.openstack.org/api-ref-networking-v2.html#createSubnet # noqa
http://developer.openstack.org/api-ref-networking-v2-ext.html#createPort # noqa
This class mainly has the methods for mocking API calls against Neutron.
"""
@staticmethod
def _get_fake_subnets(docker_endpoint_id, neutron_network_id,
fake_neutron_subnet1_id, fake_neutron_subnet2_id):
fake_neutron_subnet_v4_id,
fake_neutron_subnet_v6_id):
# The following fake response is retrieved from the Neutron doc:
# http://developer.openstack.org/api-ref-networking-v2.html#createSubnet # noqa
fake_subnet_response = {
@ -46,7 +45,7 @@ class TestKuryrEndpointCreateFailures(TestKuryrFailures):
"gateway_ip": "192.168.1.1",
"ip_version": 4,
"cidr": "192.168.1.0/24",
"id": fake_neutron_subnet1_id,
"id": fake_neutron_subnet_v4_id,
"enable_dhcp": True
}, {
"name": '-'.join([docker_endpoint_id, 'fe80::']),
@ -57,16 +56,46 @@ class TestKuryrEndpointCreateFailures(TestKuryrFailures):
"gateway_ip": "fe80::f816:3eff:fe20:57c3",
"ip_version": 6,
"cidr": "fe80::/64",
"id": fake_neutron_subnet2_id,
"id": fake_neutron_subnet_v6_id,
"enable_dhcp": True
}]
}
return fake_subnet_response
@staticmethod
def _get_fake_ports(docker_endpoint_id, neutron_network_id,
fake_neutron_port_id,
fake_neutron_subnet_v4_id, fake_neutron_subnet_v6_id):
# The following fake response is retrieved from the Neutron doc:
# http://developer.openstack.org/api-ref-networking-v2.html#createPort # noqa
fake_ports = {
'ports': [{
"status": "DOWN",
"name": '-'.join([docker_endpoint_id, '0', 'port']),
"allowed_address_pairs": [],
"admin_state_up": True,
"network_id": neutron_network_id,
"tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa",
"device_owner": "",
"mac_address": "fa:16:3e:20:57:c3",
"fixed_ips": [{
"subnet_id": fake_neutron_subnet_v4_id,
"ip_address": "192.168.1.2"
}, {
"subnet_id": fake_neutron_subnet_v6_id,
"ip_address": "fe80::f816:3eff:fe20:57c4"
}],
"id": fake_neutron_port_id,
"security_groups": [],
"device_id": ""
}]
}
return fake_ports
def _create_subnet_with_exception(self, neutron_network_id,
docker_endpoint_id, ex):
fake_neutron_subnet1_id = str(uuid.uuid4())
fake_neutron_subnet2_id = str(uuid.uuid4())
fake_neutron_subnet_v4_id = str(uuid.uuid4())
fake_neutron_subnet_v6_id = str(uuid.uuid4())
self.mox.StubOutWithMock(app.neutron, 'create_subnet')
fake_subnet_request = {
@ -84,9 +113,9 @@ class TestKuryrEndpointCreateFailures(TestKuryrFailures):
"cidr": 'fe80::/64'
}]
}
fake_subnets = self.__class__._get_fake_subnets(
fake_subnets = super(self.__class__, self)._get_fake_subnets(
docker_endpoint_id, neutron_network_id,
fake_neutron_subnet1_id, fake_neutron_subnet2_id)
fake_neutron_subnet_v4_id, fake_neutron_subnet_v6_id)
if ex:
app.neutron.create_subnet(fake_subnet_request).AndRaise(ex)
@ -95,7 +124,7 @@ class TestKuryrEndpointCreateFailures(TestKuryrFailures):
fake_subnet_request).AndReturn(fake_subnets)
self.mox.ReplayAll()
return (fake_neutron_subnet1_id, fake_neutron_subnet2_id)
return (fake_neutron_subnet_v4_id, fake_neutron_subnet_v6_id)
def _delete_subnet_with_exception(self, neutron_subnet_id, ex):
self.mox.StubOutWithMock(app.neutron, 'delete_subnet')
@ -163,6 +192,23 @@ class TestKuryrEndpointCreateFailures(TestKuryrFailures):
app.neutron.create_port(fake_port_request).AndReturn(fake_port)
self.mox.ReplayAll()
def _delete_port_with_exception(self, neutron_port_id, ex):
self.mox.StubOutWithMock(app.neutron, "delete_port")
if ex:
app.neutron.delete_port(neutron_port_id).AndRaise(ex)
else:
app.neutron.delete_port(neutron_port_id).AndReturn(None)
self.mox.ReplayAll()
@ddt
class TestKuryrEndpointCreateFailures(TestKuryrEndpointFailures):
"""Unit tests for the failures for creating endpoints.
This test covers error responses listed in the spec:
http://developer.openstack.org/api-ref-networking-v2.html#createSubnet # noqa
http://developer.openstack.org/api-ref-networking-v2-ext.html#createPort # noqa
"""
def _invoke_create_request(self, docker_network_id, docker_endpoint_id):
data = {
'NetworkID': docker_network_id,
@ -226,17 +272,17 @@ class TestKuryrEndpointCreateFailures(TestKuryrFailures):
network_id=fake_neutron_network_id,
cidr='fe80::f816:3eff:fe20:57c4/64').AndReturn({'subnets': []})
(fake_neutron_subnet1_id,
fake_neutron_subnet2_id) = self._create_subnet_with_exception(
(fake_neutron_subnet_v4_id,
fake_neutron_subnet_v6_id) = self._create_subnet_with_exception(
fake_neutron_network_id, fake_docker_endpoint_id, None)
self._create_port_with_exception(fake_neutron_network_id,
fake_docker_endpoint_id, fake_neutron_subnet1_id,
fake_neutron_subnet2_id, GivenException())
fake_docker_endpoint_id, fake_neutron_subnet_v4_id,
fake_neutron_subnet_v6_id, GivenException())
self._mock_out_network(fake_neutron_network_id, fake_docker_network_id)
# The port creation is failed and Kuryr rolles the created subnet back.
self._delete_subnets_with_exception(
[fake_neutron_subnet1_id, fake_neutron_subnet2_id], None)
[fake_neutron_subnet_v4_id, fake_neutron_subnet_v6_id], None)
response = self._invoke_create_request(
fake_docker_network_id, fake_docker_endpoint_id)
@ -245,3 +291,92 @@ class TestKuryrEndpointCreateFailures(TestKuryrFailures):
decoded_json = jsonutils.loads(response.data)
self.assertTrue('Err' in decoded_json)
self.assertEqual({'Err': GivenException.message}, decoded_json)
@ddt
class TestKuryrEndpointDeleteFailures(TestKuryrEndpointFailures):
"""Unit tests for the failures for deleting endpoints.
This test covers error responses listed in the spec:
http://developer.openstack.org/api-ref-networking-v2-ext.html#deleteProviderNetwork # noqa
"""
def _invoke_delete_request(self, docker_network_id, docker_endpoint_id):
data = {'NetworkID': docker_network_id,
'EndpointID': docker_endpoint_id}
response = self.app.post('/NetworkDriver.DeleteEndpoint',
content_type='application/json',
data=jsonutils.dumps(data))
return response
@data(exceptions.Unauthorized, exceptions.NotFound, exceptions.Conflict)
def test_delete_endpoint_subnet_failures(self, GivenException):
fake_docker_network_id = hashlib.sha256(
str(random.getrandbits(256))).hexdigest()
fake_docker_endpoint_id = hashlib.sha256(
str(random.getrandbits(256))).hexdigest()
fake_neutron_network_id = str(uuid.uuid4())
fake_neutron_port_id = str(uuid.uuid4())
fake_neutron_subnet_v4_id = str(uuid.uuid4())
fake_neutron_subnet_v6_id = str(uuid.uuid4())
self._mock_out_network(fake_neutron_network_id, fake_docker_network_id)
fake_ports = super(self.__class__, self)._get_fake_ports(
fake_docker_endpoint_id, fake_neutron_network_id,
fake_neutron_port_id,
fake_neutron_subnet_v4_id, fake_neutron_subnet_v6_id)
self.mox.StubOutWithMock(app.neutron, 'list_ports')
app.neutron.list_ports(
network_id=fake_neutron_network_id).AndReturn(fake_ports)
self.mox.StubOutWithMock(app.neutron, 'delete_port')
app.neutron.delete_port(fake_neutron_port_id).AndReturn(None)
self.mox.ReplayAll()
if GivenException is exceptions.Conflict:
self._delete_subnets_with_exception(
[fake_neutron_subnet_v4_id, fake_neutron_subnet_v6_id],
GivenException())
else:
self._delete_subnet_with_exception(
fake_neutron_subnet_v4_id, GivenException())
response = self._invoke_delete_request(
fake_docker_network_id, fake_docker_endpoint_id)
if GivenException is exceptions.Conflict:
self.assertEqual(200, response.status_code)
decoded_json = jsonutils.loads(response.data)
self.assertEqual(constants.SCHEMA['SUCCESS'], decoded_json)
else:
self.assertEqual(GivenException.status_code, response.status_code)
decoded_json = jsonutils.loads(response.data)
self.assertTrue('Err' in decoded_json)
self.assertEqual({'Err': GivenException.message}, decoded_json)
@data(exceptions.Unauthorized, exceptions.NotFound, exceptions.Conflict)
def test_delete_endpiont_port_failures(self, GivenException):
fake_docker_network_id = hashlib.sha256(
str(random.getrandbits(256))).hexdigest()
fake_docker_endpoint_id = hashlib.sha256(
str(random.getrandbits(256))).hexdigest()
fake_neutron_network_id = str(uuid.uuid4())
fake_neutron_subnet_v4_id = str(uuid.uuid4())
fake_neutron_subnet_v6_id = str(uuid.uuid4())
fake_neutron_port_id = str(uuid.uuid4())
self._mock_out_network(fake_neutron_network_id, fake_docker_network_id)
self.mox.StubOutWithMock(app.neutron, 'list_ports')
fake_ports = super(self.__class__, self)._get_fake_ports(
fake_docker_endpoint_id, fake_neutron_network_id,
fake_neutron_port_id,
fake_neutron_subnet_v4_id, fake_neutron_subnet_v6_id)
app.neutron.list_ports(
network_id=fake_neutron_network_id).AndReturn(fake_ports)
self._delete_port_with_exception(fake_neutron_port_id, GivenException)
response = self._invoke_delete_request(
fake_docker_network_id, fake_docker_endpoint_id)
self.assertEqual(GivenException.status_code, response.status_code)
decoded_json = jsonutils.loads(response.data)
self.assertTrue('Err' in decoded_json)
self.assertEqual({'Err': GivenException.message}, decoded_json)