Merge "Move roles utilities to common"
This commit is contained in:
commit
8db0a2d95a
@ -110,3 +110,11 @@ class PlanOperationError(Exception):
|
||||
|
||||
class DeriveParamsError(Exception):
|
||||
"""Error while performing a derive parameters operation"""
|
||||
|
||||
|
||||
class NotFound(Exception):
|
||||
"""Resource not found"""
|
||||
|
||||
|
||||
class RoleMetadataError(Exception):
|
||||
"""Role metadata is invalid"""
|
||||
|
121
tripleo_common/tests/utils/test_roles.py
Normal file
121
tripleo_common/tests/utils/test_roles.py
Normal file
@ -0,0 +1,121 @@
|
||||
# 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
|
||||
import yaml
|
||||
|
||||
from tripleo_common.exception import NotFound
|
||||
from tripleo_common.exception import RoleMetadataError
|
||||
from tripleo_common.tests import base
|
||||
from tripleo_common.utils import roles as rolesutils
|
||||
|
||||
SAMPLE_ROLE = """
|
||||
###############################################################################
|
||||
# Role: sample #
|
||||
###############################################################################
|
||||
- name: sample
|
||||
description: |
|
||||
Sample!
|
||||
networks:
|
||||
- InternalApi
|
||||
HostnameFormatDefault: '%stackname%-sample-%index%'
|
||||
ServicesDefault:
|
||||
- OS::TripleO::Services::Ntp
|
||||
"""
|
||||
SAMPLE_ROLE_OBJ = {
|
||||
'HostnameFormatDefault': '%stackname%-sample-%index%',
|
||||
'ServicesDefault': ['OS::TripleO::Services::Ntp'],
|
||||
'description': 'Sample!\n',
|
||||
'name': 'sample',
|
||||
'networks': ['InternalApi']
|
||||
}
|
||||
|
||||
|
||||
class TestRolesUtils(base.TestCase):
|
||||
@mock.patch('os.listdir')
|
||||
@mock.patch('os.path.exists')
|
||||
def test_get_roles_from_directory(self, exists_mock, listdir_mock):
|
||||
exists_mock.return_value = True
|
||||
listdir_mock.return_value = ['b.yaml', 'a.yaml']
|
||||
self.assertEqual(rolesutils.get_roles_list_from_directory('/foo'),
|
||||
['a', 'b'])
|
||||
|
||||
@mock.patch('os.listdir')
|
||||
@mock.patch('os.path.exists')
|
||||
def test_get_roles_from_directory_failure(self, exists_mock, listdir_mock):
|
||||
exists_mock.return_value = False
|
||||
self.assertRaises(ValueError, rolesutils.get_roles_list_from_directory,
|
||||
['/foo'])
|
||||
|
||||
def test_validate_roles(self):
|
||||
available_roles = ['a', 'b', 'c']
|
||||
requested_roles = ['b', 'c']
|
||||
try:
|
||||
rolesutils.check_role_exists(available_roles, requested_roles)
|
||||
except Exception:
|
||||
self.fail('Exception raised')
|
||||
|
||||
def test_validate_roles_with_invalid_role(self):
|
||||
available_roles = ['a', 'b', 'c']
|
||||
requested_roles = ['b', 'd']
|
||||
self.assertRaises(NotFound, rolesutils.check_role_exists,
|
||||
available_roles, requested_roles)
|
||||
|
||||
@mock.patch('tripleo_common.utils.roles.check_role_exists')
|
||||
@mock.patch('tripleo_common.utils.roles.get_roles_list_from_directory')
|
||||
def test_generate_roles_data_from_directory(self, get_roles_mock,
|
||||
check_mock):
|
||||
get_roles_mock.return_value = ['foo', 'bar', 'baz']
|
||||
m = mock.mock_open(read_data=SAMPLE_ROLE)
|
||||
with mock.patch('tripleo_common.utils.roles.open', m) as open_mock:
|
||||
r = rolesutils.generate_roles_data_from_directory('/foo',
|
||||
['foo', 'bar'])
|
||||
open_mock.assert_any_call('/foo/foo.yaml', 'r')
|
||||
open_mock.assert_any_call('/foo/bar.yaml', 'r')
|
||||
|
||||
header = '\n'.join(["#" * 79,
|
||||
"# File generated by TripleO",
|
||||
"#" * 79,
|
||||
""])
|
||||
expected = header + SAMPLE_ROLE * 2
|
||||
self.assertEqual(expected, r)
|
||||
get_roles_mock.assert_called_with('/foo')
|
||||
check_mock.assert_called_with(['foo', 'bar', 'baz'], ['foo', 'bar'])
|
||||
|
||||
def test_validate_role_yaml(self):
|
||||
role = rolesutils.validate_role_yaml(SAMPLE_ROLE)
|
||||
self.assertEqual(SAMPLE_ROLE_OBJ, role)
|
||||
|
||||
def test_validate_role_yaml_with_file(self):
|
||||
m = mock.mock_open(read_data=SAMPLE_ROLE)
|
||||
with mock.patch('tripleo_common.utils.roles.open', m):
|
||||
r = rolesutils.validate_role_yaml(role_path='/foo.yaml')
|
||||
self.assertEqual(SAMPLE_ROLE_OBJ, r)
|
||||
|
||||
def test_validate_role_yaml_invalid_params(self):
|
||||
self.assertRaises(ValueError, rolesutils.validate_role_yaml, 'foo',
|
||||
'bar')
|
||||
|
||||
def test_validate_role_yaml_missing_name(self):
|
||||
role = yaml.safe_load(SAMPLE_ROLE)
|
||||
del role[0]['name']
|
||||
self.assertRaises(RoleMetadataError, rolesutils.validate_role_yaml,
|
||||
yaml.dump(role))
|
||||
|
||||
def test_validate_role_yaml_invalid_type(self):
|
||||
role = yaml.safe_load(SAMPLE_ROLE)
|
||||
role[0]['CountDefault'] = 'should not be a string'
|
||||
self.assertRaises(RoleMetadataError, rolesutils.validate_role_yaml,
|
||||
yaml.dump(role))
|
123
tripleo_common/utils/roles.py
Normal file
123
tripleo_common/utils/roles.py
Normal file
@ -0,0 +1,123 @@
|
||||
# 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 os
|
||||
import shutil
|
||||
import yaml
|
||||
|
||||
from six.moves import cStringIO as StringIO
|
||||
from tripleo_common.exception import NotFound
|
||||
from tripleo_common.exception import RoleMetadataError
|
||||
|
||||
|
||||
def get_roles_list_from_directory(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
|
||||
|
||||
|
||||
def check_role_exists(available_roles, requested_roles):
|
||||
"""Performs a check on the requested roles to ensure they exist
|
||||
|
||||
:param available_roles list of available role names
|
||||
:param requested_roles list of requested role names
|
||||
:exception NotFound if a role in the requested list is not available
|
||||
"""
|
||||
role_check = set(requested_roles) - set(available_roles)
|
||||
if len(role_check) > 0:
|
||||
msg = "Invalid roles requested: {}\nValid Roles:\n{}".format(
|
||||
','.join(role_check), '\n'.join(available_roles)
|
||||
)
|
||||
raise NotFound(msg)
|
||||
|
||||
|
||||
def generate_roles_data_from_directory(directory, roles, validate=True):
|
||||
"""Generate a roles data file using roles from a local path
|
||||
|
||||
:param directory local filesystem path to the roles
|
||||
:param roles ordered list of roles
|
||||
:param validate validate the metadata format in the role yaml files
|
||||
:returns string contents of the roles_data.yaml
|
||||
"""
|
||||
available_roles = get_roles_list_from_directory(directory)
|
||||
check_role_exists(available_roles, roles)
|
||||
output = StringIO()
|
||||
|
||||
header = ["#" * 79,
|
||||
"# File generated by TripleO",
|
||||
"#" * 79,
|
||||
""]
|
||||
output.write("\n".join(header))
|
||||
|
||||
for role in roles:
|
||||
file_path = os.path.join(directory, "{}.yaml".format(role))
|
||||
if validate:
|
||||
validate_role_yaml(role_path=file_path)
|
||||
with open(file_path, "r") as f:
|
||||
shutil.copyfileobj(f, output)
|
||||
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
def validate_role_yaml(role_data=None, role_path=None):
|
||||
"""Basic role yaml validation
|
||||
|
||||
:param role_data the role yaml in string form
|
||||
:param role_path the path to the yaml file to validate.
|
||||
:exception RoleMetadataError
|
||||
:returns parsed role yaml object
|
||||
"""
|
||||
if role_data and role_path or (not role_data and not role_path):
|
||||
raise ValueError('Either role_data OR role_path must be specified')
|
||||
|
||||
if role_path:
|
||||
with open(role_path, 'r') as f:
|
||||
role_data = f.read()
|
||||
|
||||
try:
|
||||
role = yaml.safe_load(role_data)[0]
|
||||
except yaml.YAMLError:
|
||||
raise RoleMetadataError('Unable to parse role yaml')
|
||||
|
||||
schema = {
|
||||
'name': {'type': str},
|
||||
'CountDefault': {'type': int},
|
||||
'HostnameFormatDefault': {'type': str},
|
||||
'disable_constraints': {'type': bool},
|
||||
'disable_upgrade_deployment': {'type': bool},
|
||||
'upgrade_batch_size': {'type': int},
|
||||
'ServicesDefault': {'type': list},
|
||||
'tags': {'type': list},
|
||||
'description': {'type': str},
|
||||
'networks': {'type': list},
|
||||
}
|
||||
|
||||
if 'name' not in role:
|
||||
raise RoleMetadataError('Role name is missing from the role')
|
||||
|
||||
# validate numeric metadata is numeric
|
||||
for k in schema:
|
||||
if k in role and not isinstance(role[k], schema[k]['type']):
|
||||
msg = "Role '{}': {} is not of expected type {}".format(
|
||||
role['name'], k, schema[k]['type'])
|
||||
raise RoleMetadataError(msg)
|
||||
return role
|
Loading…
Reference in New Issue
Block a user