diff --git a/kuryr/binding.py b/kuryr/binding.py index 06bece29..2fcc2ac6 100644 --- a/kuryr/binding.py +++ b/kuryr/binding.py @@ -31,6 +31,7 @@ IP_ADDRESS_KEY = 'ip_address' KIND_VETH = 'veth' MAC_ADDRESS_KEY = 'mac_address' SUBNET_ID_KEY = 'subnet_id' +UNBINDING_SUBCOMMAND = 'unbind' VETH_POSTFIX = '-veth' VIF_TYPE_KEY = 'binding:vif_type' @@ -122,3 +123,30 @@ def port_bind(endpoint_id, neutron_port, neutron_subnets): cleanup_veth(ifname) return (ifname, peer_name, (stdout, stderr)) + + +def port_unbind(endpoint_id, neutron_port): + """Unbinds the Neutorn port from the network interface on the host. + + :param endpoint_id: the ID of the Docker container as string + :param neutron_port: a port dictionary returned from python-neutronclient + :returns: the tuple of stdout and stderr returned by processutils.execute + invoked with the executable script for unbinding + :raises: processutils.ProcessExecutionError, pyroute2.netlink.NetlinkError + """ + # NOTE(tfukushima): pyroute2.netlink requires Linux to be imported. So I + # don't import it in the module scope but here. + import pyroute2.netlink + + vif_type = neutron_port.get(VIF_TYPE_KEY, FALLBACK_VIF_TYPE) + unbinding_exec_path = os.path.join(config.CONF.bindir, vif_type) + port_id = neutron_port['id'] + stdout, stderr = processutils.execute( + unbinding_exec_path, UNBINDING_SUBCOMMAND, port_id, run_as_root=True) + ifname = endpoint_id[:8] + VETH_POSTFIX + try: + cleanup_veth(ifname) + except pyroute2.netlink.NetlinkError: + raise exceptions.VethDeleteionFailure( + 'Deleting the veth pair was failed.') + return (stdout, stderr) diff --git a/kuryr/common/exceptions.py b/kuryr/common/exceptions.py index c142cbb0..6e034693 100644 --- a/kuryr/common/exceptions.py +++ b/kuryr/common/exceptions.py @@ -45,3 +45,11 @@ class VethCreationFailure(KuryrException): This exception is thrown when the veth pair is not created appropriately and Kuryr can't proceed the binding further. """ + + +class VethDeletionFailure(KuryrException): + """Exception represents the veth pair deletion is failed. + + This exception is thrown when the veth pair is not deleted appropriately + and Kuryr can't proceed the unbinding further. + """ diff --git a/kuryr/controllers.py b/kuryr/controllers.py index 26b05ff0..d58cf0a7 100644 --- a/kuryr/controllers.py +++ b/kuryr/controllers.py @@ -637,5 +637,36 @@ def network_driver_leave(): app.logger.debug("Received JSON data {0} for /NetworkDriver.DeleteEndpoint" .format(json_data)) jsonschema.validate(json_data, schemata.LEAVE_SCHEMA) + neutron_network_name = json_data['NetworkID'] + endpoint_id = json_data['EndpointID'] + + filtered_networks = _get_networks_by_attrs(name=neutron_network_name) + + if not filtered_networks: + return flask.jsonify({ + 'Err': "Neutron network associated with ID {0} doesn't exit." + .format(neutron_network_name) + }) + else: + neutron_port_name = '-'.join([endpoint_id, 'port']) + filtered_ports = _get_ports_by_attrs(name=neutron_port_name) + if not filtered_ports: + raise exceptions.NoResourceException( + "The port doesn't exist for the name {0}" + .format(neutron_port_name)) + neutron_port = filtered_ports[0] + try: + stdout, stderr = binding.port_unbind(endpoint_id, neutron_port) + app.logger.debug(stdout) + if stderr: + app.logger.error(stderr) + except processutils.ProcessExecutionError: + with excutils.save_and_reraise_exception(): + app.logger.error( + 'Could not unbind the Neutron port from the veth ' + 'endpoint.') + except exceptions.VethDeletionFailure: + with excutils.save_and_reraise_exception(): + app.logger.error('Cleaning the veth pair up was failed.') return flask.jsonify(constants.SCHEMA['SUCCESS']) diff --git a/kuryr/tests/base.py b/kuryr/tests/base.py index 05730b8d..517480c3 100644 --- a/kuryr/tests/base.py +++ b/kuryr/tests/base.py @@ -47,6 +47,14 @@ class TestKuryrBase(TestCase): self.mox.ReplayAll() return fake_binding_response + def _mock_out_unbinding(self, endpoint_id, neutron_port): + self.mox.StubOutWithMock(binding, 'port_unbind') + fake_unbinding_response = ('fake stdout', '') + binding.port_unbind(endpoint_id, neutron_port).AndReturn( + fake_unbinding_response) + self.mox.ReplayAll() + return fake_unbinding_response + def _mock_out_network(self, neutron_network_id, docker_network_id): fake_list_response = { "networks": [{ diff --git a/kuryr/tests/test_kuryr.py b/kuryr/tests/test_kuryr.py index 4f7a4957..f58d5a2c 100644 --- a/kuryr/tests/test_kuryr.py +++ b/kuryr/tests/test_kuryr.py @@ -539,3 +539,39 @@ class TestKuryr(base.TestKuryrBase): 'StaticRoutes': [] } self.assertEqual(expected_response, decoded_json) + + def test_network_driver_leave(self): + 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()) + self._mock_out_network(fake_neutron_network_id, fake_docker_network_id) + fake_neutron_port_id = str(uuid.uuid4()) + self.mox.StubOutWithMock(app.neutron, 'list_ports') + neutron_port_name = utils.get_neutron_port_name( + fake_docker_endpoint_id) + fake_neutron_v4_subnet_id = str(uuid.uuid4()) + fake_neutron_v6_subnet_id = str(uuid.uuid4()) + fake_neutron_ports_response = self._get_fake_ports( + fake_docker_endpoint_id, fake_neutron_network_id, + fake_neutron_port_id, + fake_neutron_v4_subnet_id, fake_neutron_v6_subnet_id) + app.neutron.list_ports(name=neutron_port_name).AndReturn( + fake_neutron_ports_response) + + fake_neutron_port = fake_neutron_ports_response['ports'][0] + self._mock_out_unbinding(fake_docker_endpoint_id, fake_neutron_port) + + leave_request = { + 'NetworkID': fake_docker_network_id, + 'EndpointID': fake_docker_endpoint_id, + } + response = self.app.post('/NetworkDriver.Leave', + content_type='application/json', + data=jsonutils.dumps(leave_request)) + + self.assertEqual(200, response.status_code) + decoded_json = jsonutils.loads(response.data) + self.assertEqual(constants.SCHEMA['SUCCESS'], decoded_json) diff --git a/kuryr/tests/test_leave.py b/kuryr/tests/test_leave.py index 582c9fb8..03553501 100644 --- a/kuryr/tests/test_leave.py +++ b/kuryr/tests/test_leave.py @@ -12,13 +12,21 @@ import hashlib import random +import uuid +import ddt +from oslo_concurrency import processutils from oslo_serialization import jsonutils from werkzeug import exceptions as w_exceptions +from kuryr import app +from kuryr import binding +from kuryr.common import exceptions from kuryr.tests import base +from kuryr import utils +@ddt.ddt class TestKuryrLeaveFailures(base.TestKuryrFailures): """Unit tests for the failures for unbinding a Neutron port. """ @@ -34,6 +42,57 @@ class TestKuryrLeaveFailures(base.TestKuryrFailures): return response + def _port_unbind_with_exception(self, docker_endpoint_id, + neutron_port, ex): + fake_unbinding_response = ('fake stdout', '') + self.mox.StubOutWithMock(binding, 'port_unbind') + if ex: + binding.port_unbind(docker_endpoint_id, neutron_port).AndRaise(ex) + else: + binding.port_unbind(docker_endpoint_id, neutron_port).AndReturn( + fake_unbinding_response) + self.mox.ReplayAll() + + return fake_unbinding_response + + @ddt.data(exceptions.VethDeletionFailure, + processutils.ProcessExecutionError) + def test_leave_unbinding_failure(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()) + self._mock_out_network(fake_neutron_network_id, fake_docker_network_id) + fake_neutron_port_id = str(uuid.uuid4()) + self.mox.StubOutWithMock(app.neutron, 'list_ports') + neutron_port_name = utils.get_neutron_port_name( + fake_docker_endpoint_id) + fake_neutron_v4_subnet_id = str(uuid.uuid4()) + fake_neutron_v6_subnet_id = str(uuid.uuid4()) + fake_neutron_ports_response = self._get_fake_ports( + fake_docker_endpoint_id, fake_neutron_network_id, + fake_neutron_port_id, + fake_neutron_v4_subnet_id, fake_neutron_v6_subnet_id) + app.neutron.list_ports(name=neutron_port_name).AndReturn( + fake_neutron_ports_response) + fake_neutron_port = fake_neutron_ports_response['ports'][0] + + fake_message = "fake message" + fake_exception = GivenException(fake_message) + self._port_unbind_with_exception( + fake_docker_endpoint_id, fake_neutron_port, fake_exception) + + response = self._invoke_leave_request( + fake_docker_network_id, fake_docker_endpoint_id) + + self.assertEqual( + w_exceptions.InternalServerError.code, response.status_code) + decoded_json = jsonutils.loads(response.data) + self.assertTrue('Err' in decoded_json) + self.assertTrue(fake_message in decoded_json['Err']) + def test_leave_bad_request(self): fake_docker_network_id = hashlib.sha256( str(random.getrandbits(256))).hexdigest() diff --git a/usr/libexec/kuryr/midonet b/usr/libexec/kuryr/midonet index cef2eda7..cd9da513 100755 --- a/usr/libexec/kuryr/midonet +++ b/usr/libexec/kuryr/midonet @@ -16,12 +16,22 @@ bind_port() { mm-ctl --bind-port $1 $2 } +unbind_port() { + echo "Unbinding Neutron port $1..." + mm-ctl --unbind-port $1 +} + case $1 in "bind") shift bind_port "$@" exit 0 ;; + "unbind") + shift + unbind_port "$@" + exit 0 + ;; *) echo >&2 "$0: Invalid command $1." exit 1