diff --git a/doc/v3/api_samples/os-networks-associate/network-associate-host-req.json b/doc/v3/api_samples/os-networks-associate/network-associate-host-req.json new file mode 100644 index 000000000000..a6487211ee5d --- /dev/null +++ b/doc/v3/api_samples/os-networks-associate/network-associate-host-req.json @@ -0,0 +1,3 @@ +{ + "associate_host": "testHost" +} \ No newline at end of file diff --git a/doc/v3/api_samples/os-networks-associate/network-disassociate-host-req.json b/doc/v3/api_samples/os-networks-associate/network-disassociate-host-req.json new file mode 100644 index 000000000000..d6c5419fd17a --- /dev/null +++ b/doc/v3/api_samples/os-networks-associate/network-disassociate-host-req.json @@ -0,0 +1,3 @@ +{ + "disassociate_host": null +} \ No newline at end of file diff --git a/doc/v3/api_samples/os-networks-associate/network-disassociate-project-req.json b/doc/v3/api_samples/os-networks-associate/network-disassociate-project-req.json new file mode 100644 index 000000000000..6c0e46730130 --- /dev/null +++ b/doc/v3/api_samples/os-networks-associate/network-disassociate-project-req.json @@ -0,0 +1,3 @@ +{ + "disassociate_project": null +} \ No newline at end of file diff --git a/doc/v3/api_samples/os-networks-associate/network-disassociate-req.json b/doc/v3/api_samples/os-networks-associate/network-disassociate-req.json new file mode 100644 index 000000000000..66ab7cef0417 --- /dev/null +++ b/doc/v3/api_samples/os-networks-associate/network-disassociate-req.json @@ -0,0 +1,3 @@ +{ + "disassociate": null +} \ No newline at end of file diff --git a/etc/nova/policy.json b/etc/nova/policy.json index 6718563865d7..80f42427f96d 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -210,6 +210,8 @@ "compute_extension:v3:os-networks:view": "", "compute_extension:v3:os-networks:discoverable": "", "compute_extension:networks_associate": "rule:admin_api", + "compute_extension:v3:os-networks-associate": "rule:admin_api", + "compute_extension:v3:os-networks-associate:discoverable": "", "compute_extension:v3:os-pause-server:discoverable": "", "compute_extension:v3:os-pause-server:pause": "rule:admin_or_owner", "compute_extension:v3:os-pause-server:unpause": "rule:admin_or_owner", diff --git a/nova/api/openstack/compute/plugins/v3/networks_associate.py b/nova/api/openstack/compute/plugins/v3/networks_associate.py new file mode 100644 index 000000000000..ac7df461626f --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/networks_associate.py @@ -0,0 +1,102 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from webob import exc + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova import exception +from nova.i18n import _ +from nova import network +from nova.openstack.common import log as logging + +ALIAS = "os-networks-associate" + + +LOG = logging.getLogger(__name__) +authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS) + + +class NetworkAssociateActionController(wsgi.Controller): + """Network Association API Controller.""" + + def __init__(self, network_api=None): + self.network_api = network_api or network.API() + + @wsgi.action("disassociate_host") + @wsgi.response(202) + @extensions.expected_errors((404, 501)) + def _disassociate_host_only(self, req, id, body): + context = req.environ['nova.context'] + authorize(context) + LOG.debug("Disassociating host with network with id %s", id) + try: + self.network_api.associate(context, id, host=None) + except exception.NetworkNotFound: + msg = _("Network not found") + raise exc.HTTPNotFound(explanation=msg) + except NotImplementedError: + msg = _('Disassociate host is not implemented by the configured ' + 'Network API') + raise exc.HTTPNotImplemented(explanation=msg) + + @wsgi.action("disassociate_project") + @wsgi.response(202) + @extensions.expected_errors((404, 501)) + def _disassociate_project_only(self, req, id, body): + context = req.environ['nova.context'] + authorize(context) + LOG.debug("Disassociating project with network with id %s", id) + try: + self.network_api.associate(context, id, project=None) + except exception.NetworkNotFound: + msg = _("Network not found") + raise exc.HTTPNotFound(explanation=msg) + except NotImplementedError: + msg = _('Disassociate project is not implemented by the ' + 'configured Network API') + raise exc.HTTPNotImplemented(explanation=msg) + + @wsgi.action("associate_host") + @wsgi.response(202) + @extensions.expected_errors((404, 501)) + def _associate_host(self, req, id, body): + context = req.environ['nova.context'] + authorize(context) + + try: + self.network_api.associate(context, id, + host=body['associate_host']) + except exception.NetworkNotFound: + msg = _("Network not found") + raise exc.HTTPNotFound(explanation=msg) + except NotImplementedError: + msg = _('Associate host is not implemented by the configured ' + 'Network API') + raise exc.HTTPNotImplemented(explanation=msg) + + +class NetworksAssociate(extensions.V3APIExtensionBase): + """Network association support.""" + + name = "NetworkAssociationSupport" + alias = ALIAS + version = 1 + + def get_controller_extensions(self): + extension = extensions.ControllerExtension( + self, 'os-networks', NetworkAssociateActionController()) + + return [extension] + + def get_resources(self): + return [] diff --git a/nova/tests/api/openstack/compute/contrib/test_networks.py b/nova/tests/api/openstack/compute/contrib/test_networks.py index 96ebc369e7d4..9a23719b4334 100644 --- a/nova/tests/api/openstack/compute/contrib/test_networks.py +++ b/nova/tests/api/openstack/compute/contrib/test_networks.py @@ -29,6 +29,8 @@ from nova.api.openstack.compute.contrib import networks_associate from nova.api.openstack.compute.contrib import os_networks as networks from nova.api.openstack.compute.contrib import os_tenant_networks as tnet from nova.api.openstack.compute.plugins.v3 import networks as networks_v21 +from nova.api.openstack.compute.plugins.v3 import networks_associate as \ + networks_associate_v21 from nova.api.openstack.compute.plugins.v3 import tenant_networks as tnet_v21 from nova.api.openstack import extensions import nova.context @@ -489,27 +491,31 @@ class NetworksTestV2(NetworksTestV21): self.controller.create(req, net) -class NetworksAssociateTest(test.NoDBTestCase): +class NetworksAssociateTestV21(test.NoDBTestCase): def setUp(self): - super(NetworksAssociateTest, self).setUp() + super(NetworksAssociateTestV21, self).setUp() self.fake_network_api = FakeNetworkAPI() - ext_mgr = extensions.ExtensionManager() - ext_mgr.extensions = {'os-extended-networks': 'fake'} - self.controller = networks.NetworkController( - self.fake_network_api, - ext_mgr) - self.associate_controller = networks_associate\ - .NetworkAssociateActionController(self.fake_network_api) + self._setup() fakes.stub_out_networking(self.stubs) fakes.stub_out_rate_limiting(self.stubs) + def _setup(self): + self.controller = networks.NetworkController(self.fake_network_api) + self.associate_controller = networks_associate_v21\ + .NetworkAssociateActionController(self.fake_network_api) + + def _check_status(self, res, method, code): + self.assertEqual(method.wsgi_code, code) + def test_network_disassociate_host_only(self): uuid = FAKE_NETWORKS[0]['uuid'] req = fakes.HTTPRequest.blank('/v2/1234/os-networks/%s/action' % uuid) res = self.associate_controller._disassociate_host_only( req, uuid, {'disassociate_host': None}) - self.assertEqual(res.status_int, 202) + self._check_status(res, + self.associate_controller._disassociate_host_only, + 202) self.assertIsNotNone(self.fake_network_api.networks[0]['project_id']) self.assertIsNone(self.fake_network_api.networks[0]['host']) @@ -518,16 +524,17 @@ class NetworksAssociateTest(test.NoDBTestCase): req = fakes.HTTPRequest.blank('/v2/1234/os-networks/%s/action' % uuid) res = self.associate_controller._disassociate_project_only( req, uuid, {'disassociate_project': None}) - self.assertEqual(res.status_int, 202) + self._check_status( + res, self.associate_controller._disassociate_project_only, 202) self.assertIsNone(self.fake_network_api.networks[0]['project_id']) self.assertIsNotNone(self.fake_network_api.networks[0]['host']) def test_network_associate_with_host(self): uuid = FAKE_NETWORKS[1]['uuid'] - req = fakes.HTTPRequest.blank('/v2/1234/os-networks/%s/action' % uuid) + req = fakes.HTTPRequest.blank('/v2/1234//os-networks/%s/action' % uuid) res = self.associate_controller._associate_host( req, uuid, {'associate_host': "TestHost"}) - self.assertEqual(res.status_int, 202) + self._check_status(res, self.associate_controller._associate_host, 202) req = fakes.HTTPRequest.blank('/v2/1234/os-networks/%s' % uuid) req.environ["nova.context"].is_admin = True res_dict = self.controller.show(req, uuid) @@ -563,6 +570,21 @@ class NetworksAssociateTest(test.NoDBTestCase): req, uuid, {'disassociate_host': None}) +class NetworksAssociateTestV2(NetworksAssociateTestV21): + + def _setup(self): + ext_mgr = extensions.ExtensionManager() + ext_mgr.extensions = {'os-extended-networks': 'fake'} + self.controller = networks.NetworkController( + self.fake_network_api, + ext_mgr) + self.associate_controller = networks_associate\ + .NetworkAssociateActionController(self.fake_network_api) + + def _check_status(self, res, method, code): + self.assertEqual(res.status_int, 202) + + class TenantNetworksTestV21(test.NoDBTestCase): ctrlr = tnet_v21.TenantNetworkController diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py index 4fd56c2c563a..7321ff11897e 100644 --- a/nova/tests/fake_policy.py +++ b/nova/tests/fake_policy.py @@ -255,6 +255,7 @@ policy_data = """ "compute_extension:v3:os-networks": "", "compute_extension:v3:os-networks:view": "", "compute_extension:networks_associate": "", + "compute_extension:v3:os-networks-associate": "", "compute_extension:os-tenant-networks": "", "compute_extension:v3:os-tenant-networks": "", "compute_extension:v3:os-pause-server:pause": "", diff --git a/nova/tests/integrated/v3/api_samples/os-networks-associate/network-associate-host-req.json.tpl b/nova/tests/integrated/v3/api_samples/os-networks-associate/network-associate-host-req.json.tpl new file mode 100644 index 000000000000..762e8817518e --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/os-networks-associate/network-associate-host-req.json.tpl @@ -0,0 +1,3 @@ +{ + "associate_host": "%(host)s" +} diff --git a/nova/tests/integrated/v3/api_samples/os-networks-associate/network-disassociate-host-req.json.tpl b/nova/tests/integrated/v3/api_samples/os-networks-associate/network-disassociate-host-req.json.tpl new file mode 100644 index 000000000000..46f69b3e8190 --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/os-networks-associate/network-disassociate-host-req.json.tpl @@ -0,0 +1,3 @@ +{ + "disassociate_host": null +} diff --git a/nova/tests/integrated/v3/api_samples/os-networks-associate/network-disassociate-project-req.json.tpl b/nova/tests/integrated/v3/api_samples/os-networks-associate/network-disassociate-project-req.json.tpl new file mode 100644 index 000000000000..63b6eb683997 --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/os-networks-associate/network-disassociate-project-req.json.tpl @@ -0,0 +1,3 @@ +{ + "disassociate_project": null +} diff --git a/nova/tests/integrated/v3/api_samples/os-networks-associate/network-disassociate-req.json.tpl b/nova/tests/integrated/v3/api_samples/os-networks-associate/network-disassociate-req.json.tpl new file mode 100644 index 000000000000..2e09d15a60a9 --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/os-networks-associate/network-disassociate-req.json.tpl @@ -0,0 +1,3 @@ +{ + "disassociate": null +} diff --git a/nova/tests/integrated/v3/test_networks_associate.py b/nova/tests/integrated/v3/test_networks_associate.py new file mode 100644 index 000000000000..3bd437a1cda5 --- /dev/null +++ b/nova/tests/integrated/v3/test_networks_associate.py @@ -0,0 +1,76 @@ +# Copyright 2012 Nebula, Inc. +# Copyright 2014 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo.config import cfg + +from nova.network import api as network_api +from nova.tests.integrated.v3 import api_sample_base + +CONF = cfg.CONF +CONF.import_opt('osapi_compute_extension', + 'nova.api.openstack.compute.extensions') + + +class NetworksAssociateJsonTests(api_sample_base.ApiSampleTestBaseV3): + extension_name = "os-networks-associate" + extra_extensions_to_load = ["os-networks"] + + _sentinel = object() + + def _get_flags(self): + f = super(NetworksAssociateJsonTests, self)._get_flags() + f['osapi_compute_extension'] = CONF.osapi_compute_extension[:] + # Networks_associate requires Networks to be update + f['osapi_compute_extension'].append( + 'nova.api.openstack.compute.contrib.os_networks.Os_networks') + return f + + def setUp(self): + super(NetworksAssociateJsonTests, self).setUp() + + def fake_associate(self, context, network_id, + host=NetworksAssociateJsonTests._sentinel, + project=NetworksAssociateJsonTests._sentinel): + return True + + self.stubs.Set(network_api.API, "associate", fake_associate) + + def test_disassociate(self): + response = self._do_post('os-networks/1/action', + 'network-disassociate-req', + {}) + self.assertEqual(response.status_code, 202) + self.assertEqual(response.content, "") + + def test_disassociate_host(self): + response = self._do_post('os-networks/1/action', + 'network-disassociate-host-req', + {}) + self.assertEqual(response.status_code, 202) + self.assertEqual(response.content, "") + + def test_disassociate_project(self): + response = self._do_post('os-networks/1/action', + 'network-disassociate-project-req', + {}) + self.assertEqual(response.status_code, 202) + self.assertEqual(response.content, "") + + def test_associate_host(self): + response = self._do_post('os-networks/1/action', + 'network-associate-host-req', + {"host": "testHost"}) + self.assertEqual(response.status_code, 202) + self.assertEqual(response.content, "") diff --git a/setup.cfg b/setup.cfg index c6ba4d02dd73..48b8008303ed 100644 --- a/setup.cfg +++ b/setup.cfg @@ -103,6 +103,7 @@ nova.api.v3.extensions = multinic = nova.api.openstack.compute.plugins.v3.multinic:Multinic multiple_create = nova.api.openstack.compute.plugins.v3.multiple_create:MultipleCreate networks = nova.api.openstack.compute.plugins.v3.networks:Networks + networks_associate = nova.api.openstack.compute.plugins.v3.networks_associate:NetworksAssociate pause_server = nova.api.openstack.compute.plugins.v3.pause_server:PauseServer pci = nova.api.openstack.compute.plugins.v3.pci:Pci quota_sets = nova.api.openstack.compute.plugins.v3.quota_sets:QuotaSets