From 84e3e6e6ee2d582d79bef081d6f1e283836e8ced Mon Sep 17 00:00:00 2001 From: Lingxian Kong Date: Fri, 12 Feb 2016 15:54:28 +1300 Subject: [PATCH] Support resource sharing CLI Change-Id: Id84054be6458039804b0ca702a727e6c696f003a Implements: blueprint resource-sharing-cli --- mistralclient/api/v2/client.py | 2 + mistralclient/api/v2/members.py | 76 ++++++ mistralclient/commands/v2/members.py | 232 ++++++++++++++++++ mistralclient/shell.py | 8 +- mistralclient/tests/unit/v2/base.py | 1 + .../tests/unit/v2/test_cli_members.py | 102 ++++++++ mistralclient/tests/unit/v2/test_members.py | 102 ++++++++ 7 files changed, 522 insertions(+), 1 deletion(-) create mode 100644 mistralclient/api/v2/members.py create mode 100644 mistralclient/commands/v2/members.py create mode 100644 mistralclient/tests/unit/v2/test_cli_members.py create mode 100644 mistralclient/tests/unit/v2/test_members.py diff --git a/mistralclient/api/v2/client.py b/mistralclient/api/v2/client.py index a2d55c16..fff6e866 100644 --- a/mistralclient/api/v2/client.py +++ b/mistralclient/api/v2/client.py @@ -21,6 +21,7 @@ from mistralclient.api.v2 import actions from mistralclient.api.v2 import cron_triggers from mistralclient.api.v2 import environments from mistralclient.api.v2 import executions +from mistralclient.api.v2 import members from mistralclient.api.v2 import services from mistralclient.api.v2 import tasks from mistralclient.api.v2 import workbooks @@ -74,6 +75,7 @@ class Client(object): self.environments = environments.EnvironmentManager(self) self.action_executions = action_executions.ActionExecutionManager(self) self.services = services.ServiceManager(self) + self.members = members.MemberManager(self) def authenticate(self, mistral_url=None, username=None, api_key=None, project_name=None, auth_url=None, project_id=None, diff --git a/mistralclient/api/v2/members.py b/mistralclient/api/v2/members.py new file mode 100644 index 00000000..de2042bd --- /dev/null +++ b/mistralclient/api/v2/members.py @@ -0,0 +1,76 @@ +# Copyright 2016 - Catalyst IT Limited +# +# 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 mistralclient.api import base + + +class Member(base.Resource): + resource_name = 'Member' + + +class MemberManager(base.ResourceManager): + resource_class = Member + + def create(self, resource_id, resource_type, member_id): + self._ensure_not_empty( + resource_id=resource_id, + resource_type=resource_type, + member_id=member_id + ) + + data = { + 'member_id': member_id, + } + + url = '/%ss/%s/members' % (resource_type, resource_id) + + return self._create(url, data) + + def update(self, resource_id, resource_type, member_id='', + status='accepted'): + if not member_id: + member_id = self.client.http_client.project_id + + url = '/%ss/%s/members/%s' % (resource_type, resource_id, member_id) + + return self._update(url, {'status': status}) + + def list(self, resource_id, resource_type): + url = '/%ss/%s/members' % (resource_type, resource_id) + + return self._list(url, response_key='members') + + def get(self, resource_id, resource_type, member_id=None): + self._ensure_not_empty( + resource_id=resource_id, + resource_type=resource_type, + ) + + if not member_id: + member_id = self.client.http_client.project_id + + url = '/%ss/%s/members/%s' % (resource_type, resource_id, member_id) + + return self._get(url) + + def delete(self, resource_id, resource_type, member_id): + self._ensure_not_empty( + resource_id=resource_id, + resource_type=resource_type, + member_id=member_id + ) + + url = '/%ss/%s/members/%s' % (resource_type, resource_id, member_id) + + self._delete(url) diff --git a/mistralclient/commands/v2/members.py b/mistralclient/commands/v2/members.py new file mode 100644 index 00000000..fc0d709c --- /dev/null +++ b/mistralclient/commands/v2/members.py @@ -0,0 +1,232 @@ +# Copyright 2016 - Catalyst IT Limited +# +# 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 cliff import command +from cliff import show + +from mistralclient.commands.v2 import base +from mistralclient import exceptions + + +def format_list(member=None): + return format(member, lister=True) + + +def format(member=None, lister=False): + columns = ( + 'Resource ID', + 'Resource Type', + 'Resource Owner', + 'Member ID', + 'Status', + 'Created at', + 'Updated at' + ) + + if member: + data = ( + member.resource_id, + member.resource_type, + member.project_id, + member.member_id, + member.status, + member.created_at, + member.updated_at or '' + ) + else: + data = (tuple('' for _ in range(len(columns))),) + + return columns, data + + +class List(base.MistralLister): + """List all members.""" + + def _get_format_function(self): + return format_list + + def get_parser(self, parsed_args): + parser = super(List, self).get_parser(parsed_args) + + parser.add_argument( + 'resource_id', + help='Resource id to be shared.' + ) + parser.add_argument( + 'resource_type', + help='Resource type.' + ) + + return parser + + def _get_resources(self, parsed_args): + mistral_client = self.app.client_manager.workflow_engine + return mistral_client.members.list( + parsed_args.resource_id, + parsed_args.resource_type + ) + + +class Get(show.ShowOne): + """Show specific member information.""" + + def get_parser(self, prog_name): + parser = super(Get, self).get_parser(prog_name) + + parser.add_argument( + 'resource_id', + help='Resource ID to be shared.' + ) + parser.add_argument( + 'resource_type', + help='Resource type.' + ) + parser.add_argument( + '-m', + '--member-id', + default='', + help='Project ID to whom the resource is shared to. No need to ' + 'provide this param if you are the resource member.' + ) + + return parser + + def take_action(self, parsed_args): + mistral_client = self.app.client_manager.workflow_engine + member = mistral_client.members.get( + parsed_args.resource_id, + parsed_args.resource_type, + parsed_args.member_id, + ) + + return format(member) + + +class Create(show.ShowOne): + """Shares a resource to another tenant.""" + + def get_parser(self, prog_name): + parser = super(Create, self).get_parser(prog_name) + + parser.add_argument( + 'resource_id', + help='Resource ID to be shared.' + ) + parser.add_argument( + 'resource_type', + help='Resource type.' + ) + parser.add_argument( + 'member_id', + help='Project ID to whom the resource is shared to.' + ) + + return parser + + def take_action(self, parsed_args): + mistral_client = self.app.client_manager.workflow_engine + member = mistral_client.members.create( + parsed_args.resource_id, + parsed_args.resource_type, + parsed_args.member_id, + ) + + return format(member) + + +class Delete(command.Command): + """Delete a resource sharing relationship.""" + + def get_parser(self, prog_name): + parser = super(Delete, self).get_parser(prog_name) + + parser.add_argument( + 'resource_id', + help='Resource ID to be shared.' + ) + parser.add_argument( + 'resource_type', + help='Resource type.' + ) + parser.add_argument( + 'member_id', + help='Project ID to whom the resource is shared to.' + ) + + return parser + + def take_action(self, parsed_args): + mistral_client = self.app.client_manager.workflow_engine + + try: + mistral_client.members.delete( + parsed_args.resource_id, + parsed_args.resource_type, + parsed_args.member_id, + ) + + print( + "Request to delete %s member %s has been accepted." % + (parsed_args.resource_type, parsed_args.member_id) + ) + except Exception as e: + print(e) + + error_msg = "Unable to delete the specified member." + raise exceptions.MistralClientException(error_msg) + + +class Update(show.ShowOne): + """Update resource sharing status.""" + + def get_parser(self, prog_name): + parser = super(Update, self).get_parser(prog_name) + + parser.add_argument( + 'resource_id', + help='Resource ID to be shared.' + ) + parser.add_argument( + 'resource_type', + help='Resource type.' + ) + parser.add_argument( + '-m', + '--member-id', + default='', + help='Project ID to whom the resource is shared to. No need to ' + 'provide this param if you are the resource member.' + ) + parser.add_argument( + '-s', + '--status', + default='accepted', + choices=['pending', 'accepted', 'rejected'], + help='status of the sharing.' + ) + + return parser + + def take_action(self, parsed_args): + mistral_client = self.app.client_manager.workflow_engine + + member = mistral_client.members.update( + parsed_args.resource_id, + parsed_args.resource_type, + parsed_args.member_id, + status=parsed_args.status + ) + + return format(member) diff --git a/mistralclient/shell.py b/mistralclient/shell.py index 1369b1f8..04d4bec6 100644 --- a/mistralclient/shell.py +++ b/mistralclient/shell.py @@ -25,6 +25,7 @@ import mistralclient.commands.v2.actions import mistralclient.commands.v2.cron_triggers import mistralclient.commands.v2.environments import mistralclient.commands.v2.executions +import mistralclient.commands.v2.members import mistralclient.commands.v2.services import mistralclient.commands.v2.tasks import mistralclient.commands.v2.workbooks @@ -408,7 +409,12 @@ class MistralShell(app.App): mistralclient.commands.v2.cron_triggers.Create, 'cron-trigger-delete': mistralclient.commands.v2.cron_triggers.Delete, - 'service-list': mistralclient.commands.v2.services.List + 'service-list': mistralclient.commands.v2.services.List, + 'member-create': mistralclient.commands.v2.members.Create, + 'member-delete': mistralclient.commands.v2.members.Delete, + 'member-update': mistralclient.commands.v2.members.Update, + 'member-list': mistralclient.commands.v2.members.List, + 'member-get': mistralclient.commands.v2.members.Get, } diff --git a/mistralclient/tests/unit/v2/base.py b/mistralclient/tests/unit/v2/base.py index b72c968f..68c00e28 100644 --- a/mistralclient/tests/unit/v2/base.py +++ b/mistralclient/tests/unit/v2/base.py @@ -31,3 +31,4 @@ class BaseClientV2Test(base.BaseClientTest): self.action_executions = self._client.action_executions self.actions = self._client.actions self.services = self._client.services + self.members = self._client.members diff --git a/mistralclient/tests/unit/v2/test_cli_members.py b/mistralclient/tests/unit/v2/test_cli_members.py new file mode 100644 index 00000000..f37e120c --- /dev/null +++ b/mistralclient/tests/unit/v2/test_cli_members.py @@ -0,0 +1,102 @@ +# Copyright 2016 Catalyst IT Limited +# +# 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. + +import mock + +from mistralclient.api.v2 import members +from mistralclient.commands.v2 import members as member_cmd +from mistralclient.tests.unit import base + +MEMBER_DICT = { + 'id': '123', + 'resource_id': '456', + 'resource_type': 'workflow', + 'project_id': '1111', + 'member_id': '2222', + 'status': 'pending', + 'created_at': '1', + 'updated_at': '1' +} + +MEMBER = members.Member(mock, MEMBER_DICT) + + +class TestCLIWorkflowMembers(base.BaseCommandTest): + def test_create(self): + self.client.members.create.return_value = MEMBER + + result = self.call( + member_cmd.Create, + app_args=[MEMBER_DICT['resource_id'], MEMBER_DICT['resource_type'], + MEMBER_DICT['member_id']] + ) + + self.assertEqual( + ('456', 'workflow', '1111', '2222', 'pending', '1', '1'), + result[1] + ) + + def test_update(self): + self.client.members.update.return_value = MEMBER + + result = self.call( + member_cmd.Update, + app_args=[MEMBER_DICT['resource_id'], MEMBER_DICT['resource_type'], + '-m', MEMBER_DICT['member_id']] + ) + + self.assertEqual( + ('456', 'workflow', '1111', '2222', 'pending', '1', '1'), + result[1] + ) + + def test_list(self): + self.client.members.list.return_value = [MEMBER] + + result = self.call( + member_cmd.List, + app_args=[MEMBER_DICT['resource_id'], MEMBER_DICT['resource_type']] + ) + + self.assertListEqual( + [('456', 'workflow', '1111', '2222', 'pending', '1', '1')], + result[1] + ) + + def test_get(self): + self.client.members.get.return_value = MEMBER + + result = self.call( + member_cmd.Get, + app_args=[MEMBER_DICT['resource_id'], MEMBER_DICT['resource_type'], + '-m', MEMBER_DICT['member_id']] + ) + + self.assertEqual( + ('456', 'workflow', '1111', '2222', 'pending', '1', '1'), + result[1] + ) + + def test_delete(self): + self.call( + member_cmd.Delete, + app_args=[MEMBER_DICT['resource_id'], MEMBER_DICT['resource_type'], + MEMBER_DICT['member_id']] + ) + + self.client.members.delete.assert_called_once_with( + MEMBER_DICT['resource_id'], + MEMBER_DICT['resource_type'], + MEMBER_DICT['member_id'] + ) diff --git a/mistralclient/tests/unit/v2/test_members.py b/mistralclient/tests/unit/v2/test_members.py new file mode 100644 index 00000000..d02ed94e --- /dev/null +++ b/mistralclient/tests/unit/v2/test_members.py @@ -0,0 +1,102 @@ +# Copyright 2016 Catalyst IT Limited +# +# 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. + +import copy +import json + +from mistralclient.tests.unit.v2 import base + +MEMBER = { + 'id': '123', + 'resource_id': '456', + 'resource_type': 'workflow', + 'project_id': 'dc4ffdee54d74028b19b1b90e77aa84f', + 'member_id': '04f61e967fa14cd49950ffe2319317ad', + 'status': 'pending', +} + +WORKFLOW_MEMBERS_URL = '/workflows/%s/members' % (MEMBER['resource_id']) +WORKFLOW_MEMBER_URL = '/workflows/%s/members/%s' % ( + MEMBER['resource_id'], MEMBER['member_id'] +) + + +class TestWorkflowMembers(base.BaseClientV2Test): + def test_create(self): + mock = self.mock_http_post(content=MEMBER) + + mb = self.members.create( + MEMBER['resource_id'], + MEMBER['resource_type'], + MEMBER['member_id'] + ) + + self.assertIsNotNone(mb) + + mock.assert_called_once_with( + WORKFLOW_MEMBERS_URL, + json.dumps({'member_id': MEMBER['member_id']}) + ) + + def test_update(self): + updated_member = copy.copy(MEMBER) + updated_member['status'] = 'accepted' + + mock = self.mock_http_put(content=updated_member) + + mb = self.members.update( + MEMBER['resource_id'], + MEMBER['resource_type'], + MEMBER['member_id'] + ) + + self.assertIsNotNone(mb) + + mock.assert_called_once_with( + WORKFLOW_MEMBER_URL, + json.dumps({"status": "accepted"}) + ) + + def test_list(self): + mock = self.mock_http_get(content={'members': [MEMBER]}) + + mbs = self.members.list(MEMBER['resource_id'], MEMBER['resource_type']) + + self.assertEqual(1, len(mbs)) + + mock.assert_called_once_with(WORKFLOW_MEMBERS_URL) + + def test_get(self): + mock = self.mock_http_get(content=MEMBER) + + mb = self.members.get( + MEMBER['resource_id'], + MEMBER['resource_type'], + MEMBER['member_id'] + ) + + self.assertIsNotNone(mb) + + mock.assert_called_once_with(WORKFLOW_MEMBER_URL) + + def test_delete(self): + mock = self.mock_http_delete(status_code=204) + + self.members.delete( + MEMBER['resource_id'], + MEMBER['resource_type'], + MEMBER['member_id'] + ) + + mock.assert_called_once_with(WORKFLOW_MEMBER_URL)