diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 05d16f3..b2c748f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,16 +1,10 @@ --- +default_language_version: + python: python3 +minimum_pre_commit_version: "1.14.0" repos: - - repo: https://github.com/PyCQA/doc8.git - rev: 0.8.1rc1 - hooks: - - id: doc8 - - repo: https://github.com/python/black.git - rev: 19.3b0 - hooks: - - id: black - language_version: python3 - - repo: https://github.com/pre-commit/pre-commit-hooks.git - rev: v2.2.3 + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.5.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace @@ -19,21 +13,27 @@ repos: - id: check-executables-have-shebangs - id: check-merge-conflict - id: debug-statements + - id: check-yaml + files: .*\.(yaml|yml)$ + # https://github.com/pre-commit/pre-commit-hooks/issues/273 + args: ["--unsafe"] + - repo: https://github.com/PyCQA/doc8.git + rev: 0.8.1rc3 + hooks: + - id: doc8 + - repo: https://github.com/python/black.git + rev: 19.10b0 + hooks: + - id: black + language_version: python3 - repo: https://gitlab.com/pycqa/flake8.git - rev: 3.7.8 + rev: 3.7.9 hooks: - id: flake8 additional_dependencies: - flake8-black - - repo: https://github.com/adrienverge/yamllint.git - rev: v1.16.0 - hooks: - - id: yamllint - files: \.(yaml|yml)$ - types: [file, yaml] - entry: yamllint --strict - repo: https://github.com/codespell-project/codespell.git - rev: v1.15.0 + rev: v1.16.0 hooks: - id: codespell name: codespell @@ -44,3 +44,36 @@ repos: args: [] require_serial: false additional_dependencies: [] + - repo: https://github.com/PyCQA/flake8.git + rev: 3.7.9 + hooks: + - id: flake8 + - repo: https://github.com/adrienverge/yamllint.git + rev: v1.20.0 + hooks: + - id: yamllint + files: \.(yaml|yml)$ + types: [file, yaml] + entry: yamllint --strict -f parsable + - repo: https://github.com/openstack-dev/bashate.git + rev: 1.0.0 + hooks: + - id: bashate + entry: bashate --error . --ignore=E006,E040 + # Run bashate check for all bash scripts + # Ignores the following rules: + # E006: Line longer than 79 columns (as many scripts use jinja + # templating, this is very difficult) + # E040: Syntax error determined using `bash -n` (as many scripts + # use jinja templating, this will often fail and the syntax + # error will be discovered in execution anyway) + - repo: https://github.com/ansible/ansible-lint.git + rev: v4.2.0 + hooks: + - id: ansible-lint + always_run: true + pass_filenames: false + # do not add file filters here as ansible-lint does not give reliable + # results when called with individual files. + # https://github.com/ansible/ansible-lint/issues/611 + entry: env ANSIBLE_LIBRARY=molecule_vagrant/modules ansible-lint --force-color -p -v diff --git a/README.rst b/README.rst index 8f128d4..7c9e38b 100644 --- a/README.rst +++ b/README.rst @@ -6,18 +6,18 @@ Molecule Vagrant Plugin :target: https://badge.fury.io/py/molecule-vagrant :alt: PyPI Package -.. image:: https://img.shields.io/travis/com/pycontribs/molecule-vagrant/master.svg?label=Linux%20builds%20%40%20Travis%20CI - :target: https://travis-ci.com/pycontribs/molecule-vagrant +.. image:: https://zuul-ci.org/gated.svg + :target: https://dashboard.zuul.ansible.com/t/ansible/builds?project=ansible-community/molecule-vagrant .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/python/black :alt: Python Black Code Style -.. image:: https://img.shields.io/badge/Code%20of%20Conduct-Ansible-silver.svg +.. image:: https://img.shields.io/badge/Code%20of%20Conduct-silver.svg :target: https://docs.ansible.com/ansible/latest/community/code_of_conduct.html :alt: Ansible Code of Conduct -.. image:: https://img.shields.io/badge/Mailing%20lists-Ansible-orange.svg +.. image:: https://img.shields.io/badge/Mailing%20lists-silver.svg :target: https://docs.ansible.com/ansible/latest/community/communication.html#mailing-list-information :alt: Ansible mailing lists diff --git a/bindep.txt b/bindep.txt new file mode 100644 index 0000000..38c03e1 --- /dev/null +++ b/bindep.txt @@ -0,0 +1,33 @@ +# This is a cross-platform list tracking distribution packages needed by tests; +# see https://docs.openstack.org/infra/bindep/ for additional information. + +build-dep [platform:dpkg] +dnsmasq-base [platform:dpkg] +ebtables [platform:dpkg] +gcc [test platform:rpm] +gcc-c++ [test platform:rpm] +libselinux-python [platform:centos-7] +libvirt [platform:rpm] +libvirt-clients [platform:dpkg] +libvirt-daemon [platform:dpkg] +libvirt-daemon-kvm [platform:rpm] +libvirt-daemon-system [platform:dpkg] +libvirt-dev [platform:dpkg] +libvirt-devel [platform:rpm] +libxml2-dev [platform:dpkg] +libxslt-dev [platform:dpkg] +make [platform:centos-7] +pkg-config [platform:dpkg] +python3 [test platform:rpm !platform:centos-7] +python3-devel [test platform:rpm !platform:centos-7] +python3-libselinux [test platform:rpm !platform:centos-7] +python3-libvirt [test platform:rpm !platform:centos-7 platform:dpkg] +python3-netifaces [test !platform:centos-7 platform:rpm] +python36 [test !platform:centos-7 !platform:fedora-28] +qemu [platform:dpkg] +qemu-kvm [platform:rpm] +ruby-dev [platform:dpkg] +ruby-devel [platform:rpm] +ruby-libvirt [platform:dpkg] +vagrant [platform:dpkg] +zlib1g-dev [platform:dpkg] diff --git a/molecule b/molecule new file mode 120000 index 0000000..86e8681 --- /dev/null +++ b/molecule @@ -0,0 +1 @@ +../molecule-vagrant/molecule_vagrant/test/scenarios/molecule \ No newline at end of file diff --git a/molecule_vagrant/__init__.py b/molecule_vagrant/__init__.py index e69de29..0af417a 100644 --- a/molecule_vagrant/__init__.py +++ b/molecule_vagrant/__init__.py @@ -0,0 +1,3 @@ +"""Plugin exports.""" + +__name__ = __name__.split("_")[-1] diff --git a/molecule_vagrant/cookiecutter/cookiecutter.json b/molecule_vagrant/cookiecutter/cookiecutter.json new file mode 100644 index 0000000..2ec6fb2 --- /dev/null +++ b/molecule_vagrant/cookiecutter/cookiecutter.json @@ -0,0 +1,5 @@ +{ + "molecule_directory": "molecule", + "role_name": "OVERRIDDEN", + "scenario_name": "OVERRIDDEN" +} diff --git a/molecule_vagrant/cookiecutter/scenario/driver/vagrant/cookiecutter.json b/molecule_vagrant/cookiecutter/scenario/driver/vagrant/cookiecutter.json deleted file mode 100644 index 0e88865..0000000 --- a/molecule_vagrant/cookiecutter/scenario/driver/vagrant/cookiecutter.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "molecule_directory": "molecule", - "role_name": "OVERRIDEN", - "scenario_name": "OVERRIDEN" -} diff --git a/molecule_vagrant/cookiecutter/scenario/driver/vagrant/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/prepare.yml b/molecule_vagrant/cookiecutter/scenario/driver/vagrant/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/prepare.yml deleted file mode 100644 index f6eb955..0000000 --- a/molecule_vagrant/cookiecutter/scenario/driver/vagrant/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/prepare.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -{% raw -%} -- name: Prepare - hosts: all - gather_facts: false - tasks: - - name: Install python for Ansible - raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal) - become: true - changed_when: false -{%- endraw %} diff --git a/molecule_vagrant/cookiecutter/scenario/driver/vagrant/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/INSTALL.rst b/molecule_vagrant/cookiecutter/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/INSTALL.rst similarity index 84% rename from molecule_vagrant/cookiecutter/scenario/driver/vagrant/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/INSTALL.rst rename to molecule_vagrant/cookiecutter/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/INSTALL.rst index 4f44b67..0c4bf5c 100644 --- a/molecule_vagrant/cookiecutter/scenario/driver/vagrant/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/INSTALL.rst +++ b/molecule_vagrant/cookiecutter/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/INSTALL.rst @@ -1,6 +1,6 @@ -******* +********************************* Vagrant driver installation guide -******* +********************************* Requirements ============ @@ -20,4 +20,4 @@ widely recommended `'--user' flag`_ when invoking ``pip``. .. code-block:: bash - $ pip install 'molecule[vagrant]' + $ pip install 'molecule_vagrant' diff --git a/molecule_vagrant/cookiecutter/scenario/driver/vagrant/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/playbook.yml b/molecule_vagrant/cookiecutter/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/converge.yml similarity index 100% rename from molecule_vagrant/cookiecutter/scenario/driver/vagrant/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/playbook.yml rename to molecule_vagrant/cookiecutter/{{cookiecutter.molecule_directory}}/{{cookiecutter.scenario_name}}/converge.yml diff --git a/molecule_vagrant/driver.py b/molecule_vagrant/driver.py index 6ac4688..bf77525 100644 --- a/molecule_vagrant/driver.py +++ b/molecule_vagrant/driver.py @@ -38,7 +38,7 @@ class Vagrant(Driver): .. important:: - This driver is alpha quality software. Do not perform any additonal + This driver is alpha quality software. Do not perform any additional tasks inside the ``create`` playbook. Molecule does not know about the Vagrant instances' configuration until the ``converge`` playbook is executed. @@ -86,7 +86,7 @@ class Vagrant(Driver): .. code-block:: bash - $ pip install molecule[vagrant] + $ pip install molecule-vagrant Change the provider passed to Vagrant. @@ -126,7 +126,7 @@ class Vagrant(Driver): def __init__(self, config=None): super(Vagrant, self).__init__(config) - self._name = 'vagrant' + self._name = "vagrant" @property def name(self): @@ -139,20 +139,20 @@ def name(self, value): @property def testinfra_options(self): return { - 'connection': 'ansible', - 'ansible-inventory': self._config.provisioner.inventory_file, + "connection": "ansible", + "ansible-inventory": self._config.provisioner.inventory_file, } @property def login_cmd_template(self): - connection_options = ' '.join(self.ssh_connection_options) + connection_options = " ".join(self.ssh_connection_options) return ( - 'ssh {{address}} ' - '-l {{user}} ' - '-p {{port}} ' - '-i {{identity_file}} ' - '{}' + "ssh {{address}} " + "-l {{user}} " + "-p {{port}} " + "-i {{identity_file}} " + "{}" ).format(connection_options) @property @@ -161,9 +161,9 @@ def default_safe_files(self): self.vagrantfile, self.vagrantfile_config, self.instance_config, - os.path.join(self._config.scenario.ephemeral_directory, '.vagrant'), - os.path.join(self._config.scenario.ephemeral_directory, 'vagrant-*.out'), - os.path.join(self._config.scenario.ephemeral_directory, 'vagrant-*.err'), + os.path.join(self._config.scenario.ephemeral_directory, ".vagrant"), + os.path.join(self._config.scenario.ephemeral_directory, "vagrant-*.out"), + os.path.join(self._config.scenario.ephemeral_directory, "vagrant-*.err"), ] @property @@ -171,7 +171,7 @@ def default_ssh_connection_options(self): return self._get_ssh_connection_options() def login_options(self, instance_name): - d = {'instance': instance_name} + d = {"instance": instance_name} return util.merge_dicts(d, self._get_instance_config(instance_name)) @@ -180,12 +180,12 @@ def ansible_connection_options(self, instance_name): d = self._get_instance_config(instance_name) return { - 'ansible_user': d['user'], - 'ansible_host': d['address'], - 'ansible_port': d['port'], - 'ansible_private_key_file': d['identity_file'], - 'connection': 'ssh', - 'ansible_ssh_common_args': ' '.join(self.ssh_connection_options), + "ansible_user": d["user"], + "ansible_host": d["address"], + "ansible_port": d["port"], + "ansible_private_key_file": d["identity_file"], + "connection": "ssh", + "ansible_ssh_common_args": " ".join(self.ssh_connection_options), } except StopIteration: return {} @@ -196,17 +196,17 @@ def ansible_connection_options(self, instance_name): @property def vagrantfile(self): - return os.path.join(self._config.scenario.ephemeral_directory, 'Vagrantfile') + return os.path.join(self._config.scenario.ephemeral_directory, "Vagrantfile") @property def vagrantfile_config(self): - return os.path.join(self._config.scenario.ephemeral_directory, 'vagrant.yml') + return os.path.join(self._config.scenario.ephemeral_directory, "vagrant.yml") def _get_instance_config(self, instance_name): instance_config_dict = util.safe_load_file(self._config.driver.instance_config) return next( - item for item in instance_config_dict if item['instance'] == instance_name + item for item in instance_config_dict if item["instance"] == instance_name ) def sanity_checks(self): @@ -217,6 +217,7 @@ def template_dir(self): """ Return path to its own cookiecutterm templates. It is used by init command in order to figure out where to load the templates from. """ - return os.path.join( - os.path.dirname(__file__), "cookiecutter/scenario/driver/vagrant" - ) + return os.path.join(os.path.dirname(__file__), "cookiecutter") + + def modules_dir(self): + return os.path.join(os.path.dirname(__file__), "modules") diff --git a/molecule_vagrant/modules/__init__.py b/molecule_vagrant/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/molecule_vagrant/modules/vagrant.py b/molecule_vagrant/modules/vagrant.py new file mode 100644 index 0000000..f8bb3c2 --- /dev/null +++ b/molecule_vagrant/modules/vagrant.py @@ -0,0 +1,626 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2015-2018 Cisco Systems, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from ansible.module_utils.basic import AnsibleModule +import contextlib +import datetime +import io +import os +import subprocess +import sys + +import molecule +import molecule.config +import molecule.util + +try: + import vagrant +except ImportError: + sys.exit("ERROR: Driver missing, install python-vagrant.") +ANSIBLE_METADATA = { + "metadata_version": "0.1", + "status": ["preview"], + "supported_by": "community", +} + +DOCUMENTATION = """ +--- +module: vagrant +short_description: Manage Vagrant instances +description: + - Manage the life cycle of Vagrant instances. + - Supports check mode. Run with --check and --diff to view config difference, + and list of actions to be taken. +version_added: 2.0 +author: + - Cisco Systems, Inc. +options: + instance_name: + description: + - Assign a name to a new instance or match an existing instance. + required: True + default: None + instance_interfaces: + description: + - Assign interfaces to a new instance. + required: False + default: [] + instance_raw_config_args: + description: + - Additional Vagrant options not explcitly exposed by this module. + required: False + default: None + config_options: + description: + - Additional config options not explcitly exposed by this module. + required: False + default: {} + platform_box: + description: + - Name of Vagrant box. + required: True + default: None + platform_box_version: + description: + - Explicit version of Vagrant box to use. + required: False + default: None + platform_box_url: + description: + - The URL to a Vagrant box. + required: False + default: None + provider_name: + description: + - Name of the Vagrant provider to use. + required: False + default: virtualbox + provider_memory: + description: + - Amount of memory to allocate to the instance. + required: False + default: 512 + provider_cpus: + description: + - Number of CPUs to allocate to the instance. + required: False + default: 2 + provider_options: + description: + - Additional provider options not explcitly exposed by this module. + required: False + default: {} + provider_override_args: + description: + - Additional override options not explcitly exposed by this module. + required: False + default: None + provider_raw_config_args: + description: + - Additional Vagrant options not explcitly exposed by this module. + required: False + default: None + force_stop: + description: + - Force halt the instance, then destroy the instance. + required: False + default: False + state: + description: + - The desired state of the instance. + required: True + choices: ['up', 'halt', 'destroy'] + default: None +requirements: + - python >= 2.6 + - python-vagrant + - vagrant +""" + +EXAMPLES = """ +See doc/source/configuration.rst +""" + +VAGRANTFILE_TEMPLATE = """ +require 'yaml' + +Vagrant.configure('2') do |config| + vagrant_config_hash = YAML::load_file('{{ vagrantfile_config }}') + + if Vagrant.has_plugin?('vagrant-cachier') + config.cache.scope = 'machine' + end + + ## + # Configs + ## + + c = vagrant_config_hash['config'] + if !c['options']['synced_folder'] + config.vm.synced_folder ".", "/vagrant", disabled: true + end + c['options'].delete('synced_folder') + + c['options'].each { |key, value| + eval("config.#{key} = #{value}") + } + + ## + # Platforms + ## + + platform = vagrant_config_hash['platform'] + + config.vm.box = platform['box'] + + if platform['box_version'] + config.vm.box_version = platform['box_version'] + end + + if platform['box_url'] + config.vm.box_url = platform['box_url'] + end + + ## + # Provider + ## + + provider = vagrant_config_hash['provider'] + provider_memory = provider['options']['memory'] + provider_cpus = provider['options']['cpus'] + provider['options'].delete('memory') + provider['options'].delete('cpus') + + ## + # Virtualbox + ## + + if provider['name'] == 'virtualbox' + config.vm.provider provider['name'] do |virtualbox, override| + virtualbox.memory = provider_memory + virtualbox.cpus = provider_cpus + + if provider['options']['linked_clone'] + if Gem::Version.new(Vagrant::VERSION) >= Gem::Version.new('1.8.0') + virtualbox.linked_clone = provider['options']['linked_clone'] + end + else + if Gem::Version.new(Vagrant::VERSION) >= Gem::Version.new('1.8.0') + virtualbox.linked_clone = true + end + end + + # Custom + provider['options'].each { |key, value| + if key != 'linked_clone' + eval("virtualbox.#{key} = #{value}") + end + } + + # Raw Configuration + if provider['raw_config_args'] + provider['raw_config_args'].each { |raw_config_arg| + eval("virtualbox.#{raw_config_arg}") + } + end + + if provider['override_args'] + provider['override_args'].each { |override_arg| + eval("override.#{override_arg}") + } + end + end + + # The vagrant-vbguest plugin attempts to update packages + # before a RHEL based VM is registered. + # TODO: Port from the old .j2, should be done in raw config + if (vagrant_config_hash['platform'] =~ /rhel/i) != nil + if Vagrant.has_plugin?('vagrant-vbguest') + config.vbguest.auto_update = false + end + end + end + + ## + # VMware (vmware_fusion, vmware_workstation and vmware_desktop) + ## + + if provider['name'].start_with?('vmware_') + config.vm.provider provider['name'] do |vmware, override| + vmware.vmx['memsize'] = provider_memory + vmware.vmx['numvcpus'] = provider_cpus + + # Custom + provider['options'].each { |key, value| + eval("vmware.#{key} = #{value}") + } + + # Raw Configuration + if provider['raw_config_args'] + provider['raw_config_args'].each { |raw_config_arg| + eval("vmware.#{raw_config_arg}") + } + end + + if provider['override_args'] + provider['override_args'].each { |override_arg| + eval("override.#{override_arg}") + } + end + end + end + + ## + # Parallels + ## + + if provider['name'] == 'parallels' + config.vm.provider provider['name'] do |parallels, override| + parallels.memory = provider_memory + parallels.cpus = provider_cpus + + # Custom + provider['options'].each { |key, value| + eval("parallels.#{key} = #{value}") + } + + # Raw Configuration + if provider['raw_config_args'] + provider['raw_config_args'].each { |raw_config_arg| + eval("parallels.#{raw_config_arg}") + } + end + + if provider['override_args'] + provider['override_args'].each { |override_arg| + eval("override.#{override_arg}") + } + end + end + end + + ## + # Libvirt + ## + + if provider['name'] == 'libvirt' + config.vm.provider provider['name'] do |libvirt, override| + libvirt.memory = provider_memory + libvirt.cpus = provider_cpus + + # Custom + provider['options'].each { |key, value| + eval("libvirt.#{key} = #{value}") + } + + # Raw Configuration + if provider['raw_config_args'] + provider['raw_config_args'].each { |raw_config_arg| + eval("libvirt.#{raw_config_arg}") + } + end + + if provider['override_args'] + provider['override_args'].each { |override_arg| + eval("override.#{override_arg}") + } + end + end + end + + + ## + # Instances + ## + + if vagrant_config_hash['instance'] + instance = vagrant_config_hash['instance'] + config.vm.define instance['name'] do |c| + c.vm.hostname = instance['name'] + + if instance['interfaces'] + instance['interfaces'].each { |interface| + c.vm.network "#{interface['network_name']}", + Hash[interface.select{|k| k != 'network_name'}.map{|k,v| [k.to_sym, v]}] + } + end + + if instance['raw_config_args'] + instance['raw_config_args'].each { |raw_config_arg| + eval("c.#{raw_config_arg}") + } + end + end + end +end +""".strip() # noqa + +RETURN = r""" +rc: + description: The command return code (0 means success) + returned: always + type: int +cmd: + description: The command executed by the task + returned: always + type: str +stdout: + description: The command standard output + returned: changed + type: str +stderr: + description: Output on stderr + returned: changed + type: str +""" + + +class VagrantClient(object): + def __init__(self, module): + self._module = module + + self._config = self._get_config() + self._vagrantfile = self._config.driver.vagrantfile + self._vagrant = self._get_vagrant() + self._write_configs() + self._has_error = None + self._datetime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self.result = {} + + @contextlib.contextmanager + def stdout_cm(self): + """ Redirect the stdout to a log file. """ + with open(self._get_stdout_log(), "a+") as fh: + msg = "### {} ###\n".format(self._datetime) + fh.write(msg) + fh.flush() + + yield fh + + @contextlib.contextmanager + def stderr_cm(self): + """ Redirect the stderr to a log file. """ + with open(self._get_stderr_log(), "a+") as fh: + msg = "### {} ###\n".format(self._datetime) + fh.write(msg) + fh.flush() + + try: + yield fh + except subprocess.CalledProcessError as e: + self._has_error = True + # msg = "CMD: {} returned {}\n{}".format( + # e.cmd, e.returncode, e.output or "" + # ) + self.result["cmd"] = e.cmd + self.result["rc"] = e.returncode + self.result["stderr"] = e.output or "" + + # fh.write(msg) + # raise + except Exception as e: + self._has_error = True + if hasattr(e, "message"): + fh.write(e.message) + else: + fh.write(e) + fh.flush() + raise + + def up(self): + changed = False + if not self._created(): + changed = True + provision = self._module.params["provision"] + try: + self._vagrant.up(provision=provision) + except Exception: + # NOTE(retr0h): Ignore the exception since python-vagrant + # passes the actual error as a no-argument ContextManager. + pass + + # NOTE(retr0h): Ansible wants only one module return `fail_json` + # or `exit_json`. + if not self._has_error: + self._module.exit_json( + changed=changed, log=self._get_stdout_log(), **self._conf() + ) + else: + msg = "ERROR: See log file '{}'".format(self._get_stderr_log()) + with io.open(self._get_stderr_log(), "r", encoding="utf-8") as f: + self.result["stderr"] = f.read() + self._module.fail_json(msg=msg, **self.result) + + def destroy(self): + changed = False + if self._created(): + changed = True + if self._module.params["force_stop"]: + self._vagrant.halt(force=True) + self._vagrant.destroy() + + self._module.exit_json(changed=changed) + + def halt(self): + changed = False + if self._created(): + changed = True + self._vagrant.halt(force=self._module.params["force_stop"]) + + self._module.exit_json(changed=changed) + + def _conf(self): + instance_name = self._module.params["instance_name"] + + return self._vagrant.conf(vm_name=instance_name) + + def _status(self): + instance_name = self._module.params["instance_name"] + try: + s = self._vagrant.status(vm_name=instance_name)[0] + + return {"name": s.name, "state": s.state, "provider": s.provider} + except AttributeError: + pass + except subprocess.CalledProcessError: + pass + + def _created(self): + status = self._status() + if status and status["state"] == "running": + return status + return {} + + def _get_config(self): + molecule_file = os.environ["MOLECULE_FILE"] + + return molecule.config.Config(molecule_file) + + def _write_vagrantfile(self): + template = molecule.util.render_template( + VAGRANTFILE_TEMPLATE, + vagrantfile_config=self._config.driver.vagrantfile_config, + ) + molecule.util.write_file(self._vagrantfile, template) + + def _write_vagrantfile_config(self, data): + molecule.util.write_file( + self._config.driver.vagrantfile_config, molecule.util.safe_dump(data) + ) + + def _write_configs(self): + self._write_vagrantfile_config(self._get_vagrant_config_dict()) + self._write_vagrantfile() + + def _get_vagrant(self): + env = os.environ.copy() + env["VAGRANT_CWD"] = os.environ["MOLECULE_EPHEMERAL_DIRECTORY"] + v = vagrant.Vagrant(out_cm=self.stdout_cm, err_cm=self.stderr_cm, env=env) + + return v + + def _get_vagrant_config_dict(self): + d = { + "config": { + # NOTE(retr0h): Options provided here will be passed to + # Vagrant as "config.#{key} = #{value}". + "options": { + # NOTE(retr0h): `synced_folder` does not represent the + # actual key used by Vagrant. Is used as a flag to + # simply enable/disable shared folder. + "synced_folder": False, + "ssh.insert_key": True, + } + }, + "platform": { + "box": self._module.params["platform_box"], + "box_version": self._module.params["platform_box_version"], + "box_url": self._module.params["platform_box_url"], + }, + "instance": { + "name": self._module.params["instance_name"], + "interfaces": self._module.params["instance_interfaces"], + "raw_config_args": self._module.params["instance_raw_config_args"], + }, + "provider": { + "name": self._module.params["provider_name"], + # NOTE(retr0h): Options provided here will be passed to + # Vagrant as "$provider_name.#{key} = #{value}". + "options": { + "memory": self._module.params["provider_memory"], + "cpus": self._module.params["provider_cpus"], + }, + "raw_config_args": self._module.params["provider_raw_config_args"], + "override_args": self._module.params["provider_override_args"], + }, + } + + molecule.util.merge_dicts( + d["config"]["options"], self._module.params["config_options"] + ) + + molecule.util.merge_dicts( + d["provider"]["options"], self._module.params["provider_options"] + ) + + return d + + def _get_stdout_log(self): + return self._get_vagrant_log("out") + + def _get_stderr_log(self): + return self._get_vagrant_log("err") + + def _get_vagrant_log(self, __type): + instance_name = self._module.params["instance_name"] + + return os.path.join( + self._config.scenario.ephemeral_directory, + "vagrant-{}.{}".format(instance_name, __type), + ) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + instance_name=dict(type="str", required=True), + instance_interfaces=dict(type="list", default=[]), + instance_raw_config_args=dict(type="list", default=None), + config_options=dict(type="dict", default={}), + platform_box=dict(type="str", required=False), + platform_box_version=dict(type="str"), + platform_box_url=dict(type="str"), + provider_name=dict(type="str", default="virtualbox"), + provider_memory=dict(type="int", default=512), + provider_cpus=dict(type="int", default=2), + provider_options=dict(type="dict", default={}), + provider_override_args=dict(type="list", default=None), + provider_raw_config_args=dict(type="list", default=None), + provision=dict(type="bool", default=False), + force_stop=dict(type="bool", default=False), + state=dict(type="str", default="up", choices=["up", "destroy", "halt"]), + ), + supports_check_mode=False, + ) + + v = VagrantClient(module) + + if module.params["state"] == "up": + v.up() + + if module.params["state"] == "destroy": + v.destroy() + + if module.params["state"] == "halt": + v.halt() + + module.exit_json(**module.result) + + +if __name__ == "__main__": + main() diff --git a/molecule_vagrant/playbooks/create.yml b/molecule_vagrant/playbooks/create.yml new file mode 100644 index 0000000..bf7b7a0 --- /dev/null +++ b/molecule_vagrant/playbooks/create.yml @@ -0,0 +1,62 @@ +--- +- name: Create + hosts: localhost + connection: local + gather_facts: false + no_log: "{{ molecule_no_log }}" + tasks: + - name: Create molecule instance(s) + vagrant: + instance_name: "{{ item.name }}" + instance_interfaces: "{{ item.interfaces | default(omit) }}" + instance_raw_config_args: "{{ item.instance_raw_config_args | default(omit) }}" + + config_options: "{{ item.config_options | default(omit) }}" + + platform_box: "{{ item.box | default('generic/alpine310') }}" + platform_box_version: "{{ item.box_version | default(omit) }}" + platform_box_url: "{{ item.box_url | default(omit) }}" + + provider_name: "{{ molecule_yml.driver.provider.name }}" + provider_memory: "{{ item.memory | default(omit) }}" + provider_cpus: "{{ item.cpus | default(omit) }}" + provider_options: "{{ item.provider_options | default(omit) }}" + provider_raw_config_args: "{{ item.provider_raw_config_args | default(omit) }}" + provider_override_args: "{{ item.provider_override_args | default(omit) }}" + + provision: "{{ item.provision | default(omit) }}" + + state: up + register: server + with_items: "{{ molecule_yml.platforms }}" + loop_control: + label: "{{ item.name }}" + no_log: false + + # NOTE(retr0h): Vagrant/VBox sucks and parallelizing instance creation + # causes issues. + + # Mandatory configuration for Molecule to function. + + - when: server.changed | bool + block: + + - name: Populate instance config dict + set_fact: + instance_conf_dict: { + 'instance': "{{ item.Host }}", + 'address': "{{ item.HostName }}", + 'user': "{{ item.User }}", + 'port': "{{ item.Port }}", + 'identity_file': "{{ item.IdentityFile }}", } + with_items: "{{ server.results }}" + register: instance_config_dict + + - name: Convert instance config dict to a list + set_fact: + instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}" + + - name: Dump instance config + copy: + content: "{{ instance_conf | to_json | from_json | molecule_to_yaml | molecule_header }}" + dest: "{{ molecule_instance_config }}" diff --git a/molecule_vagrant/playbooks/destroy.yml b/molecule_vagrant/playbooks/destroy.yml new file mode 100644 index 0000000..3b7ffb2 --- /dev/null +++ b/molecule_vagrant/playbooks/destroy.yml @@ -0,0 +1,37 @@ +--- +- name: Destroy + hosts: localhost + connection: local + gather_facts: false + no_log: "{{ molecule_no_log }}" + tasks: + - name: Destroy molecule instance(s) + vagrant: + instance_name: "{{ item.name }}" + platform_box: "{{ item.box | default(omit) }}" + provider_name: "{{ molecule_yml.driver.provider.name }}" + provider_options: "{{ item.provider_options | default(omit) }}" + provider_raw_config_args: "{{ item.provider_raw_config_args | default(omit) }}" + force_stop: "{{ item.force_stop | default(true) }}" + + state: destroy + register: server + with_items: "{{ molecule_yml.platforms }}" + loop_control: + label: "{{ item.name }}" + no_log: false + + # NOTE(retr0h): Vagrant/VBox sucks and parallelizing instance deletion + # causes issues. + + # Mandatory configuration for Molecule to function. + + - name: Populate instance config + set_fact: + instance_conf: {} + + - name: Dump instance config # noqa 503 + copy: + content: "{{ instance_conf | to_json | from_json | molecule_to_yaml | molecule_header }}" + dest: "{{ molecule_instance_config }}" + when: server.changed | bool diff --git a/molecule_vagrant/playbooks/prepare.yml b/molecule_vagrant/playbooks/prepare.yml new file mode 100644 index 0000000..6aeb962 --- /dev/null +++ b/molecule_vagrant/playbooks/prepare.yml @@ -0,0 +1,15 @@ +--- +- name: Prepare + hosts: all + gather_facts: false + tasks: + - name: Bootstrap python for Ansible + raw: | + command -v python3 python || ( + command -v apk >/dev/null && sudo apk add --no-progress --update python3 || + (test -e /usr/bin/dnf && sudo dnf install -y python3) || + (test -e /usr/bin/apt && (apt -y update && apt install -y python-minimal)) || + echo "Warning: Python not boostrapped due to unknown platform." + ) + become: true + changed_when: false diff --git a/molecule_vagrant/test/functional/.ansible-lint b/molecule_vagrant/test/functional/.ansible-lint deleted file mode 100644 index c54b8ec..0000000 --- a/molecule_vagrant/test/functional/.ansible-lint +++ /dev/null @@ -1,9 +0,0 @@ -# ansible-lint config for functional testing, used to bypass expected metadata -# errors in molecule-generated roles. Loaded via the metadata_lint_update -# pytest helper. For reference, see "E7xx - metadata" in: -# https://docs.ansible.com/ansible-lint/rules/default_rules.html -skip_list: - # metadata/701 - Role info should contain platforms - - '701' - # metadata/703 - Should change default metadata: " - - '703' diff --git a/molecule_vagrant/test/functional/test_azure.py b/molecule_vagrant/test/functional/test_func.py similarity index 76% rename from molecule_vagrant/test/functional/test_azure.py rename to molecule_vagrant/test/functional/test_func.py index 8cc715e..41b15d8 100644 --- a/molecule_vagrant/test/functional/test_azure.py +++ b/molecule_vagrant/test/functional/test_func.py @@ -25,33 +25,27 @@ from molecule import logger from molecule.test.conftest import run_command, change_dir_to -from molecule.test.functional.conftest import metadata_lint_update # import change_dir_to, temp_dir LOG = logger.get_logger(__name__) -@pytest.mark.xfail(reason="need to fix template path") +# @pytest.mark.xfail(reason="need to fix template path") def test_command_init_scenario(temp_dir): role_directory = os.path.join(temp_dir.strpath, "test-init") - options = {"role_name": "test-init"} - cmd = sh.molecule.bake("init", "role", **options) + options = {} + cmd = sh.molecule.bake("init", "role", "test-init", **options) run_command(cmd) - metadata_lint_update(role_directory) with change_dir_to(role_directory): molecule_directory = pytest.helpers.molecule_directory() scenario_directory = os.path.join(molecule_directory, "test-scenario") - options = { - "scenario_name": "test-scenario", - "role_name": "test-init", - "driver-name": "vagrant", - } - cmd = sh.molecule.bake("init", "scenario", **options) + options = {"role_name": "test-init", "driver-name": "vagrant"} + cmd = sh.molecule.bake("init", "scenario", "test-scenario", **options) run_command(cmd) assert os.path.isdir(scenario_directory) - cmd = sh.molecule.bake("test", "-s", "test-scenario") + cmd = sh.molecule.bake("--debug", "test", "-s", "test-scenario") run_command(cmd) diff --git a/molecule_vagrant/test/scenarios/driver/vagrant/molecule/default/molecule.yml b/molecule_vagrant/test/scenarios/driver/vagrant/molecule/default/molecule.yml deleted file mode 100644 index a5e4e17..0000000 --- a/molecule_vagrant/test/scenarios/driver/vagrant/molecule/default/molecule.yml +++ /dev/null @@ -1,40 +0,0 @@ ---- -dependency: - name: galaxy -driver: - name: vagrant - provider: - name: virtualbox -lint: - name: yamllint - options: - config-file: ../../../resources/.yamllint -platforms: - - name: instance - box: debian/jessie64 - interfaces: - - auto_config: true - network_name: private_network - type: dhcp - - virtualbox__intnet: test_network - network_name: private_network - ip: 192.168.0.1 - provision: false -provisioner: - name: ansible - config_options: - defaults: - callback_whitelist: profile_roles,profile_tasks,timer - playbooks: - create: ../../../../../resources/playbooks/vagrant/create.yml - destroy: ../../../../../resources/playbooks/vagrant/destroy.yml - env: - ANSIBLE_ROLES_PATH: ../../../../../resources/roles/ - lint: - name: ansible-lint -scenario: - name: default -verifier: - name: testinfra - lint: - name: flake8 diff --git a/molecule_vagrant/test/scenarios/driver/vagrant/molecule/default/playbook.yml b/molecule_vagrant/test/scenarios/driver/vagrant/molecule/default/playbook.yml deleted file mode 100644 index 2d0e971..0000000 --- a/molecule_vagrant/test/scenarios/driver/vagrant/molecule/default/playbook.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -- name: Converge - hosts: all - gather_facts: false - become: true - roles: - - molecule diff --git a/molecule_vagrant/test/scenarios/driver/vagrant/molecule/default/tests/test_default.py b/molecule_vagrant/test/scenarios/driver/vagrant/molecule/default/tests/test_default.py deleted file mode 100644 index 0bcc018..0000000 --- a/molecule_vagrant/test/scenarios/driver/vagrant/molecule/default/tests/test_default.py +++ /dev/null @@ -1,40 +0,0 @@ -import os - -import testinfra.utils.ansible_runner - -testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( - os.environ['MOLECULE_INVENTORY_FILE'] -).get_hosts('all') - - -def test_hostname(host): - assert 'instance' == host.check_output('hostname -s') - - -def test_etc_molecule_directory(host): - f = host.file('/etc/molecule') - - assert f.is_directory - assert f.user == 'root' - assert f.group == 'root' - assert f.mode == 0o755 - - -def test_etc_molecule_ansible_hostname_file(host): - f = host.file('/etc/molecule/instance') - - assert f.is_file - assert f.user == 'root' - assert f.group == 'root' - assert f.mode == 0o644 - - -def test_hostonly_interface(host): - i = host.interface('eth1').addresses - - # NOTE(retr0h): Contains ipv4 and ipv6 addresses. - assert len(i) == 2 - - -def test_internal_interface(host): - assert '192.168.0.1' in host.interface('eth2').addresses diff --git a/molecule_vagrant/test/scenarios/driver/vagrant/molecule/multi-node/playbook.yml b/molecule_vagrant/test/scenarios/driver/vagrant/molecule/multi-node/playbook.yml deleted file mode 100644 index 8fe4807..0000000 --- a/molecule_vagrant/test/scenarios/driver/vagrant/molecule/multi-node/playbook.yml +++ /dev/null @@ -1,28 +0,0 @@ ---- -- name: Converge - hosts: all - gather_facts: false - become: true - roles: - - molecule - -- name: Converge - hosts: bar - gather_facts: false - become: true - roles: - - molecule - -- name: Converge - hosts: foo - gather_facts: false - become: true - roles: - - molecule - -- name: Converge - hosts: baz - gather_facts: false - become: true - roles: - - molecule diff --git a/molecule_vagrant/test/scenarios/driver/vagrant/molecule/multi-node/tests/test_default.py b/molecule_vagrant/test/scenarios/driver/vagrant/molecule/multi-node/tests/test_default.py deleted file mode 100644 index ad52555..0000000 --- a/molecule_vagrant/test/scenarios/driver/vagrant/molecule/multi-node/tests/test_default.py +++ /dev/null @@ -1,38 +0,0 @@ -import os -import re - -import testinfra.utils.ansible_runner - -testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( - os.environ['MOLECULE_INVENTORY_FILE'] -).get_hosts('all') - - -def test_hostname(host): - assert re.search(r'instance-[12]', host.check_output('hostname -s')) - - -def test_etc_molecule_directory(host): - f = host.file('/etc/molecule') - - assert f.is_directory - assert f.user == 'root' - assert f.group == 'root' - assert f.mode == 0o755 - - -def test_etc_molecule_ansible_hostname_file(host): - filename = '/etc/molecule/{}'.format(host.check_output('hostname -s')) - f = host.file(filename) - - assert f.is_file - assert f.user == 'root' - assert f.group == 'root' - assert f.mode == 0o644 - - -def test_hostonly_interface(host): - i = host.interface('eth1').addresses - - # NOTE(retr0h): Contains ipv4 and ipv6 addresses. - assert len(i) == 2 diff --git a/molecule_vagrant/test/scenarios/driver/vagrant/molecule/multi-node/tests/test_instance-1.py b/molecule_vagrant/test/scenarios/driver/vagrant/molecule/multi-node/tests/test_instance-1.py deleted file mode 100644 index 05b514f..0000000 --- a/molecule_vagrant/test/scenarios/driver/vagrant/molecule/multi-node/tests/test_instance-1.py +++ /dev/null @@ -1,35 +0,0 @@ -import os - -import testinfra.utils.ansible_runner - -testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( - os.environ['MOLECULE_INVENTORY_FILE'] -).get_hosts('instance-1') - - -def test_distro(host): - f = host.file('/etc/debian_version') - - assert f.is_file - - -def test_cpus(host): - cpus = host.ansible("setup")['ansible_facts']['ansible_processor_vcpus'] - - assert 1 == int(cpus) - - -def test_memory(host): - total_memory = host.ansible("setup")['ansible_facts']['ansible_memtotal_mb'] - - assert (1024 / 2) <= int(total_memory) <= 1024 - - -def test_has_shared_directory(host): - f = host.file('/vagrant') - - assert f.is_directory - - -def test_internal_interface(host): - assert '192.168.0.1' in host.interface('eth2').addresses diff --git a/molecule_vagrant/test/scenarios/driver/vagrant/molecule/multi-node/tests/test_instance-2.py b/molecule_vagrant/test/scenarios/driver/vagrant/molecule/multi-node/tests/test_instance-2.py deleted file mode 100644 index 968ad47..0000000 --- a/molecule_vagrant/test/scenarios/driver/vagrant/molecule/multi-node/tests/test_instance-2.py +++ /dev/null @@ -1,35 +0,0 @@ -import os - -import testinfra.utils.ansible_runner - -testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( - os.environ['MOLECULE_INVENTORY_FILE'] -).get_hosts('instance-2') - - -def test_distro(host): - f = host.file('/etc/redhat-release') - - assert f.is_file - - -def test_cpus(host): - cpus = host.ansible("setup")['ansible_facts']['ansible_processor_vcpus'] - - assert 2 == int(cpus) - - -def test_memory(host): - total_memory = host.ansible("setup")['ansible_facts']['ansible_memtotal_mb'] - - assert (1024 + 1024 / 2) <= int(total_memory) <= 2048 - - -def test_does_not_have_shared_directory(host): - f = host.file('/vagrant') - - assert not f.is_directory - - -def test_internal_interface(host): - assert '192.168.0.2' in host.interface('eth2').addresses diff --git a/molecule_vagrant/test/scenarios/molecule/default/converge.yml b/molecule_vagrant/test/scenarios/molecule/default/converge.yml new file mode 100644 index 0000000..cd8ecfb --- /dev/null +++ b/molecule_vagrant/test/scenarios/molecule/default/converge.yml @@ -0,0 +1,11 @@ +--- +- name: Converge + hosts: all + gather_facts: false + become: true + tasks: + - name: sample task # noqa 305 + shell: + cmd: uname + warn: false + changed_when: false diff --git a/molecule_vagrant/test/scenarios/molecule/default/molecule.yml b/molecule_vagrant/test/scenarios/molecule/default/molecule.yml new file mode 100644 index 0000000..3ccd98b --- /dev/null +++ b/molecule_vagrant/test/scenarios/molecule/default/molecule.yml @@ -0,0 +1,13 @@ +--- +dependency: + name: galaxy +driver: + name: vagrant + provider: + name: virtualbox +platforms: + - name: instance + box: generic/alpine310 + provision: false +provisioner: + name: ansible diff --git a/molecule_vagrant/test/scenarios/molecule/multi-node/converge.yml b/molecule_vagrant/test/scenarios/molecule/multi-node/converge.yml new file mode 100644 index 0000000..424c830 --- /dev/null +++ b/molecule_vagrant/test/scenarios/molecule/multi-node/converge.yml @@ -0,0 +1,44 @@ +--- +- name: Converge + hosts: all + gather_facts: false + become: true + tasks: + - name: sample task # noqa 305 + shell: + cmd: uname + warn: false + changed_when: false + +- name: Converge + hosts: bar + gather_facts: false + become: true + tasks: + - name: sample task # noqa 305 + shell: + cmd: uname + warn: false + changed_when: false + +- name: Converge + hosts: foo + gather_facts: false + become: true + tasks: + - name: sample task # noqa 305 + shell: + cmd: uname + warn: false + changed_when: false + +- name: Converge + hosts: baz + gather_facts: false + become: true + tasks: + - name: sample task # noqa 305 + shell: + cmd: uname + warn: false + changed_when: false diff --git a/molecule_vagrant/test/scenarios/driver/vagrant/molecule/multi-node/molecule.yml b/molecule_vagrant/test/scenarios/molecule/multi-node/molecule.yml similarity index 67% rename from molecule_vagrant/test/scenarios/driver/vagrant/molecule/multi-node/molecule.yml rename to molecule_vagrant/test/scenarios/molecule/multi-node/molecule.yml index dbb4172..6960963 100644 --- a/molecule_vagrant/test/scenarios/driver/vagrant/molecule/multi-node/molecule.yml +++ b/molecule_vagrant/test/scenarios/molecule/multi-node/molecule.yml @@ -5,10 +5,6 @@ driver: name: vagrant provider: name: virtualbox -lint: - name: yamllint - options: - config-file: ../../../resources/.yamllint platforms: - name: instance-1 box: debian/jessie64 @@ -45,18 +41,3 @@ provisioner: config_options: defaults: callback_whitelist: profile_roles,profile_tasks,timer - playbooks: - create: ../../../../../resources/playbooks/vagrant/create.yml - destroy: ../../../../../resources/playbooks/vagrant/destroy.yml - env: - ANSIBLE_ROLES_PATH: ../../../../../resources/roles/ - lint: - name: ansible-lint -scenario: - name: multi-node -verifier: - name: testinfra - options: - sudo: true - lint: - name: flake8 diff --git a/molecule_vagrant/test/scenarios/driver/vagrant/molecule/multi-node/prepare.yml b/molecule_vagrant/test/scenarios/molecule/multi-node/prepare.yml similarity index 100% rename from molecule_vagrant/test/scenarios/driver/vagrant/molecule/multi-node/prepare.yml rename to molecule_vagrant/test/scenarios/molecule/multi-node/prepare.yml diff --git a/pytest.ini b/pytest.ini index cea058e..7c8a58c 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,5 +1,5 @@ [pytest] -addopts = -v -rxXs --doctest-modules --durations 10 --cov=molecule_vagrant --cov-report term-missing:skip-covered --cov-report xml +addopts = -v -rxXs --doctest-modules --durations 10 --no-cov-on-fail --cov=molecule_vagrant --cov-report term-missing:skip-covered doctest_optionflags = ALLOW_UNICODE ELLIPSIS junit_suite_name = molecule_test_suite norecursedirs = dist doc build .tox .eggs test/scenarios test/resources diff --git a/setup.cfg b/setup.cfg index cd0e3b6..70f7186 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,12 +6,12 @@ universal = 1 [metadata] name = molecule-vagrant -url = https://github.com/pycontribs/molecule-vagrant +url = https://github.com/ansible-community/molecule-vagrant project_urls = - Bug Tracker = https://github.com/pycontribs/molecule-vagrant/issues - Release Management = https://github.com/pycontribs/molecule-vagrant/projects - CI: Travis = https://travis-ci.com/pycontribs/molecule-vagrant - Source Code = https://github.com/pycontribs/molecule-vagrant + Bug Tracker = https://github.com/ansible-community/molecule-vagrant/issues + Release Management = https://github.com/ansible-community/molecule-vagrant/releases + CI: Zuul = https://dashboard.zuul.ansible.com/t/ansible/builds?project=ansible-community/molecule-vagrant + Source Code = https://github.com/ansible-community/molecule-vagrant description = Vagrant Molecule Plugin :: run molecule tests using Vagrant long_description = file: README.rst long_description_content_type = text/x-rst @@ -63,24 +63,27 @@ setup_requires = # These are required in actual runtime: install_requires = - ansible - molecule >= 3.0a3 + ansible >= 2.9.5 + molecule >= 3.0.2 pyyaml >= 5.1, < 6 python-vagrant + selinux [options.extras_require] test = + ansi2html # soft-dependency of pytest-html + coverage>=4.4, < 5 # drop '<5' when we drop py27 as we cannot mix them flake8>=3.6.0, < 4 mock>=3.0.5, < 4 pytest>=4.6.3, < 5 pytest-cov>=2.7.1, < 3 pytest-helpers-namespace>=2019.1.8, < 2020 + pytest-html pytest-mock>=1.10.4, < 2 pytest-verbose-parametrize>=1.7.0, < 2 pytest-xdist>=1.29.0, < 2 pytest-dependency - shade>=1.31.0, < 2 [options.entry_points] molecule.driver = diff --git a/tools/Vagrantfile b/tools/Vagrantfile new file mode 100644 index 0000000..7c00f1a --- /dev/null +++ b/tools/Vagrantfile @@ -0,0 +1,3 @@ +Vagrant.configure("2") do |config| + config.vm.box = "generic/alpine310" +end diff --git a/tools/test-setup.sh b/tools/test-setup.sh new file mode 100755 index 0000000..0596231 --- /dev/null +++ b/tools/test-setup.sh @@ -0,0 +1,57 @@ +#!/bin/bash +set -euxo pipefail +# Used by Zuul CI to perform extra bootstrapping + +# Platforms coverage: +# Fedora 30 : has vagrant-libvirt no compilation needed +# CentOS 7 : install upstream vagrant rpm and compiles plugin (broken runtime) +# CentOS 8 : install upstream vagrant rpm and compiles plugin (broken runtime) + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +# Bumping system tox because version from CentOS 7 is too old +# We are not using pip --user due to few bugs in tox role which does not allow +# us to override how is called. Once these are addressed we will switch back +# non-sudo +command -v python3 python + +PYTHON=$(command -v python3 python|head -n1) +PKG_CMD=$(command -v dnf yum|head -n1) + +sudo $PYTHON -m pip install -U tox "zipp<0.6.0;python_version=='2.7'" + +# === LIBVIRT SETUP === +# https://bugs.launchpad.net/ubuntu/+source/libvirt/+bug/1588004 +sudo rm -f /etc/systemd/libvirtd.service /etc/systemd/system/multi-user.target.wants/libvirt-bin.service || true +sudo systemctl enable --now libvirtd +sudo usermod --append --groups libvirt "$(whoami)" + +# === VAGRANT SETUP === +# Install Vagrant using their questionable practices, see locked ticket: +# https://github.com/hashicorp/vagrant/issues/11070 + +which vagrant || \ + sudo $PKG_CMD install -y vagrant-libvirt || { + sudo $PKG_CMD install -y https://releases.hashicorp.com/vagrant/2.2.7/vagrant_2.2.7_x86_64.rpm + } + +vagrant plugin list | grep vagrant-libvirt || { + CONFIGURE_ARGS="with-libvirt-include=/usr/include/libvirt with-libvirt-lib=/usr/lib64" vagrant plugin install vagrant-libvirt +} + +rpm -qa | grep libselinux + +vagrant version +vagrant global-status + +vagrant plugin list | tee >(grep -q "No plugins installed." && { + echo "FATAL: Vagrant is not usable without any provider plugins." + exit 1 +}) + +# Used to test that Vagrant is usable and also to pre-download the image +# we will use during testing. +cd $DIR + +vagrant up --no-provision +vagrant destroy -f diff --git a/tox.ini b/tox.ini index 35c94ef..db62788 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ # For more information about tox, see https://tox.readthedocs.io/en/latest/ [tox] -minversion = 3.4.0 -envlist = lint,dist,py27,py35,py36,py37,py38,devel,distros +minversion = 3.9.0 +envlist = lint,packaging,py27,py35,py36,py37,py38,devel,distros skipsdist = True skip_missing_interpreters = True isolated_build = True @@ -30,10 +30,14 @@ setenv = # This should pass these args to molecule, no effect here as this is the default # but it validates that it accepts extra params. MOLECULE_OPTS=--destroy always + MOLECULE_NO_LOG="false" + _EXTRAS=-l --html={envlogdir}/reports.html --self-contained-html + PYTEST_ADDOPTS={env:_EXTRAS} {env:PYTEST_ADDOPTS:} passenv = CI CURL_CA_BUNDLE DOCKER_* + HOME PYTEST_OPTIONS REQUESTS_CA_BUNDLE SSH_AUTH_SOCK @@ -42,13 +46,14 @@ passenv = TRAVIS TRAVIS_* TWINE_* + VAGRANT_HOME whitelist_externals = bash twine pytest pre-commit -[testenv:dist] +[testenv:packaging] usedevelop = false skip_install = true deps = @@ -56,7 +61,7 @@ deps = pep517 >= 0.5.0 twine >= 2.0.0 commands = - bash -c "rm -rf {toxinidir}/dist/ && mkdir -p {toxinidir}/dist/" + bash -c "rm -rf {toxinidir}/dist/ {toxinidir}/build/ && mkdir -p {toxinidir}/dist/" python -m pep517.build \ --source \ --binary \ @@ -71,7 +76,7 @@ commands = {[testenv]commands} deps = git+https://github.com/ansible/ansible.git#egg=ansible - git+https://github.com/ansible/molecule#egg=molecule + git+https://github.com/ansible-community/molecule#egg=molecule [testenv:lint] description = Performs linting, style checks @@ -85,7 +90,7 @@ commands = description = Builds the packages and uploads them to https://pypi.org envdir={toxworkdir}/dist deps= - {[testenv:dist]deps} + {[testenv:packaging]deps} commands = - {[testenv:dist]commands} + {[testenv:packaging]commands} twine upload --verbose dist/* diff --git a/zuul.d/layout.yaml b/zuul.d/layout.yaml new file mode 100644 index 0000000..644e19b --- /dev/null +++ b/zuul.d/layout.yaml @@ -0,0 +1,47 @@ +--- +# zuul.d/layout.yaml +- job: + name: molecule-tox-devel-centos-8 + parent: molecule-tox-py36 + vars: + tox_envlist: devel + attempts: 1 + +- job: + name: molecule-tox-py27-centos-7 + parent: molecule-tox-py27 + attempts: 1 + +- job: + name: molecule-tox-py36-centos-8 + parent: molecule-tox-py36 + attempts: 1 + +- job: + name: molecule-tox-py37-fedora-30 + parent: molecule-tox-py37 + attempts: 1 + +- project: + templates: + - publish-to-pypi + check: + jobs: &defaults + - molecule-tox-linters: + vars: + tox_envlist: lint + - molecule-tox-packaging: + vars: + tox_envlist: packaging + - molecule-tox-py27-centos-7: + # broken vagrant-libvirt install + voting: false + - molecule-tox-py36-centos-8: + # broken vagrant-libvirt install + voting: false + - molecule-tox-py37-fedora-30 + - molecule-tox-devel-centos-8: + # broken vagrant-libvirt install + voting: false + gate: + jobs: *defaults