diff --git a/releasenotes/notes/role-management-commands-fc2f67dd5e81016e.yaml b/releasenotes/notes/role-management-commands-fc2f67dd5e81016e.yaml new file mode 100644 index 000000000..32c8ff896 --- /dev/null +++ b/releasenotes/notes/role-management-commands-fc2f67dd5e81016e.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Added new commands for listing available example roles and generating + role_data.yaml files for an environment. ``openstack overcloud roles list`` + provides a list of available roles shipped with tripleo-heat-templates. + ``openstack overcloud role info`` lists out the details of the specific role. + ``openstack overcloud roles generate`` can be used with the available role + names to create a roles_data.yaml used by the deploy command. diff --git a/setup.cfg b/setup.cfg index fe250f907..f5fe342d1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -85,6 +85,9 @@ openstack.tripleoclient.v1 = overcloud_profiles_match = tripleoclient.v1.overcloud_profiles:MatchProfiles overcloud_profiles_list = tripleoclient.v1.overcloud_profiles:ListProfiles overcloud_raid_create = tripleoclient.v1.overcloud_raid:CreateRAID + 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_support_report_collect = tripleoclient.v1.overcloud_support:ReportExecute overcloud_update_clear_breakpoints = tripleoclient.v1.overcloud_update:ClearBreakpointsOvercloud overcloud_update_stack = tripleoclient.v1.overcloud_update:UpdateOvercloud diff --git a/tripleoclient/tests/v1/overcloud_roles/__init__.py b/tripleoclient/tests/v1/overcloud_roles/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tripleoclient/tests/v1/overcloud_roles/test_overcloud_roles.py b/tripleoclient/tests/v1/overcloud_roles/test_overcloud_roles.py new file mode 100644 index 000000000..db574cdac --- /dev/null +++ b/tripleoclient/tests/v1/overcloud_roles/test_overcloud_roles.py @@ -0,0 +1,176 @@ +# Copyright 2017 Red Hat, Inc. +# +# 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 tripleoclient.exceptions import NotFound +from tripleoclient.tests.v1.overcloud_deploy import fakes +from tripleoclient.v1 import overcloud_roles + + +class TestOvercloudRolesListAvailable(fakes.TestDeployOvercloud): + + def setUp(self): + super(TestOvercloudRolesListAvailable, self).setUp() + self.cmd = overcloud_roles.RoleList(self.app, None) + + @mock.patch('os.path.realpath') + def test_action(self, realpath_mock): + realpath_mock.return_value = '/foo' + get_roles_mock = mock.MagicMock() + get_roles_mock.return_value = ['a', 'b'] + self.cmd._get_roles = get_roles_mock + + arglist = [] + verifylist = [ + ('roles_path', '/usr/share/openstack-tripleo-heat-templates/roles') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + get_roles_mock.assert_called_once_with('/foo') + + @mock.patch('os.path.realpath') + def test_action_role_path(self, realpath_mock): + realpath_mock.return_value = '/tmp' + get_roles_mock = mock.MagicMock() + get_roles_mock.return_value = ['a', 'b'] + self.cmd._get_roles = get_roles_mock + + arglist = ['--roles-path', '/tmp'] + verifylist = [ + ('roles_path', '/tmp') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + get_roles_mock.assert_called_once_with('/tmp') + + +class TestOvercloudRolesGenerateData(fakes.TestDeployOvercloud): + + def setUp(self): + super(TestOvercloudRolesGenerateData, self).setUp() + self.cmd = overcloud_roles.RolesGenerate(self.app, None) + + @mock.patch('shutil.copyfileobj') + @mock.patch('tripleoclient.v1.overcloud_roles.open') + @mock.patch('os.path.realpath') + def test_action(self, realpath_mock, open_mock, copy_mock): + realpath_mock.return_value = '/tmp' + get_roles_mock = mock.MagicMock() + get_roles_mock.return_value = ['Controller', 'Compute'] + capture_mock = mock.MagicMock() + self.cmd._get_roles = get_roles_mock + self.cmd._capture_output = capture_mock + + arglist = ['--roles-path', '/tmp', 'Controller', 'Compute'] + verifylist = [ + ('roles_path', '/tmp'), + ('roles', ['Controller', 'Compute']) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + capture_mock.assert_called_once_with(None) + get_roles_mock.assert_called_once_with('/tmp') + open_mock.assert_any_call('/tmp/Controller.yaml', 'r') + open_mock.assert_any_call('/tmp/Compute.yaml', 'r') + + @mock.patch('shutil.copyfileobj') + @mock.patch('tripleoclient.v1.overcloud_roles.open') + @mock.patch('os.path.realpath') + def test_action_with_outputfile(self, realpath_mock, open_mock, copy_mock): + realpath_mock.return_value = '/tmp' + get_roles_mock = mock.MagicMock() + get_roles_mock.return_value = ['Controller', 'Compute'] + capture_mock = mock.MagicMock() + self.cmd._get_roles = get_roles_mock + self.cmd._capture_output = capture_mock + + arglist = ['--roles-path', '/tmp', '-o', 'foo.yaml', + 'Controller', 'Compute'] + verifylist = [ + ('output_file', 'foo.yaml'), + ('roles_path', '/tmp'), + ('roles', ['Controller', 'Compute']) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + capture_mock.assert_called_once_with('foo.yaml') + get_roles_mock.assert_called_once_with('/tmp') + open_mock.assert_any_call('/tmp/Controller.yaml', 'r') + open_mock.assert_any_call('/tmp/Compute.yaml', 'r') + + @mock.patch('os.path.realpath') + def test_action_with_invald_roles(self, realpath_mock): + realpath_mock.return_value = '/tmp' + get_roles_mock = mock.MagicMock() + get_roles_mock.return_value = ['Controller', 'Compute'] + capture_mock = mock.MagicMock() + self.cmd._get_roles = get_roles_mock + self.cmd._capture_output = capture_mock + + arglist = ['--roles-path', '/tmp', 'Foo', 'Bar'] + verifylist = [ + ('roles_path', '/tmp'), + ('roles', ['Foo', 'Bar']) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(NotFound, self.cmd.take_action, parsed_args) + capture_mock.assert_called_once_with(None) + get_roles_mock.assert_called_once_with('/tmp') + + +class TestOvercloudRoleInfo(fakes.TestDeployOvercloud): + + def setUp(self): + super(TestOvercloudRoleInfo, self).setUp() + self.cmd = overcloud_roles.RoleShow(self.app, None) + + @mock.patch('yaml.safe_load') + @mock.patch('tripleoclient.v1.overcloud_roles.open') + @mock.patch('os.path.realpath') + def test_action(self, realpath_mock, open_mock, yaml_mock): + realpath_mock.return_value = '/tmp' + yaml_mock.return_value = [{'name': 'foo', 'Services': ['a', 'b']}] + + arglist = ['--roles-path', '/tmp', 'foo'] + verifylist = [ + ('roles_path', '/tmp'), + ('role', 'foo') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + open_mock.assert_called_once_with('/tmp/foo.yaml', 'r') + + @mock.patch('tripleoclient.v1.overcloud_roles.open') + @mock.patch('os.path.realpath') + def test_action_invalid_role(self, realpath_mock, open_mock): + realpath_mock.return_value = '/tmp' + open_mock.side_effect = IOError('bar') + + arglist = ['--roles-path', '/tmp', 'foo'] + verifylist = [ + ('roles_path', '/tmp'), + ('role', 'foo') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(NotFound, self.cmd.take_action, parsed_args) + open_mock.assert_called_once_with('/tmp/foo.yaml', 'r') diff --git a/tripleoclient/v1/overcloud_roles.py b/tripleoclient/v1/overcloud_roles.py new file mode 100644 index 000000000..77f8fcc1c --- /dev/null +++ b/tripleoclient/v1/overcloud_roles.py @@ -0,0 +1,157 @@ +# Copyright 2017 Red Hat, Inc. +# +# 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 __future__ import print_function + +import collections +import os +import shutil +import sys +import yaml + +from osc_lib.command import command +from tripleoclient.constants import TRIPLEO_HEAT_TEMPLATES +from tripleoclient.exceptions import NotFound + + +class RolesBaseCommand(command.Command): + auth_required = False + + def get_parser(self, prog_name): + parser = super(RolesBaseCommand, self).get_parser(prog_name) + path = os.path.join(TRIPLEO_HEAT_TEMPLATES, 'roles') + parser.add_argument('--roles-path', metavar='', + default=path, + help='Filesystem path containing the role yaml ' + 'files. By default this is {}'.format(path)) + return parser + + def _get_roles(self, directory): + """Return array of roles in roles path""" + if not os.path.exists(directory): + raise ValueError("Invalid roles path specified: {}".format( + directory)) + roles = [] + for f in os.listdir(directory): + if f.endswith(".yaml"): + roles.append(f[:-5]) + roles.sort() + return roles + + +class RolesGenerate(RolesBaseCommand): + """Generate roles_data.yaml file""" + def get_parser(self, prog_name): + parser = super(RolesGenerate, self).get_parser(prog_name) + parser.add_argument('-o', '--output-file', metavar='', + help='File to capture all output to. For example, ' + 'roles_data.yaml') + parser.add_argument('roles', nargs="+", metavar='', + help='List of roles to use to generate the ' + 'roles_data.yaml file for the deployment. ' + 'NOTE: Ordering is important if no role has ' + 'the "primary" and "controller" tags. If no ' + 'role is tagged then the first role listed ' + 'will be considered the primary role. This ' + 'usually is the controller role.') + return parser + + def _capture_output(self, filename=None): + """Capture stdout to a file if provided""" + if filename is not None: + sys.stdout = open(filename, 'w') + + def _print_header(self): + """Print file header""" + header = ["#" * 79, + "# File generated by tripleoclient", + "#" * 79] + print("\n".join(header)) + + def take_action(self, parsed_args): + """Generate roles_data.yaml from imputed roles + + From the provided roles, validate that we have yaml files for the each + role in our roles path and print them out concatenated together in the + order they were provided. + """ + self.log.debug('take_action({})'.format(parsed_args)) + self._capture_output(parsed_args.output_file) + roles_path = os.path.realpath(parsed_args.roles_path) + valid_roles = set(self._get_roles(roles_path)) + # eliminate any dupes from the command line with an OrderedDict + requested_roles = collections.OrderedDict.fromkeys(parsed_args.roles) + role_check = set(requested_roles.keys()) - valid_roles + if len(role_check) > 0: + msg = "Invalid roles requested: {}\nValid Roles:\n{}".format( + role_check, '\n'.join(valid_roles) + ) + raise NotFound(msg) + + self._print_header() + for role in requested_roles: + file_path = os.path.join(roles_path, "{}.yaml".format(role)) + with open(file_path, "r") as f: + shutil.copyfileobj(f, sys.stdout) + + +class RoleList(RolesBaseCommand): + """List availables roles""" + 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)) + roles_path = os.path.realpath(parsed_args.roles_path) + roles = self._get_roles(roles_path) + print('\n'.join(roles)) + + +class RoleShow(RolesBaseCommand): + """Show information about a given role""" + def get_parser(self, prog_name): + parser = super(RoleShow, self).get_parser(prog_name) + parser.add_argument('role', metavar='', + help='Role to display more information about.') + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action({})'.format(parsed_args)) + roles_path = os.path.realpath(parsed_args.roles_path) + file_path = os.path.join(roles_path, + '{}.yaml'.format(parsed_args.role)) + try: + with open(file_path, 'r') as f: + role = yaml.safe_load(f)[0] + except IOError: + raise NotFound("Role '{}' not found. Use 'openstack overcloud " + "roles list' to see the available roles.". + format(parsed_args.role)) + + if 'name' in role: + print('#' * 79) + print("# Role Data for '{}'".format(role['name'])) + print('#' * 79) + + for key in sorted(role.keys()): + print("{}:".format(key), end='') + value = role[key] + + if isinstance(value, (list, tuple)): + print('') + print('\n'.join([' * {0}'.format(v) for v in value])) + else: + print(" '{}'".format(value))