From 0ed9746db393bb169dceb3ead5912305b7d8e2af Mon Sep 17 00:00:00 2001 From: James Cammarata Date: Tue, 4 Nov 2014 15:16:11 -0600 Subject: [PATCH] Adding Play class for v2 --- v2/ansible/parsing/mod_args.py | 2 +- v2/ansible/playbook/block.py | 23 ++--- v2/ansible/playbook/helpers.py | 76 +++++++++++++++ v2/ansible/playbook/play.py | 137 +++++++++++++++++++++++++++ v2/ansible/playbook/role/__init__.py | 23 +---- v2/ansible/playbook/role/metadata.py | 13 +-- v2/test/playbook/test_block.py | 10 -- v2/test/playbook/test_play.py | 120 +++++++++++++++++++++++ 8 files changed, 349 insertions(+), 55 deletions(-) create mode 100644 v2/ansible/playbook/helpers.py create mode 100644 v2/test/playbook/test_play.py diff --git a/v2/ansible/parsing/mod_args.py b/v2/ansible/parsing/mod_args.py index 5e7c4225dfa..7f4f42bddd2 100644 --- a/v2/ansible/parsing/mod_args.py +++ b/v2/ansible/parsing/mod_args.py @@ -190,7 +190,7 @@ class ModuleArgsParser: task, dealing with all sorts of levels of fuzziness. ''' - assert type(ds) == dict + assert isinstance(ds, dict) thing = None diff --git a/v2/ansible/playbook/block.py b/v2/ansible/playbook/block.py index 5f21cdaf606..cc5ccacc405 100644 --- a/v2/ansible/playbook/block.py +++ b/v2/ansible/playbook/block.py @@ -19,9 +19,9 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from ansible.playbook.base import Base -from ansible.playbook.task import Task from ansible.playbook.attribute import Attribute, FieldAttribute +from ansible.playbook.base import Base +from ansible.playbook.helpers import load_list_of_tasks class Block(Base): @@ -60,25 +60,20 @@ class Block(Base): is_block = True break if not is_block: - return dict(block=ds) + if isinstance(ds, list): + return dict(block=ds) + else: + return dict(block=[ds]) return ds - def _load_list_of_tasks(self, ds): - assert type(ds) == list - task_list = [] - for task in ds: - t = Task.load(task) - task_list.append(t) - return task_list - def _load_block(self, attr, ds): - return self._load_list_of_tasks(ds) + return load_list_of_tasks(ds) def _load_rescue(self, attr, ds): - return self._load_list_of_tasks(ds) + return load_list_of_tasks(ds) def _load_always(self, attr, ds): - return self._load_list_of_tasks(ds) + return load_list_of_tasks(ds) # not currently used #def _load_otherwise(self, attr, ds): diff --git a/v2/ansible/playbook/helpers.py b/v2/ansible/playbook/helpers.py new file mode 100644 index 00000000000..6985ad7808c --- /dev/null +++ b/v2/ansible/playbook/helpers.py @@ -0,0 +1,76 @@ +# (c) 2012-2014, Michael DeHaan +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from types import NoneType + + +def load_list_of_blocks(ds, role=None, loader=None): + ''' + Given a list of mixed task/block data (parsed from YAML), + return a list of Block() objects, where implicit blocks + are created for each bare Task. + ''' + + # we import here to prevent a circular dependency with imports + from ansible.playbook.block import Block + + assert type(ds) in (list, NoneType) + + block_list = [] + if ds: + for block in ds: + b = Block.load(block, role=role, loader=loader) + block_list.append(b) + + return block_list + +def load_list_of_tasks(ds, block=None, role=None, loader=None): + ''' + Given a list of task datastructures (parsed from YAML), + return a list of Task() objects. + ''' + + # we import here to prevent a circular dependency with imports + from ansible.playbook.task import Task + + assert type(ds) == list + + task_list = [] + for task in ds: + t = Task.load(task, block=block, role=role, loader=loader) + task_list.append(t) + + return task_list + +def load_list_of_roles(ds, loader=None): + ''' + Loads and returns a list of RoleInclude objects from the datastructure + list of role definitions + ''' + + # we import here to prevent a circular dependency with imports + from ansible.playbook.role.include import RoleInclude + + assert isinstance(ds, list) + + roles = [] + for role_def in ds: + i = RoleInclude.load(role_def, loader=loader) + roles.append(i) + + return roles + diff --git a/v2/ansible/playbook/play.py b/v2/ansible/playbook/play.py index ae8ccff5952..3c8a4bcb87f 100644 --- a/v2/ansible/playbook/play.py +++ b/v2/ansible/playbook/play.py @@ -18,3 +18,140 @@ # Make coding more python3-ish from __future__ import (absolute_import, division, print_function) __metaclass__ = type + +from ansible.errors import AnsibleError, AnsibleParserError + +from ansible.parsing.yaml import DataLoader + +from ansible.playbook.attribute import Attribute, FieldAttribute +from ansible.playbook.base import Base +from ansible.playbook.helpers import load_list_of_blocks, load_list_of_roles + + +__all__ = ['Play'] + + +class Play(Base): + + """ + A play is a language feature that represents a list of roles and/or + task/handler blocks to execute on a given set of hosts. + + Usage: + + Play.load(datastructure) -> Play + Play.something(...) + """ + + # ================================================================================= + # Connection-Related Attributes + _accelerate = FieldAttribute(isa='bool', default=False) + _accelerate_ipv6 = FieldAttribute(isa='bool', default=False) + _accelerate_port = FieldAttribute(isa='int', default=5099) + _connection = FieldAttribute(isa='string', default='smart') + _gather_facts = FieldAttribute(isa='string', default='smart') + _hosts = FieldAttribute(isa='list', default=[]) + _name = FieldAttribute(isa='string', default='') + _port = FieldAttribute(isa='int', default=22) + _remote_user = FieldAttribute(isa='string', default='root') + _su = FieldAttribute(isa='bool', default=False) + _su_user = FieldAttribute(isa='string', default='root') + _sudo = FieldAttribute(isa='bool', default=False) + _sudo_user = FieldAttribute(isa='string', default='root') + _tags = FieldAttribute(isa='list', default=[]) + + # Variable Attributes + _vars = FieldAttribute(isa='dict', default=dict()) + _vars_files = FieldAttribute(isa='list', default=[]) + _vars_prompt = FieldAttribute(isa='dict', default=dict()) + _vault_password = FieldAttribute(isa='string') + + # Block (Task) Lists Attributes + _handlers = FieldAttribute(isa='list', default=[]) + _pre_tasks = FieldAttribute(isa='list', default=[]) + _post_tasks = FieldAttribute(isa='list', default=[]) + _tasks = FieldAttribute(isa='list', default=[]) + + # Role Attributes + _roles = FieldAttribute(isa='list', default=[]) + + # Flag/Setting Attributes + _any_errors_fatal = FieldAttribute(isa='bool', default=False) + _max_fail_percentage = FieldAttribute(isa='string', default='0') + _no_log = FieldAttribute(isa='bool', default=False) + _serial = FieldAttribute(isa='int', default=0) + + # ================================================================================= + + def __init__(self): + super(Play, self).__init__() + + def __repr__(self): + return self.get_name() + + def get_name(self): + ''' return the name of the Play ''' + return "PLAY: %s" % self._attributes.get('name') + + @staticmethod + def load(data, loader=None): + p = Play() + return p.load_data(data, loader=loader) + + def munge(self, ds): + ''' + Adjusts play datastructure to cleanup old/legacy items + ''' + + assert isinstance(ds, dict) + + # The use of 'user' in the Play datastructure was deprecated to + # line up with the same change for Tasks, due to the fact that + # 'user' conflicted with the user module. + if 'user' in ds: + # this should never happen, but error out with a helpful message + # to the user if it does... + if 'remote_user' in ds: + raise AnsibleParserError("both 'user' and 'remote_user' are set for %s. The use of 'user' is deprecated, and should be removed" % self.get_name(), obj=ds) + + ds['remote_user'] = ds['user'] + del ds['user'] + + return ds + + def _load_tasks(self, attr, ds): + ''' + Loads a list of blocks from a list which may be mixed tasks/blocks. + Bare tasks outside of a block are given an implicit block. + ''' + return load_list_of_blocks(ds) + + def _load_pre_tasks(self, attr, ds): + ''' + Loads a list of blocks from a list which may be mixed tasks/blocks. + Bare tasks outside of a block are given an implicit block. + ''' + return load_list_of_blocks(ds) + + def _load_post_tasks(self, attr, ds): + ''' + Loads a list of blocks from a list which may be mixed tasks/blocks. + Bare tasks outside of a block are given an implicit block. + ''' + return load_list_of_blocks(ds) + + def _load_handlers(self, attr, ds): + ''' + Loads a list of blocks from a list which may be mixed handlers/blocks. + Bare handlers outside of a block are given an implicit block. + ''' + return load_list_of_blocks(ds) + + def _load_roles(self, attr, ds): + ''' + Loads and returns a list of RoleInclude objects from the datastructure + list of role definitions + ''' + return load_list_of_roles(ds, loader=self._loader) + + # FIXME: post_validation needs to ensure that su/sudo are not both set diff --git a/v2/ansible/playbook/role/__init__.py b/v2/ansible/playbook/role/__init__.py index ed7355f9214..4950e944d3d 100644 --- a/v2/ansible/playbook/role/__init__.py +++ b/v2/ansible/playbook/role/__init__.py @@ -30,7 +30,7 @@ from ansible.errors import AnsibleError, AnsibleParserError from ansible.parsing.yaml import DataLoader from ansible.playbook.attribute import FieldAttribute from ansible.playbook.base import Base -from ansible.playbook.block import Block +from ansible.playbook.helpers import load_list_of_blocks from ansible.playbook.role.include import RoleInclude from ansible.playbook.role.metadata import RoleMetadata @@ -95,11 +95,11 @@ class Role: task_data = self._load_role_yaml('tasks') if task_data: - self._task_blocks = self._load_list_of_blocks(task_data) + self._task_blocks = load_list_of_blocks(task_data) handler_data = self._load_role_yaml('handlers') if handler_data: - self._handler_blocks = self._load_list_of_blocks(handler_data) + self._handler_blocks = load_list_of_blocks(handler_data) # vars and default vars are regular dictionaries self._role_vars = self._load_role_yaml('vars') @@ -135,23 +135,6 @@ class Role: return m # exactly one main file return possible_mains[0] # zero mains (we still need to return something) - def _load_list_of_blocks(self, ds): - ''' - Given a list of mixed task/block data (parsed from YAML), - return a list of Block() objects, where implicit blocks - are created for each bare Task. - ''' - - assert type(ds) in (list, NoneType) - - block_list = [] - if ds: - for block in ds: - b = Block(block) - block_list.append(b) - - return block_list - def _load_dependencies(self): ''' Recursively loads role dependencies from the metadata list of diff --git a/v2/ansible/playbook/role/metadata.py b/v2/ansible/playbook/role/metadata.py index 485e3da59f2..19b0f01f621 100644 --- a/v2/ansible/playbook/role/metadata.py +++ b/v2/ansible/playbook/role/metadata.py @@ -24,6 +24,7 @@ from six import iteritems, string_types from ansible.errors import AnsibleParserError from ansible.playbook.attribute import Attribute, FieldAttribute from ansible.playbook.base import Base +from ansible.playbook.helpers import load_list_of_roles from ansible.playbook.role.include import RoleInclude @@ -58,18 +59,10 @@ class RoleMetadata(Base): def _load_dependencies(self, attr, ds): ''' - This is a helper loading function for the dependencis list, + This is a helper loading function for the dependencies list, which returns a list of RoleInclude objects ''' - - assert isinstance(ds, list) - - deps = [] - for role_def in ds: - i = RoleInclude.load(role_def, loader=self._loader) - deps.append(i) - - return deps + return load_list_of_roles(ds, loader=self._loader) def _load_galaxy_info(self, attr, ds): ''' diff --git a/v2/test/playbook/test_block.py b/v2/test/playbook/test_block.py index ccb8f2b6d3c..348681527bb 100644 --- a/v2/test/playbook/test_block.py +++ b/v2/test/playbook/test_block.py @@ -37,16 +37,6 @@ class TestBlock(unittest.TestCase): def test_construct_block_with_role(self): pass - def test_block__load_list_of_tasks(self): - task = dict(action='test') - b = Block() - self.assertEqual(b._load_list_of_tasks([]), []) - res = b._load_list_of_tasks([task]) - self.assertEqual(len(res), 1) - assert isinstance(res[0], Task) - res = b._load_list_of_tasks([task,task,task]) - self.assertEqual(len(res), 3) - def test_load_block_simple(self): ds = dict( block = [], diff --git a/v2/test/playbook/test_play.py b/v2/test/playbook/test_play.py new file mode 100644 index 00000000000..14732a1f9fb --- /dev/null +++ b/v2/test/playbook/test_play.py @@ -0,0 +1,120 @@ +# (c) 2012-2014, Michael DeHaan +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.compat.tests import unittest +from ansible.compat.tests.mock import patch, MagicMock + +from ansible.errors import AnsibleError, AnsibleParserError +from ansible.playbook.play import Play +from ansible.playbook.role import Role +from ansible.playbook.task import Task + +from test.mock.loader import DictDataLoader + +class TestPlay(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_empty_play(self): + p = Play.load(dict()) + self.assertEqual(str(p), "PLAY: ") + + def test_basic_play(self): + p = Play.load(dict( + name="test play", + hosts=['foo'], + gather_facts=False, + connection='local', + remote_user="root", + sudo=True, + sudo_user="testing", + )) + + def test_play_with_user_conflict(self): + p = Play.load(dict( + name="test play", + hosts=['foo'], + user="testing", + gather_facts=False, + )) + self.assertEqual(p.remote_user, "testing") + + def test_play_with_user_conflict(self): + play_data = dict( + name="test play", + hosts=['foo'], + user="testing", + remote_user="testing", + ) + self.assertRaises(AnsibleParserError, Play.load, play_data) + + def test_play_with_tasks(self): + p = Play.load(dict( + name="test play", + hosts=['foo'], + gather_facts=False, + tasks=[dict(action='shell echo "hello world"')], + )) + + def test_play_with_handlers(self): + p = Play.load(dict( + name="test play", + hosts=['foo'], + gather_facts=False, + handlers=[dict(action='shell echo "hello world"')], + )) + + def test_play_with_pre_tasks(self): + p = Play.load(dict( + name="test play", + hosts=['foo'], + gather_facts=False, + pre_tasks=[dict(action='shell echo "hello world"')], + )) + + def test_play_with_post_tasks(self): + p = Play.load(dict( + name="test play", + hosts=['foo'], + gather_facts=False, + post_tasks=[dict(action='shell echo "hello world"')], + )) + + def test_play_with_roles(self): + fake_loader = DictDataLoader({ + '/etc/ansible/roles/foo/tasks.yml': """ + - name: role task + shell: echo "hello world" + """, + }) + + p = Play.load(dict( + name="test play", + hosts=['foo'], + gather_facts=False, + roles=['foo'], + ), loader=fake_loader) + +