From 6c224f5acfeb2288f2f4be41aee112fd01cbf4d0 Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Mon, 16 Feb 2015 23:21:00 -0800 Subject: [PATCH] Add project and domain params to network create Without this patch, openstackclient has no way to specify to which project a network belongs upon creation. Instead, it uses the project ID that the user is authenticating with to fill the tenant_id column. This is a problem because an admin user is unable to specify a project for a non-admin network. To fix this and to improve feature parity with the neutron client, this patch adds project and domain parameters to the network create command and uses the given project name to look up the project ID. Neutron does not allow the project to be changed after creation, so no such parameter has been added to the neutron set command. Neutron calls the field 'tenant_id', but this change exposes the parameter as '--project' to support the newer terminology. If no project is specified, the client defaults to the previous behavior of using the auth project. Change-Id: Ia33ff7d599542c5b88baf2a69b063a23089a3cc4 --- doc/source/command-objects/network.rst | 10 ++ openstackclient/network/v2/network.py | 21 ++++ openstackclient/tests/identity/v2_0/fakes.py | 7 ++ .../tests/network/v2/test_network.py | 100 ++++++++++++++++++ 4 files changed, 138 insertions(+) diff --git a/doc/source/command-objects/network.rst b/doc/source/command-objects/network.rst index 0edd298048..dcba9f82b8 100644 --- a/doc/source/command-objects/network.rst +++ b/doc/source/command-objects/network.rst @@ -13,10 +13,20 @@ Create new network .. code:: bash os network create + [--domain ] [--enable | --disable] + [--project ] [--share | --no-share] +.. option:: --domain + + Owner's domain (name or ID)" + +.. option:: --project + + Owner's project (name or ID) + .. option:: --enable Enable network (default) diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index 1a79c80ab8..9b2466422a 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -22,6 +22,7 @@ from cliff import show from openstackclient.common import exceptions from openstackclient.common import utils +from openstackclient.identity import common as identity_common from openstackclient.network import common @@ -82,6 +83,14 @@ class CreateNetwork(show.ShowOne): action='store_false', help='Do not share the network between projects', ) + parser.add_argument( + '--project', + metavar='', + help="Owner's project (name or ID)") + parser.add_argument( + '--domain', + metavar='', + help="Owner's domain (name or ID)") return parser def take_action(self, parsed_args): @@ -101,6 +110,18 @@ class CreateNetwork(show.ShowOne): 'admin_state_up': parsed_args.admin_state} if parsed_args.shared is not None: body['shared'] = parsed_args.shared + if parsed_args.project is not None: + identity_client = self.app.client_manager.identity + if parsed_args.domain is not None: + domain = identity_common.find_domain(identity_client, + parsed_args.domain) + project_id = utils.find_resource(identity_client.projects, + parsed_args.project, + domain_id=domain.id).id + else: + project_id = utils.find_resource(identity_client.projects, + parsed_args.project).id + body['tenant_id'] = project_id return {'network': body} diff --git a/openstackclient/tests/identity/v2_0/fakes.py b/openstackclient/tests/identity/v2_0/fakes.py index b136f84165..6688606a42 100644 --- a/openstackclient/tests/identity/v2_0/fakes.py +++ b/openstackclient/tests/identity/v2_0/fakes.py @@ -142,6 +142,13 @@ class FakeIdentityv2Client(object): self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] + def __getattr__(self, name): + # Map v3 'projects' back to v2 'tenants' + if name == "projects": + return self.tenants + else: + raise AttributeError(name) + class TestIdentityv2(utils.TestCommand): def setUp(self): diff --git a/openstackclient/tests/network/v2/test_network.py b/openstackclient/tests/network/v2/test_network.py index e14dd88b1d..90085f287a 100644 --- a/openstackclient/tests/network/v2/test_network.py +++ b/openstackclient/tests/network/v2/test_network.py @@ -16,6 +16,9 @@ import mock from openstackclient.common import exceptions from openstackclient.network.v2 import network +from openstackclient.tests import fakes +from openstackclient.tests.identity.v2_0 import fakes as identity_fakes_v2 +from openstackclient.tests.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.network import common RESOURCE = 'network' @@ -65,6 +68,7 @@ class TestCreateNetwork(common.TestNetworkBase): ('name', FAKE_NAME), ('admin_state', True), ('shared', None), + ('project', None), ] mocker = mock.Mock(return_value=copy.deepcopy(RESPONSE)) self.app.client_manager.network.create_network = mocker @@ -85,15 +89,36 @@ class TestCreateNetwork(common.TestNetworkBase): arglist = [ "--disable", "--share", + "--project", identity_fakes_v3.project_name, + "--domain", identity_fakes_v3.domain_name, FAKE_NAME, ] + self.given_show_options verifylist = [ ('admin_state', False), ('shared', True), + ('project', identity_fakes_v3.project_name), + ('domain', identity_fakes_v3.domain_name), ('name', FAKE_NAME), ] + self.then_show_options mocker = mock.Mock(return_value=copy.deepcopy(RESPONSE)) self.app.client_manager.network.create_network = mocker + identity_client = identity_fakes_v3.FakeIdentityv3Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) + self.app.client_manager.identity = identity_client + self.projects_mock = self.app.client_manager.identity.projects + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes_v3.PROJECT), + loaded=True, + ) + self.domains_mock = self.app.client_manager.identity.domains + self.domains_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes_v3.DOMAIN), + loaded=True, + ) cmd = network.CreateNetwork(self.app, self.namespace) parsed_args = self.check_parser(cmd, arglist, verifylist) @@ -104,6 +129,7 @@ class TestCreateNetwork(common.TestNetworkBase): 'admin_state_up': False, 'name': FAKE_NAME, 'shared': True, + 'tenant_id': identity_fakes_v3.project_id, } }) self.assertEqual(FILTERED, result) @@ -135,6 +161,80 @@ class TestCreateNetwork(common.TestNetworkBase): }) self.assertEqual(FILTERED, result) + def test_create_with_project_identityv2(self): + arglist = [ + "--project", identity_fakes_v2.project_name, + FAKE_NAME, + + ] + verifylist = [ + ('admin_state', True), + ('shared', None), + ('name', FAKE_NAME), + ('project', identity_fakes_v2.project_name), + ] + mocker = mock.Mock(return_value=copy.deepcopy(RESPONSE)) + self.app.client_manager.network.create_network = mocker + identity_client = identity_fakes_v2.FakeIdentityv2Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) + self.app.client_manager.identity = identity_client + self.projects_mock = self.app.client_manager.identity.tenants + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes_v2.PROJECT), + loaded=True, + ) + cmd = network.CreateNetwork(self.app, self.namespace) + + parsed_args = self.check_parser(cmd, arglist, verifylist) + result = list(cmd.take_action(parsed_args)) + + mocker.assert_called_with({ + RESOURCE: { + 'admin_state_up': True, + 'name': FAKE_NAME, + 'tenant_id': identity_fakes_v2.project_id, + } + }) + self.assertEqual(FILTERED, result) + + def test_create_with_domain_identityv2(self): + arglist = [ + "--project", identity_fakes_v3.project_name, + "--domain", identity_fakes_v3.domain_name, + FAKE_NAME, + ] + verifylist = [ + ('admin_state', True), + ('shared', None), + ('project', identity_fakes_v3.project_name), + ('domain', identity_fakes_v3.domain_name), + ('name', FAKE_NAME), + ] + mocker = mock.Mock(return_value=copy.deepcopy(RESPONSE)) + self.app.client_manager.network.create_network = mocker + identity_client = identity_fakes_v2.FakeIdentityv2Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) + self.app.client_manager.identity = identity_client + self.projects_mock = self.app.client_manager.identity.tenants + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes_v2.PROJECT), + loaded=True, + ) + cmd = network.CreateNetwork(self.app, self.namespace) + parsed_args = self.check_parser(cmd, arglist, verifylist) + + self.assertRaises( + AttributeError, + cmd.take_action, + parsed_args, + ) + class TestDeleteNetwork(common.TestNetworkBase): def test_delete(self):