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:
parent
daaa97fbda
commit
c7e49bb878
|
@ -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'])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue