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-environmentschanges/00/466900/9 7.1.0
parent
933b4fa422
commit
be2a20637a
@ -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.
|
@ -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')
|
@ -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))
|
Loading…
Reference in new issue