More v2 roles class work

* added ability to set parents (will be used when the deps are loaded)
* added role caching, so roles are not reloaded needlessly (and for
  use in detecting when roles have already been run)
* reworked the way metadata was stored - now individual attribute fields
  instead of a dictionary blob
This commit is contained in:
James Cammarata 2014-10-27 13:25:32 -05:00
parent 547c0d8140
commit 7ea84d7499
2 changed files with 105 additions and 10 deletions

View file

@ -23,7 +23,9 @@ from six import iteritems, string_types
import os import os
from ansible.errors import AnsibleError from hashlib import md5
from ansible.errors import AnsibleError, AnsibleParserError
from ansible.parsing.yaml import DataLoader from ansible.parsing.yaml import DataLoader
from ansible.playbook.attribute import FieldAttribute from ansible.playbook.attribute import FieldAttribute
from ansible.playbook.base import Base from ansible.playbook.base import Base
@ -31,6 +33,20 @@ from ansible.playbook.block import Block
from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject, AnsibleMapping from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject, AnsibleMapping
__all__ = ['Role']
# The role cache is used to prevent re-loading roles, which
# may already exist. Keys into this cache are the MD5 hash
# of the role definition (for dictionary definitions, this
# will be based on the repr() of the dictionary object)
_ROLE_CACHE = dict()
_VALID_METADATA_KEYS = [
'dependencies',
'allow_duplicates',
'galaxy_info',
]
class Role(Base): class Role(Base):
_role_name = FieldAttribute(isa='string') _role_name = FieldAttribute(isa='string')
@ -41,12 +57,19 @@ class Role(Base):
_task_blocks = FieldAttribute(isa='list', default=[]) _task_blocks = FieldAttribute(isa='list', default=[])
_handler_blocks = FieldAttribute(isa='list', default=[]) _handler_blocks = FieldAttribute(isa='list', default=[])
_params = FieldAttribute(isa='dict', default=dict()) _params = FieldAttribute(isa='dict', default=dict())
_metadata = FieldAttribute(isa='dict', default=dict())
_default_vars = FieldAttribute(isa='dict', default=dict()) _default_vars = FieldAttribute(isa='dict', default=dict())
_role_vars = FieldAttribute(isa='dict', default=dict()) _role_vars = FieldAttribute(isa='dict', default=dict())
# Attributes based on values in metadata. These MUST line up
# with the values stored in _VALID_METADATA_KEYS
_dependencies = FieldAttribute(isa='list', default=[])
_allow_duplicates = FieldAttribute(isa='bool', default=False)
_galaxy_info = FieldAttribute(isa='dict', default=dict())
def __init__(self, loader=DataLoader): def __init__(self, loader=DataLoader):
self._role_path = None self._role_path = None
self._parents = []
super(Role, self).__init__(loader=loader) super(Role, self).__init__(loader=loader)
def __repr__(self): def __repr__(self):
@ -56,10 +79,30 @@ class Role(Base):
return self._attributes['role_name'] return self._attributes['role_name']
@staticmethod @staticmethod
def load(data): def load(data, parent_role=None):
assert isinstance(data, string_types) or isinstance(data, dict) assert isinstance(data, string_types) or isinstance(data, dict)
r = Role()
r.load_data(data) # Check to see if this role has been loaded already, based on the
# role definition, partially to save loading time and also to make
# sure that roles are run a single time unless specifically allowed
# to run more than once
# FIXME: the tags and conditionals, if specified in the role def,
# should not figure into the resulting hash
cache_key = md5(repr(data))
if cache_key in _ROLE_CACHE:
r = _ROLE_CACHE[cache_key]
else:
# load the role
r = Role()
r.load_data(data)
# and cache it for next time
_ROLE_CACHE[cache_key] = r
# now add the parent to the (new) role
if parent_role:
r.add_parent(parent_role)
return r return r
#------------------------------------------------------------------------------ #------------------------------------------------------------------------------
@ -101,12 +144,16 @@ class Role(Base):
new_ds['role_path'] = role_path new_ds['role_path'] = role_path
# load the role's files, if they exist # load the role's files, if they exist
new_ds['metadata'] = self._load_role_yaml(role_path, 'meta')
new_ds['task_blocks'] = self._load_role_yaml(role_path, 'tasks') new_ds['task_blocks'] = self._load_role_yaml(role_path, 'tasks')
new_ds['handler_blocks'] = self._load_role_yaml(role_path, 'handlers') new_ds['handler_blocks'] = self._load_role_yaml(role_path, 'handlers')
new_ds['default_vars'] = self._load_role_yaml(role_path, 'defaults') new_ds['default_vars'] = self._load_role_yaml(role_path, 'defaults')
new_ds['role_vars'] = self._load_role_yaml(role_path, 'vars') new_ds['role_vars'] = self._load_role_yaml(role_path, 'vars')
# we treat metadata slightly differently: we instead pull out the
# valid metadata keys and munge them directly into new_ds
metadata_ds = self._munge_metadata(role_name, role_path)
new_ds.update(metadata_ds)
# and return the newly munged ds # and return the newly munged ds
return new_ds return new_ds
@ -256,6 +303,32 @@ class Role(Base):
return ds return ds
def _munge_metadata(self, role_name, role_path):
'''
loads the metadata main.yml (if it exists) and creates a clean
datastructure we can merge into the newly munged ds
'''
meta_ds = dict()
metadata = self._load_role_yaml(role_path, 'meta')
if metadata:
if not isinstance(metadata, dict):
raise AnsibleParserError("The metadata for role '%s' should be a dictionary, instead it is a %s" % (role_name, type(metadata)), obj=metadata)
for key in metadata:
if key in _VALID_METADATA_KEYS:
if isinstance(metadata[key], dict):
meta_ds[key] = metadata[key].copy()
elif isinstance(metadata[key], list):
meta_ds[key] = metadata[key][:]
else:
meta_ds[key] = metadata[key]
else:
raise AnsibleParserError("%s is not a valid metadata key for role '%s'" % (key, role_name), obj=metadata)
return meta_ds
#------------------------------------------------------------------------------ #------------------------------------------------------------------------------
# attribute loading defs # attribute loading defs
@ -280,6 +353,13 @@ class Role(Base):
#------------------------------------------------------------------------------ #------------------------------------------------------------------------------
# other functions # other functions
def add_parent(self, parent_role):
''' adds a role to the list of this roles parents '''
assert isinstance(role, Role)
if parent_role not in self._parents:
self._parents.append(parent_role)
def get_variables(self): def get_variables(self):
# returns the merged variables for this role, including # returns the merged variables for this role, including
# recursively merging those of all child roles # recursively merging those of all child roles

View file

@ -22,6 +22,7 @@ __metaclass__ = type
from ansible.compat.tests import unittest from ansible.compat.tests import unittest
from ansible.compat.tests.mock import patch, MagicMock from ansible.compat.tests.mock import patch, MagicMock
from ansible.errors import AnsibleParserError
from ansible.playbook.block import Block from ansible.playbook.block import Block
from ansible.playbook.role import Role from ansible.playbook.role import Role
from ansible.playbook.task import Task from ansible.playbook.task import Task
@ -118,18 +119,32 @@ class TestRole(unittest.TestCase):
@patch.object(Role, '_load_role_yaml') @patch.object(Role, '_load_role_yaml')
def test_load_role_with_metadata(self, _load_role_yaml, _get_role_path): def test_load_role_with_metadata(self, _load_role_yaml, _get_role_path):
_get_role_path.return_value = ('foo', '/etc/ansible/roles/foo')
def fake_load_role_yaml(role_path, subdir): def fake_load_role_yaml(role_path, subdir):
if role_path == '/etc/ansible/roles/foo': if role_path == '/etc/ansible/roles/foo':
if subdir == 'meta': if subdir == 'meta':
return dict(dependencies=[], allow_duplicates=False) return dict(dependencies=['bar'], allow_duplicates=True, galaxy_info=dict(a='1', b='2', c='3'))
elif role_path == '/etc/ansible/roles/bad1':
if subdir == 'meta':
return 1
elif role_path == '/etc/ansible/roles/bad2':
if subdir == 'meta':
return dict(foo='bar')
return None return None
_load_role_yaml.side_effect = fake_load_role_yaml _load_role_yaml.side_effect = fake_load_role_yaml
_get_role_path.return_value = ('foo', '/etc/ansible/roles/foo')
r = Role.load('foo') r = Role.load('foo')
self.assertEqual(r.metadata, dict(dependencies=[], allow_duplicates=False)) self.assertEqual(r.dependencies, ['bar'])
self.assertEqual(r.allow_duplicates, True)
self.assertEqual(r.galaxy_info, dict(a='1', b='2', c='3'))
_get_role_path.return_value = ('bad1', '/etc/ansible/roles/bad1')
self.assertRaises(AnsibleParserError, Role.load, 'bad1')
_get_role_path.return_value = ('bad2', '/etc/ansible/roles/bad2')
self.assertRaises(AnsibleParserError, Role.load, 'bad2')
@patch.object(Role, '_get_role_path') @patch.object(Role, '_get_role_path')
@patch.object(Role, '_load_role_yaml') @patch.object(Role, '_load_role_yaml')