Provide commands for example roles

In order to improve the experiance for the end user, we have existing
sets of roles that can be combined in a roles_data.yaml file for the
deployment. This change exposes the ability to generate roles_data.yaml
from a folder containing role yaml files rather than requiring the end
user manually construct the roles_data.yaml.

Change-Id: I326bae5bdee088e03aa89128d253612ef89e5c0c
Related-Blueprint: example-custom-role-environments
This commit is contained in:
Alex Schultz 2017-05-22 14:56:37 -06:00
parent 933b4fa422
commit be2a20637a
5 changed files with 345 additions and 0 deletions

View File

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

View File

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

View File

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

View File

@ -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='<roles directory>',
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='<output file>',
help='File to capture all output to. For example, '
'roles_data.yaml')
parser.add_argument('roles', nargs="+", metavar='<role>',
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='<role>',
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))