From 14a7aef2920d74efdcb88237750ed0c9d929ef6a Mon Sep 17 00:00:00 2001 From: John Dewey Date: Thu, 5 Jan 2017 15:38:40 -0800 Subject: [PATCH] Implemented yaml inventory file Moved the generated host inventory from the proprietary "ini" format to Ansible's new yaml format. Fixes: #683 --- molecule/config.py | 20 +---- molecule/driver/docker.py | 21 ++--- molecule/provisioner.py | 57 ++++++------- test/unit/driver/test_docker.py | 15 ++-- test/unit/test_config.py | 45 ----------- test/unit/test_provisioner.py | 139 +++++++++++++++++++++++--------- 6 files changed, 143 insertions(+), 154 deletions(-) diff --git a/molecule/config.py b/molecule/config.py index e94930ee9..2850176ad 100644 --- a/molecule/config.py +++ b/molecule/config.py @@ -18,7 +18,6 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -import collections import os import anyconfig @@ -78,23 +77,6 @@ def lint(self): def platforms(self): return self.config['platforms'] - @property - def platform_groups(self): - # [baz] - # instance-2-default - # [foo] - # instance-1-default - # instance-2-default - # [bar] - # instance-1-default - dd = collections.defaultdict(list) - for platform in self.config['platforms']: - for group in platform.get('groups', []): - name = '{}-{}'.format(platform['name'], self.scenario.name) - dd[group].append(name) - - return dict(dd) - @property def provisioner(self): if self.config['provisioner']['name'] == 'ansible': @@ -153,7 +135,7 @@ def _get_defaults(self): 'config_file': 'ansible.cfg', 'diff': True, 'host_key_checking': False, - 'inventory_file': 'ansible_inventory', + 'inventory_file': 'ansible_inventory.yml', 'limit': 'all', 'playbook': 'playbook.yml', 'raw_ssh_args': [ diff --git a/molecule/driver/docker.py b/molecule/driver/docker.py index 744ca9ab2..0e3e111da 100644 --- a/molecule/driver/docker.py +++ b/molecule/driver/docker.py @@ -18,8 +18,6 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -import collections - from molecule.driver import base @@ -45,26 +43,17 @@ def __init__(self, config): @property def testinfra_options(self): """ - Returns Testinfra specific options dict. + Returns a Testinfra specific options dict. :returns: dict """ return {'connection': 'docker'} @property - def inventory(self): - # TODO: This should belong in provisioner. + def connection_options(self): """ - Construct a dict of hosts/connection options and returns a dict. + Returns a driver specific connection options dict. - :returns: dict + :returns: str """ - # instance-1-default ansible_connection=docker - # instance-2-default ansible_connection=docker - host_options = 'ansible_connection=docker' - dd = collections.defaultdict(list) - for d in self._config.platforms: - name = '{}-{}'.format(d['name'], self._config.scenario.name) - dd[name].append(host_options) - - return dict(dd) + return {'ansible_connection': 'docker'} diff --git a/molecule/provisioner.py b/molecule/provisioner.py index 3086724ec..c4bb28704 100644 --- a/molecule/provisioner.py +++ b/molecule/provisioner.py @@ -18,8 +18,11 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. +import collections import os +import yaml +import yaml.representer import jinja2 from molecule import ansible_playbook @@ -79,12 +82,30 @@ def options(self): @property def inventory(self): - return self._config.driver.inventory + # ungrouped: + # hosts: + # instance-1-default: + # instance-2-default: + # $group_name: + # hosts: + # instance-1-default: + # ansible_connection: docker + # instance-2-default: + # ansible_connection: docker + dd = vivify() + for platform in self._config.platforms: + for group in platform.get('groups', ['ungrouped']): + name = '{}-{}'.format(platform['name'], + self._config.scenario.name) + connection_options = self._config.driver.connection_options + dd[group]['hosts'][name] = connection_options + + return dd @property def inventory_file(self): return os.path.join(self._config.ephemeral_directory, - 'ansible_inventory') + 'ansible_inventory.yml') @property def config_file(self): @@ -106,15 +127,12 @@ def write_inventory(self): :return: None """ - self._verify_inventory() + yaml.add_representer(collections.defaultdict, + yaml.representer.Representer.represent_dict) - template = jinja2.Environment().from_string( - self._get_inventory_template()).render( - host_data=self.inventory, - groups_data=self._config.platform_groups) with open(self.inventory_file, 'w') as f: - f.write(template) + f.write(yaml.dump(self.inventory)) def write_config(self): """ @@ -152,25 +170,6 @@ def _verify_inventory(self): util.print_error(msg) util.sysexit() - def _get_inventory_template(self): - """ - Returns an inventory template string. - - :return: str - """ - return """ -# Molecule managed - -{% for k, v in host_data.iteritems() -%} -{{ k }} {{ v|join(' ') }} -{% endfor -%} - -{% for k, v in groups_data.iteritems() %} -[{{ k }}] -{{ v|join('\n') }} -{% endfor -%} -""".strip() - def _get_config_template(self): """ Returns a config template string. @@ -186,3 +185,7 @@ def _get_config_template(self): ansible_managed = Ansible managed: Do NOT edit this file manually! retry_files_enabled = False """ + + +def vivify(): + return collections.defaultdict(vivify) diff --git a/test/unit/driver/test_docker.py b/test/unit/driver/test_docker.py index ed16ac982..9bbeb287b 100644 --- a/test/unit/driver/test_docker.py +++ b/test/unit/driver/test_docker.py @@ -45,18 +45,15 @@ def test_testinfra_options_property(docker_instance): assert {'connection': 'docker'} == docker_instance.testinfra_options +def test_connection_options_property(docker_instance): + x = {'ansible_connection': 'docker'} + + assert x == docker_instance.connection_options + + def test_name_property(docker_instance): assert 'docker' == docker_instance.name def test_options_property(docker_instance): assert {} == docker_instance.options - - -def test_inventory_property(docker_instance): - x = { - 'instance-1-default': ['ansible_connection=docker'], - 'instance-2-default': ['ansible_connection=docker'] - } - - assert x == docker_instance.inventory diff --git a/test/unit/test_config.py b/test/unit/test_config.py index 3b165b1a4..18872e6f0 100644 --- a/test/unit/test_config.py +++ b/test/unit/test_config.py @@ -91,51 +91,6 @@ def test_platforms_property(config_instance): assert x == config_instance.platforms -def test_platform_groups_property(config_instance): - x = { - 'bar': ['instance-1-default'], - 'foo': ['instance-1-default', 'instance-2-default'], - 'baz': ['instance-2-default'] - } - - assert x == config_instance.platform_groups - - -@pytest.fixture -def platforms_data_incomplete_groups(): - return { - 'platforms': [{ - 'name': 'instance-1', - 'groups': ['foo', 'bar'], - }, { - 'name': 'instance-2', - }] - } - - -def test_platform_groups_property_handles_missing_group( - platforms_data_incomplete_groups, molecule_file, config_data): - configs = [platforms_data_incomplete_groups, config_data] - c = config.Config(molecule_file, configs=configs) - - x = {'foo': ['instance-1-default'], 'bar': ['instance-1-default']} - - assert x == c.platform_groups - - -@pytest.fixture -def platforms_data_no_groups(): - return {'platforms': [{'name': 'instance-1', }, {'name': 'instance-2', }]} - - -def test_platform_groups_property_handles_no_groups( - platforms_data_no_groups, molecule_file, config_data): - configs = [platforms_data_no_groups, config_data] - c = config.Config(molecule_file, configs=configs) - - assert {} == c.platform_groups - - def test_provisioner_property(config_instance): assert isinstance(config_instance.provisioner, provisioner.Ansible) diff --git a/test/unit/test_provisioner.py b/test/unit/test_provisioner.py index c105ff3c1..dbc214d5a 100644 --- a/test/unit/test_provisioner.py +++ b/test/unit/test_provisioner.py @@ -19,9 +19,9 @@ # DEALINGS IN THE SOFTWARE. import os -import re import pytest +import yaml from molecule import provisioner from molecule import config @@ -59,7 +59,7 @@ def test_options_property(provisioner_instance): 'config_file': 'ansible.cfg', 'diff': True, 'host_key_checking': False, - 'inventory_file': 'ansible_inventory', + 'inventory_file': 'ansible_inventory.yml', 'limit': 'all', 'playbook': 'playbook.yml', 'raw_ssh_args': [ @@ -91,8 +91,51 @@ def test_options_property_handles_cli_args( def test_inventory_property(provisioner_instance): x = { - 'instance-1-default': ['ansible_connection=docker'], - 'instance-2-default': ['ansible_connection=docker'] + 'bar': { + 'hosts': { + 'instance-1-default': { + 'ansible_connection': 'docker' + } + } + }, + 'foo': { + 'hosts': { + 'instance-1-default': { + 'ansible_connection': 'docker' + }, + 'instance-2-default': { + 'ansible_connection': 'docker' + } + } + }, + 'baz': { + 'hosts': { + 'instance-2-default': { + 'ansible_connection': 'docker' + } + } + } + } + + assert x == provisioner_instance.inventory + + +def test_inventory_property_handles_missing_groups(temp_dir, + provisioner_instance): + platforms = [{'name': 'instance-1'}, {'name': 'instance-2'}] + provisioner_instance._config.config['platforms'] = platforms + + x = { + 'ungrouped': { + 'hosts': { + 'instance-1-default': { + 'ansible_connection': 'docker' + }, + 'instance-2-default': { + 'ansible_connection': 'docker' + } + } + } } assert x == provisioner_instance.inventory @@ -100,7 +143,7 @@ def test_inventory_property(provisioner_instance): def test_inventory_file_property(provisioner_instance): x = os.path.join(provisioner_instance._config.ephemeral_directory, - 'ansible_inventory') + 'ansible_inventory.yml') assert x == provisioner_instance.inventory_file @@ -137,39 +180,36 @@ def test_write_inventory(temp_dir, provisioner_instance): assert os.path.exists(provisioner_instance.inventory_file) - content = open(provisioner_instance.inventory_file, 'r').read() - assert re.search(r'# Molecule managed', content) - assert re.search(r'instance-1-default ansible_connection=docker', content) - assert re.search(r'instance-2-default ansible_connection=docker', content) - - assert re.search(r'\[bar\].*?instance-1-default.*?(\[\w+])?', content, - re.DOTALL) - assert re.search( - r'\[foo\].*?instance-1-default.*?instance-2-default.*?(\[\w+])?', - content, re.DOTALL) - assert re.search(r'\[baz\].*?instance-2-default.*?(\[\w+])?', content, - re.DOTALL) - - -def test_write_inventory_handles_missing_groups(temp_dir, - provisioner_instance): - platforms = [{'name': 'instance-1'}, {'name': 'instance-2'}] - provisioner_instance._config.config['platforms'] = platforms - provisioner_instance.write_inventory() - - assert os.path.exists(provisioner_instance.inventory_file) - - -def test_write_inventory_prints_error_when_missing_hosts( - temp_dir, patched_print_error, provisioner_instance): - provisioner_instance._config.config['platforms'] = [] - with pytest.raises(SystemExit) as e: - provisioner_instance.write_inventory() - - assert 1 == e.value.code - - msg = "Instances missing from the 'platform' section of molecule.yml." - patched_print_error.assert_called_once_with(msg) + with open(provisioner_instance.inventory_file, 'r') as stream: + data = yaml.load(stream) + x = { + 'bar': { + 'hosts': { + 'instance-1-default': { + 'ansible_connection': 'docker' + } + } + }, + 'foo': { + 'hosts': { + 'instance-1-default': { + 'ansible_connection': 'docker' + }, + 'instance-2-default': { + 'ansible_connection': 'docker' + } + } + }, + 'baz': { + 'hosts': { + 'instance-2-default': { + 'ansible_connection': 'docker' + } + } + } + } + + assert x == data def test_write_config(temp_dir, provisioner_instance): @@ -189,3 +229,26 @@ def test_setup(mocker, temp_dir, provisioner_instance): patched_provisioner_write_inventory.assert_called_once_with() patched_provisioner_write_config.assert_called_once_with() + + +def test_verify_inventory(provisioner_instance): + provisioner_instance._verify_inventory() + + +def test_verify_inventory_raises_when_missing_hosts( + temp_dir, patched_print_error, provisioner_instance): + provisioner_instance._config.config['platforms'] = [] + with pytest.raises(SystemExit) as e: + provisioner_instance._verify_inventory() + + assert 1 == e.value.code + + msg = "Instances missing from the 'platform' section of molecule.yml." + patched_print_error.assert_called_once_with(msg) + + +def test_vivify(): + d = provisioner.vivify() + d['bar']['baz'] = 'qux' + + assert 'qux' == str(d['bar']['baz'])