diff --git a/releasenotes/notes/workflow-based-listings-6935d507c40a7e9d.yaml b/releasenotes/notes/workflow-based-listings-6935d507c40a7e9d.yaml new file mode 100644 index 000000000..f1d2f8d7a --- /dev/null +++ b/releasenotes/notes/workflow-based-listings-6935d507c40a7e9d.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + New ``openstack overcloud roles`` ``list`` and ``show`` commands + were added in order to look at the roles as they are defined in the + plan in the Swift container. +deprecations: + - | + ``openstack overcloud role list`` and ``openstack overcloud role + show`` are deprecated in favour of ``openstack overcloud roles + list`` and ``openstack overcloud roles show`` respectively. The new + commands operate directly on the plan rather than on the local + filesystem. diff --git a/setup.cfg b/setup.cfg index 20a437428..a3d0b7474 100644 --- a/setup.cfg +++ b/setup.cfg @@ -86,6 +86,8 @@ openstack.tripleoclient.v1 = overcloud_role_show= tripleoclient.v1.overcloud_roles:RoleShow overcloud_role_list = tripleoclient.v1.overcloud_roles:RoleList overcloud_roles_generate = tripleoclient.v1.overcloud_roles:RolesGenerate + overcloud_roles_list = tripleoclient.v1.overcloud_plan_roles:ListRoles + overcloud_roles_show = tripleoclient.v1.overcloud_plan_roles:ShowRole overcloud_support_report_collect = tripleoclient.v1.overcloud_support:ReportExecute overcloud_update_stack = tripleoclient.v1.overcloud_update:UpdateOvercloud overcloud_execute = tripleoclient.v1.overcloud_execute:RemoteExecute diff --git a/tripleoclient/tests/v1/test_overcloud_plan_roles.py b/tripleoclient/tests/v1/test_overcloud_plan_roles.py new file mode 100644 index 000000000..5b6e07c57 --- /dev/null +++ b/tripleoclient/tests/v1/test_overcloud_plan_roles.py @@ -0,0 +1,273 @@ +# 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 osc_lib import exceptions +from osc_lib.tests import utils + +from tripleoclient.v1 import overcloud_plan_roles + + +class TestOvercloudListCurrentRoles(utils.TestCommand): + + def setUp(self): + super(TestOvercloudListCurrentRoles, self).setUp() + + self.cmd = overcloud_plan_roles.ListRoles(self.app, None) + self.app.client_manager.workflow_engine = mock.Mock() + self.workflow = self.app.client_manager.workflow_engine + + def test_list_empty_on_non_default_plan(self): + self.workflow.action_executions.create.return_value = ( + mock.Mock(output='{"result": []}')) + + arglist = ['--name', 'overcast', '--current'] + verifylist = [('name', 'overcast'), ('current', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.workflow.action_executions.create.assert_called_once_with( + 'tripleo.role.list', + {'container': 'overcast', 'detail': False}, + run_sync=True, save_result=True + ) + self.assertEqual(0, len(result[1])) + + def test_list(self): + self.workflow.action_executions.create.return_value = ( + mock.MagicMock( + output='{"result": ["ObjectStorage", "Controller"]}')) + + arglist = ['--current'] + verifylist = [('name', 'overcloud'), ('current', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.workflow.action_executions.create.assert_called_once_with( + 'tripleo.role.list', + {'container': 'overcloud', 'detail': False}, + run_sync=True, save_result=True + ) + + self.assertEqual(2, len(result[1])) + self.assertEqual([('Controller',), ('ObjectStorage',)], result[1]) + + def test_list_with_details(self): + self.workflow.action_executions.create.return_value = ( + mock.MagicMock(output=( + '{"result": [{"name":"Controller","description":"Test desc",' + '"random": "abcd"},{"name":"Test"}]}'))) + + parsed_args = self.check_parser(self.cmd, + ['--current', '--detail'], + []) + result = self.cmd.take_action(parsed_args) + + self.workflow.action_executions.create.assert_called_once_with( + 'tripleo.role.list', + {'container': 'overcloud', 'detail': True}, + run_sync=True, save_result=True + ) + + data = result[1] + self.assertEqual(2, len(data)) + + self.assertEqual(data[0][0], "Controller") + self.assertEqual(data[0][3], "random: abcd") + self.assertEqual(data[1][0], "Test") + self.assertEqual(data[1][3], "") + + def test_list_with_details_empty(self): + self.workflow.action_executions.create.return_value = ( + mock.Mock(output='{"result": []}')) + + parsed_args = self.check_parser(self.cmd, + ['--current', '--detail'], + []) + result = self.cmd.take_action(parsed_args) + + self.workflow.action_executions.create.assert_called_once_with( + 'tripleo.role.list', + {'container': 'overcloud', 'detail': True}, + run_sync=True, save_result=True + ) + self.assertEqual(0, len(result[1])) + + def test_list_with_details_sorted(self): + self.workflow.action_executions.create.return_value = ( + mock.MagicMock(output=( + '{"result": [{"name":"Compute"},{"name":"Random"},' + '{"name": "BlockStorage","ServicesDefault":["c","b","a"]}]}'))) + + parsed_args = self.check_parser(self.cmd, + ['--current', '--detail'], + []) + result = self.cmd.take_action(parsed_args) + + self.workflow.action_executions.create.assert_called_once_with( + 'tripleo.role.list', + {'container': 'overcloud', 'detail': True}, + run_sync=True, save_result=True + ) + + self.assertEqual(3, len(result[1])) + + # Test main list sorted + self.assertEqual(result[1][0][0], "BlockStorage") + self.assertEqual(result[1][1][0], "Compute") + self.assertEqual(result[1][2][0], "Random") + + # Test service sublist sorted + self.assertEqual(result[1][0][2], "a\nb\nc") + + +class TestOvercloudListRole(utils.TestCommand): + + def setUp(self): + super(TestOvercloudListRole, self).setUp() + + self.cmd = overcloud_plan_roles.ListRoles(self.app, None) + + self.workflow = self.app.client_manager.workflow_engine = mock.Mock() + self.websocket = mock.Mock() + self.websocket.__enter__ = lambda s: self.websocket + self.websocket.__exit__ = lambda s, *exc: None + self.tripleoclient = mock.Mock() + self.tripleoclient.messaging_websocket.return_value = self.websocket + self.app.client_manager.tripleoclient = self.tripleoclient + + def test_list_empty(self): + self.websocket.wait_for_messages.return_value = [{ + 'execution': {'id': 'IDID'}, + 'status': 'SUCCESS', + 'available_roles': [] + }] + + arglist = ['--name', 'overcast'] + verifylist = [('name', 'overcast')] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.workflow.executions.create.assert_called_once_with( + 'tripleo.plan_management.v1.list_available_roles', + workflow_input={'container': 'overcast'}, + ) + self.assertEqual(0, len(result[1])) + + def test_list(self): + self.websocket.wait_for_messages.return_value = [{ + 'execution': {'id': 'IDID'}, + 'status': 'SUCCESS', + 'available_roles': [{'name': 'ObjectStorage'}, + {'name': 'Compute'}] + }] + + parsed_args = self.check_parser(self.cmd, [], [('name', 'overcloud')]) + result = self.cmd.take_action(parsed_args) + + self.workflow.executions.create.assert_called_once_with( + 'tripleo.plan_management.v1.list_available_roles', + workflow_input={'container': 'overcloud'}, + ) + + self.assertEqual(2, len(result[1])) + self.assertEqual([('Compute',), ('ObjectStorage',)], result[1]) + + def test_list_with_details(self): + self.websocket.wait_for_messages.return_value = [{ + 'execution': {'id': 'IDID'}, + 'status': 'SUCCESS', + 'available_roles': [ + {'name': 'Controller', 'description': 'Test description', + 'random': 'abcd'}, + {'name': 'Test'}] + }] + + parsed_args = self.check_parser(self.cmd, ['--detail'], []) + result = self.cmd.take_action(parsed_args) + + self.workflow.executions.create.assert_called_once_with( + 'tripleo.plan_management.v1.list_available_roles', + workflow_input={'container': 'overcloud'}, + ) + + data = result[1] + self.assertEqual(2, len(data)) + + self.assertEqual(data[0][0], "Controller") + self.assertEqual(data[0][3], "random: abcd") + self.assertEqual(data[1][0], "Test") + self.assertEqual(data[1][3], "") + + +class TestOvercloudShowRole(utils.TestCommand): + + def setUp(self): + super(TestOvercloudShowRole, self).setUp() + + self.cmd = overcloud_plan_roles.ShowRole(self.app, None) + + self.workflow = self.app.client_manager.workflow_engine = mock.Mock() + self.websocket = mock.Mock() + self.websocket.__enter__ = lambda s: self.websocket + self.websocket.__exit__ = lambda s, *exc: None + self.tripleoclient = mock.Mock() + self.tripleoclient.messaging_websocket.return_value = self.websocket + self.app.client_manager.tripleoclient = self.tripleoclient + + def test_role_not_found(self): + self.websocket.wait_for_messages.return_value = [{ + 'execution': {'id': 'IDID'}, + 'status': 'SUCCESS', + 'available_roles': [] + }] + + arglist = ['--name', 'overcast', 'doesntexist'] + verifylist = [('name', 'overcast'), ('role', 'doesntexist')] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + def test_role(self): + self.websocket.wait_for_messages.return_value = [{ + 'execution': {'id': 'IDID'}, + 'status': 'SUCCESS', + 'available_roles': [ + {"name": "Test", "a": "b"}, + {"name": "Controller", "description": "Test desc", + "random": "abcd", "efg": "123", + "ServicesDefault": ["b", "c", "a"]}]}] + + arglist = ['Controller'] + verifylist = [('name', 'overcloud'), ('role', 'Controller')] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.workflow.executions.create.assert_called_once_with( + 'tripleo.plan_management.v1.list_available_roles', + workflow_input={'container': 'overcloud'}, + ) + + self.assertEqual(len(result), 2) + + # Check that all the columns are picked up correctly + expected = ['Name', 'Description', 'Services Default', 'efg', 'random'] + actual = result[0] + self.assertEqual(expected, actual) + + # Check the content + expected = ['Controller', 'Test desc', "a\nb\nc", '123', 'abcd'] + actual = result[1] + self.assertEqual(expected, actual) diff --git a/tripleoclient/v1/overcloud_plan_roles.py b/tripleoclient/v1/overcloud_plan_roles.py new file mode 100644 index 000000000..8eb22fe80 --- /dev/null +++ b/tripleoclient/v1/overcloud_plan_roles.py @@ -0,0 +1,158 @@ +# 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 logging + +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib.i18n import _ + +from tripleoclient.workflows import roles + + +class ListRoles(command.Lister): + """List the current and available roles in a given plan""" + + log = logging.getLogger(__name__ + ".ListRoles") + + def get_parser(self, prog_name): + parser = super(ListRoles, self).get_parser(prog_name) + parser.add_argument( + '--name', + dest='name', + default='overcloud', + help=_('The name of the plan, which is used for the object ' + 'storage container, workflow environment and orchestration ' + 'stack names.'), + ) + parser.add_argument( + '--detail', + action='store_true', + help=_('Include details about each role')) + parser.add_argument( + '--current', + action='store_true', + help=_('Only show the information for the roles currently enabled ' + 'for the plan.')) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action({})'.format(parsed_args)) + + if parsed_args.current: + result = roles.list_roles( + self.app.client_manager.workflow_engine, + container=parsed_args.name, + detail=parsed_args.detail) + else: + result = roles.list_available_roles( + self.app.client_manager, + container=parsed_args.name) + # The workflow returns all the details by default, trim + # them down if not required. + if not parsed_args.detail: + result = [r['name'] for r in result] + + if parsed_args.detail: + if result: + result.sort(key=lambda r: r['name']) + + role_list = self.format_role_details(result) + column_names = ("Role Name", + "Description", + "Services Default", + "Other Details") + return (column_names, role_list) + else: + if result: + result.sort() + return (("Role Name",), [(r,) for r in result]) + + def format_role_details(self, result): + role_list = [] + for r in result: + name = r.pop('name') + description = service_defaults = '' + detail = [] + + if 'description' in r: + description = r.pop('description') + if 'ServicesDefault' in r: + r['ServicesDefault'].sort() + service_defaults = '\n'.join(r.pop('ServicesDefault')) + for k, v in r.items(): + detail.append("%s: %s" % (k, v)) + + role_list.append((name, description, service_defaults, + '\n'.join(detail))) + return role_list + + +class ShowRole(command.ShowOne): + """Show details for a specific role, given a plan""" + + log = logging.getLogger(__name__ + ".ShowRole") + + def get_parser(self, prog_name): + parser = super(ShowRole, self).get_parser(prog_name) + parser.add_argument( + '--name', + dest='name', + default='overcloud', + help=_('The name of the plan, which is used for the object ' + 'storage container, workflow environment and orchestration ' + 'stack names.'), + ) + parser.add_argument('role', + metavar="", + help=_('Name of the role to look up.')) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action({})'.format(parsed_args)) + + role = self.get_role_details(parsed_args.name, parsed_args.role) + if not role: + raise exceptions.CommandError( + "Could not find role %s" % parsed_args.role) + + return self.format_role(role) + + def get_role_details(self, name, role_name): + result = roles.list_available_roles( + self.app.client_manager, + container=name) + + for r in result: + if r['name'] == role_name: + return r + return [] + + def format_role(self, role): + column_names = ['Name'] + data = [role.pop('name')] + + if 'description' in role: + column_names.append('Description') + data.append(role.pop('description')) + if 'ServicesDefault' in role: + column_names.append('Services Default') + role['ServicesDefault'].sort() + data.append('\n'.join(role.pop('ServicesDefault'))) + + other_fields = list(role.keys()) + other_fields.sort() + for field in other_fields: + column_names.append(field) + data.append(role[field]) + + return column_names, data diff --git a/tripleoclient/v1/overcloud_roles.py b/tripleoclient/v1/overcloud_roles.py index c085aa110..483addf5c 100644 --- a/tripleoclient/v1/overcloud_roles.py +++ b/tripleoclient/v1/overcloud_roles.py @@ -86,20 +86,29 @@ class RolesGenerate(RolesBaseCommand): class RoleList(RolesBaseCommand): - """List availables roles""" + """List availables roles (DEPRECATED). + + Please use "openstack overcloud roles list" instead. + """ def get_parser(self, prog_name): parser = super(RoleList, self).get_parser(prog_name) return parser def take_action(self, parsed_args): self.log.debug('take_action({})'.format(parsed_args)) + self.log.warning('This command is deprecated. Please use "openstack ' + 'overcloud roles list" instead.') roles_path = os.path.realpath(parsed_args.roles_path) roles = rolesutils.get_roles_list_from_directory(roles_path) print('\n'.join(roles)) class RoleShow(RolesBaseCommand): - """Show information about a given role""" + """Show information about a given role (DEPRECATED). + + + Please use "openstack overcloud roles show" intead. + """ def get_parser(self, prog_name): parser = super(RoleShow, self).get_parser(prog_name) parser.add_argument('role', metavar='', @@ -108,6 +117,8 @@ class RoleShow(RolesBaseCommand): def take_action(self, parsed_args): self.log.debug('take_action({})'.format(parsed_args)) + self.log.warning('This command is deprecated. Please use "openstack ' + 'overcloud roles show" instead.') roles_path = os.path.realpath(parsed_args.roles_path) role_name = parsed_args.role file_path = os.path.join(roles_path, '{}.yaml'.format(role_name)) diff --git a/tripleoclient/workflows/roles.py b/tripleoclient/workflows/roles.py new file mode 100644 index 000000000..dd65aaa76 --- /dev/null +++ b/tripleoclient/workflows/roles.py @@ -0,0 +1,45 @@ +# 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 logging + +from tripleoclient import exceptions +from tripleoclient.workflows import base + +LOG = logging.getLogger(__name__) + + +def list_roles(workflow_client, **input_): + return base.call_action(workflow_client, 'tripleo.role.list', **input_) + + +def list_available_roles(clients, **workflow_input): + workflow_client = clients.workflow_engine + tripleoclients = clients.tripleoclient + + available_roles = [] + with tripleoclients.messaging_websocket() as ws: + execution = base.start_workflow( + workflow_client, + 'tripleo.plan_management.v1.list_available_roles', + workflow_input=workflow_input + ) + + for payload in base.wait_for_messages(workflow_client, ws, execution): + if payload['status'] == 'SUCCESS': + available_roles = payload['available_roles'] + else: + raise exceptions.WorkflowServiceError( + 'Error retrieving available roles: {}'.format( + payload.get('message'))) + + return available_roles