diff --git a/api-ref/source/containers.inc b/api-ref/source/containers.inc index 69573bb9b..fb9108ccd 100644 --- a/api-ref/source/containers.inc +++ b/api-ref/source/containers.inc @@ -774,3 +774,38 @@ This request does not return anything in the response body. .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id + + +Attach a network to a container +=============================== + +.. rest_method:: POST /v1/containers/{container_ident}/network_attach?network={network} + +Attach a network to a container. + +Response Codes +-------------- + +.. rest_status_code:: success status.yaml + + - 202 + +.. rest_status_code:: error status.yaml + + - 401 + - 403 + - 404 + +.. rest_parameters:: parameters.yaml + + - container_ident: container_ident + - network: network + +Response +-------- + +This request does not return anything in the response body. + +.. rest_parameters:: parameters.yaml + + - X-Openstack-Request-Id: request_id diff --git a/etc/zun/policy.json b/etc/zun/policy.json index b0a94cf83..7637b196a 100644 --- a/etc/zun/policy.json +++ b/etc/zun/policy.json @@ -32,6 +32,7 @@ "container:commit": "rule:default", "container:add_security_group": "rule:default", "container:network_detach": "rule:default", + "container:network_attach": "rule:default", "image:pull": "rule:default", "image:get_all": "rule:default", diff --git a/zun/api/controllers/v1/containers.py b/zun/api/controllers/v1/containers.py index 8d21175fb..fa126267c 100644 --- a/zun/api/controllers/v1/containers.py +++ b/zun/api/controllers/v1/containers.py @@ -114,7 +114,8 @@ class ContainersController(base.Controller): 'stats': ['GET'], 'commit': ['POST'], 'add_security_group': ['POST'], - 'network_detach': ['POST'] + 'network_detach': ['POST'], + 'network_attach': ['POST'] } @pecan.expose('json') @@ -765,3 +766,17 @@ class ContainersController(base.Controller): neutron_net = neutron_api.get_neutron_network(kwargs.get('network')) compute_api.network_detach(context, container, neutron_net['id']) pecan.response.status = 202 + + @base.Controller.api_version("1.8") + @pecan.expose('json') + @exception.wrap_pecan_controller_exception + @validation.validate_query_param(pecan.request, schema.network_attach) + def network_attach(self, container_id, **kwargs): + container = _get_container(container_id) + check_policy_on_container(container.as_dict(), + "container:network_attach") + context = pecan.request.context + compute_api = pecan.request.compute_api + neutron_api = neutron.NeutronAPI(context) + neutron_net = neutron_api.get_neutron_network(kwargs.get('network')) + compute_api.network_attach(context, container, neutron_net['id']) diff --git a/zun/api/controllers/v1/schemas/containers.py b/zun/api/controllers/v1/schemas/containers.py index 2f8692d5d..fccb013e6 100644 --- a/zun/api/controllers/v1/schemas/containers.py +++ b/zun/api/controllers/v1/schemas/containers.py @@ -180,3 +180,5 @@ network_detach = { 'required': ['network'], 'additionalProperties': False } + +network_attach = copy.deepcopy(network_detach) diff --git a/zun/api/controllers/versions.py b/zun/api/controllers/versions.py index 7e5724824..7065d3c65 100644 --- a/zun/api/controllers/versions.py +++ b/zun/api/controllers/versions.py @@ -40,10 +40,11 @@ REST_API_VERSION_HISTORY = """REST API Version History: * 1.5 - Add runtime to container * 1.6 - Support detach network from a container * 1.7 - Disallow non-admin users to force delete containers + * 1.8 - Support attach a network to a container """ BASE_VER = '1.1' -CURRENT_MAX_VER = '1.7' +CURRENT_MAX_VER = '1.8' class Version(object): diff --git a/zun/api/rest_api_version_history.rst b/zun/api/rest_api_version_history.rst index abbd2d83e..c6f7f491c 100644 --- a/zun/api/rest_api_version_history.rst +++ b/zun/api/rest_api_version_history.rst @@ -70,3 +70,9 @@ user documentation. Disallow non-admin users to force delete containers Only Admin User can use "delete --force" to force delete a container. + +1.8 +--- + + Add attach a network to a container. + Users can use this api to attach a neutron network to a container. diff --git a/zun/compute/api.py b/zun/compute/api.py index 6bbb263bc..394d3a185 100644 --- a/zun/compute/api.py +++ b/zun/compute/api.py @@ -134,3 +134,6 @@ class API(object): def network_detach(self, context, container, *args): return self.rpcapi.network_detach(context, container, *args) + + def network_attach(self, context, container, *args): + return self.rpcapi.network_attach(context, container, *args) diff --git a/zun/compute/manager.py b/zun/compute/manager.py index d7a950087..235efe4f6 100644 --- a/zun/compute/manager.py +++ b/zun/compute/manager.py @@ -722,3 +722,12 @@ class Manager(periodic_task.PeriodicTasks): except Exception as e: with excutils.save_and_reraise_exception(reraise=False): LOG.exception("Unexpected exception: %s", six.text_type(e)) + + def network_attach(self, context, container, network): + LOG.debug('Attach network: %(network)s to container: %(container)s.', + {'container': container, 'network': network}) + try: + self.driver.network_attach(context, container, network) + except Exception as e: + with excutils.save_and_reraise_exception(reraise=False): + LOG.exception("Unexpected exception: %s", six.text_type(e)) diff --git a/zun/compute/rpcapi.py b/zun/compute/rpcapi.py index 754e0c5c8..777311e57 100644 --- a/zun/compute/rpcapi.py +++ b/zun/compute/rpcapi.py @@ -181,3 +181,7 @@ class API(rpc_service.API): def network_detach(self, context, container, network): return self._call(container.host, 'network_detach', container=container, network=network) + + def network_attach(self, context, container, network): + return self._call(container.host, 'network_attach', + container=container, network=network) diff --git a/zun/container/docker/driver.py b/zun/container/docker/driver.py index 3f12e76f9..d4bff8616 100644 --- a/zun/container/docker/driver.py +++ b/zun/container/docker/driver.py @@ -771,6 +771,35 @@ class DockerDriver(driver.ContainerDriver): container.addresses = update container.save(context) + def network_attach(self, context, container, network): + with docker_utils.docker_client() as docker: + network_api = zun_network.api(context, + docker_api=docker) + if network in container.addresses: + raise exception.ZunException('Container %(container)s has' + ' alreay connected to the network' + '%(network)s.' + % {'container': container['uuid'], + 'network': network}) + self._get_or_create_docker_network(context, network_api, network) + requested_network = {'network': network, + 'port': '', + 'v4-fixed-ip': '', + 'v6-fixed-ip': ''} + docker_net_name = self._get_docker_network_name(context, network) + addrs = network_api.connect_container_to_network( + container, docker_net_name, requested_network, + security_groups=None) + if addrs is None: + raise exception.ZunException(_( + 'Unexpected missing of addresses')) + update = {} + update[network] = addrs + addresses = container.addresses + addresses.update(update) + container.addresses = addresses + container.save(context) + class NovaDockerDriver(DockerDriver): capabilities = { diff --git a/zun/container/driver.py b/zun/container/driver.py index b70a95993..617c18d48 100644 --- a/zun/container/driver.py +++ b/zun/container/driver.py @@ -231,3 +231,6 @@ class ContainerDriver(object): def network_detach(self, context, container, network): raise NotImplementedError() + + def network_attach(self, context, container, network): + raise NotImplementedError() diff --git a/zun/tests/unit/api/controllers/test_root.py b/zun/tests/unit/api/controllers/test_root.py index b3a415471..2ef4ecbac 100644 --- a/zun/tests/unit/api/controllers/test_root.py +++ b/zun/tests/unit/api/controllers/test_root.py @@ -17,7 +17,7 @@ import webtest from zun.api import app from zun.tests.unit.api import base as api_base -CURRENT_VERSION = "container 1.7" +CURRENT_VERSION = "container 1.8" class TestRootController(api_base.FunctionalTest): @@ -27,7 +27,7 @@ class TestRootController(api_base.FunctionalTest): 'default_version': {'id': 'v1', 'links': [{'href': 'http://localhost/v1/', 'rel': 'self'}], - 'max_version': '1.7', + 'max_version': '1.8', 'min_version': '1.1', 'status': 'CURRENT'}, 'description': 'Zun is an OpenStack project which ' @@ -35,7 +35,7 @@ class TestRootController(api_base.FunctionalTest): 'versions': [{'id': 'v1', 'links': [{'href': 'http://localhost/v1/', 'rel': 'self'}], - 'max_version': '1.7', + 'max_version': '1.8', 'min_version': '1.1', 'status': 'CURRENT'}]} diff --git a/zun/tests/unit/api/controllers/v1/test_containers.py b/zun/tests/unit/api/controllers/v1/test_containers.py index 39c673db2..4f9692e01 100644 --- a/zun/tests/unit/api/controllers/v1/test_containers.py +++ b/zun/tests/unit/api/controllers/v1/test_containers.py @@ -23,7 +23,7 @@ from zun.tests.unit.api import base as api_base from zun.tests.unit.db import utils from zun.tests.unit.objects import utils as obj_utils -CURRENT_VERSION = "container 1.7" +CURRENT_VERSION = "container 1.8" class TestContainerController(api_base.FunctionalTest): diff --git a/zun/tests/unit/compute/test_compute_manager.py b/zun/tests/unit/compute/test_compute_manager.py index bd255940c..3262916c3 100644 --- a/zun/tests/unit/compute/test_compute_manager.py +++ b/zun/tests/unit/compute/test_compute_manager.py @@ -624,3 +624,8 @@ class TestManager(base.TestCase): container = Container(self.context, **utils.get_test_container()) self.compute_manager.network_detach(self.context, container, 'network') mock_detach.assert_called_once_with(self.context, container, mock.ANY) + + @mock.patch.object(fake_driver, 'network_attach') + def test_container_network_attach(self, mock_attach): + container = Container(self.context, **utils.get_test_container()) + self.compute_manager.network_attach(self.context, container, 'network') diff --git a/zun/tests/unit/container/docker/test_docker_driver.py b/zun/tests/unit/container/docker/test_docker_driver.py index 4ebba6502..43a95ad13 100644 --- a/zun/tests/unit/container/docker/test_docker_driver.py +++ b/zun/tests/unit/container/docker/test_docker_driver.py @@ -475,6 +475,26 @@ class TestDockerDriver(base.DriverTestCase): 'network-fake_project', mock.ANY) + @mock.patch('zun.network.kuryr_network.KuryrNetwork' + '.connect_container_to_network') + @mock.patch('zun.network.kuryr_network.KuryrNetwork' + '.disconnect_container_from_network') + @mock.patch('zun.network.kuryr_network.KuryrNetwork' + '.list_networks') + def test_network_attach(self, mock_list, mock_disconnect, mock_connect): + mock_container = mock.MagicMock() + mock_container.security_groups = None + mock_list.return_value = {'network': 'network'} + requested_network = [{'network': 'network', + 'port': '', + 'v4-fixed-ip': '', + 'v6-fixed-ip': ''}] + self.driver.network_attach(self.context, mock_container, 'network') + mock_connect.assert_called_once_with(mock_container, + 'network-fake_project', + requested_network[0], + security_groups=None) + class TestNovaDockerDriver(base.DriverTestCase): def setUp(self):