Support resource sharing CLI

Change-Id: Id84054be6458039804b0ca702a727e6c696f003a
Implements: blueprint resource-sharing-cli
This commit is contained in:
Lingxian Kong 2016-02-12 15:54:28 +13:00
parent 8bd8592c83
commit 84e3e6e6ee
7 changed files with 522 additions and 1 deletions

View File

@ -21,6 +21,7 @@ from mistralclient.api.v2 import actions
from mistralclient.api.v2 import cron_triggers from mistralclient.api.v2 import cron_triggers
from mistralclient.api.v2 import environments from mistralclient.api.v2 import environments
from mistralclient.api.v2 import executions from mistralclient.api.v2 import executions
from mistralclient.api.v2 import members
from mistralclient.api.v2 import services from mistralclient.api.v2 import services
from mistralclient.api.v2 import tasks from mistralclient.api.v2 import tasks
from mistralclient.api.v2 import workbooks from mistralclient.api.v2 import workbooks
@ -74,6 +75,7 @@ class Client(object):
self.environments = environments.EnvironmentManager(self) self.environments = environments.EnvironmentManager(self)
self.action_executions = action_executions.ActionExecutionManager(self) self.action_executions = action_executions.ActionExecutionManager(self)
self.services = services.ServiceManager(self) self.services = services.ServiceManager(self)
self.members = members.MemberManager(self)
def authenticate(self, mistral_url=None, username=None, api_key=None, def authenticate(self, mistral_url=None, username=None, api_key=None,
project_name=None, auth_url=None, project_id=None, project_name=None, auth_url=None, project_id=None,

View File

@ -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)

View File

@ -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 '<none>'
)
else:
data = (tuple('<none>' 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)

View File

@ -25,6 +25,7 @@ import mistralclient.commands.v2.actions
import mistralclient.commands.v2.cron_triggers import mistralclient.commands.v2.cron_triggers
import mistralclient.commands.v2.environments import mistralclient.commands.v2.environments
import mistralclient.commands.v2.executions import mistralclient.commands.v2.executions
import mistralclient.commands.v2.members
import mistralclient.commands.v2.services import mistralclient.commands.v2.services
import mistralclient.commands.v2.tasks import mistralclient.commands.v2.tasks
import mistralclient.commands.v2.workbooks import mistralclient.commands.v2.workbooks
@ -408,7 +409,12 @@ class MistralShell(app.App):
mistralclient.commands.v2.cron_triggers.Create, mistralclient.commands.v2.cron_triggers.Create,
'cron-trigger-delete': 'cron-trigger-delete':
mistralclient.commands.v2.cron_triggers.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,
} }

View File

@ -31,3 +31,4 @@ class BaseClientV2Test(base.BaseClientTest):
self.action_executions = self._client.action_executions self.action_executions = self._client.action_executions
self.actions = self._client.actions self.actions = self._client.actions
self.services = self._client.services self.services = self._client.services
self.members = self._client.members

View File

@ -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']
)

View File

@ -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)