Enable minimal testing

This commit is contained in:
Luke Murphy
2019-10-23 13:04:57 +02:00
parent e96a71866d
commit 76ef7ff97e
42 changed files with 1773 additions and 472 deletions

View File

View File

@ -0,0 +1,5 @@
{
"molecule_directory": "molecule",
"role_name": "OVERRIDDEN",
"scenario_name": "OVERRIDDEN"
}

View File

@ -0,0 +1,16 @@
***************************************
Hetzner Cloud driver installation guide
***************************************
Requirements
============
* Ansible >= 2.8
* ``HCLOUD_TOKEN`` exposed in your environment
Install
=======
.. code-block:: bash
$ pip install 'molecule[hetznercloud]'

View File

@ -0,0 +1,88 @@
---
{% raw -%}
- name: Create
hosts: localhost
connection: local
gather_facts: false
no_log: "{{ molecule_no_log }}"
vars:
ssh_port: 22
ssh_user: root
ssh_path: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/ssh_key"
tasks:
- name: Create SSH key
user:
name: "{{ lookup('env', 'USER') }}"
generate_ssh_key: true
ssh_key_file: "{{ ssh_path }}"
force: true
register: generated_ssh_key
- name: Register the SSH key name
set_fact:
ssh_key_name: "molecule-generated-{{ 12345 | random | to_uuid }}"
- name: Register SSH key for test instance(s)
hcloud_ssh_key:
name: "{{ ssh_key_name }}"
public_key: "{{ generated_ssh_key.ssh_public_key }}"
state: present
- name: Create molecule instance(s)
hcloud_server:
name: "{{ item.name }}"
server_type: "{{ item.server_type }}"
ssh_keys:
- "{{ ssh_key_name }}"
volumes: "{{ item.volumes | default(omit) }}"
image: "{{ item.image }}"
location: "{{ item.location | default(omit) }}"
datacenter: "{{ item.datacenter | default(omit) }}"
user_data: "{{ item.user_data | default(omit) }}"
api_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}"
state: present
register: server
with_items: "{{ molecule_yml.platforms }}"
async: 7200
poll: 0
- name: Wait for instance(s) creation to complete
async_status:
jid: "{{ item.ansible_job_id }}"
register: hetzner_jobs
until: hetzner_jobs.finished
retries: 300
with_items: "{{ server.results }}"
- name: Populate instance config dict
set_fact:
instance_conf_dict: {
'instance': "{{ item.hcloud_server.name }}",
'ssh_key_name': "{{ ssh_key_name }}",
'address': "{{ item.hcloud_server.ipv4_address }}",
'user': "{{ ssh_user }}",
'port': "{{ ssh_port }}",
'identity_file': "{{ ssh_path }}", }
with_items: "{{ hetzner_jobs.results }}"
register: instance_config_dict
when: server.changed | bool
- name: Convert instance config dict to a list
set_fact:
instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}"
when: server.changed | bool
- name: Dump instance config
copy:
content: "{{ instance_conf | to_json | from_json | molecule_to_yaml | molecule_header }}"
dest: "{{ molecule_instance_config }}"
when: server.changed | bool
- name: Wait for SSH
wait_for:
port: "{{ ssh_port }}"
host: "{{ item.address }}"
search_regex: SSH
delay: 10
with_items: "{{ lookup('file', molecule_instance_config) | molecule_from_yaml }}"
{%- endraw %}

View File

@ -0,0 +1,57 @@
---
{% raw -%}
- name: Destroy
hosts: localhost
connection: local
gather_facts: false
no_log: "{{ molecule_no_log }}"
tasks:
- name: Populate the instance config
block:
- name: Populate instance config from file
set_fact:
instance_conf: "{{ lookup('file', molecule_instance_config) | molecule_from_yaml }}"
skip_instances: false
rescue:
- name: Populate instance config when file missing
set_fact:
instance_conf: {}
skip_instances: true
- name: Destroy molecule instance(s)
hcloud_server:
name: "{{ item.instance }}"
api_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}"
state: absent
register: server
with_items: "{{ instance_conf }}"
when: not skip_instances
async: 7200
poll: 0
- name: Wait for instance(s) deletion to complete
async_status:
jid: "{{ item.ansible_job_id }}"
register: hetzner_jobs
until: hetzner_jobs.finished
retries: 300
with_items: "{{ server.results }}"
- name: Remove registered SSH key
hcloud_ssh_key:
name: "{{ instance_conf[0].ssh_key_name }}"
state: absent
when:
- not skip_instances
- instance_conf | length # must contain at least one instance
- name: Populate instance config
set_fact:
instance_conf: {}
- name: Dump instance config
copy:
content: "{{ instance_conf | molecule_to_yaml | molecule_header }}"
dest: "{{ molecule_instance_config }}"
when: server.changed | bool
{%- endraw %}

View File

@ -0,0 +1,7 @@
---
- name: Converge
hosts: all
tasks:
- name: "Include {{ cookiecutter.role_name }}"
include_role:
name: "{{ cookiecutter.role_name }}"

View File

@ -0,0 +1,11 @@
---
{% 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 python-zipstream)
become: true
changed_when: false
{%- endraw %}

View File

@ -0,0 +1,173 @@
# Copyright (c) 2019 Red Hat, 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.
import os
from molecule import logger, util
from molecule.api import Driver
from molecule.util import lru_cache, sysexit_with_message
log = logger.get_logger(__name__)
class HetznerCloud(Driver):
"""
The class responsible for managing `Hetzner Cloud`_ instances.
`Hetzner Cloud`_ is **not** the default driver used in Molecule.
Molecule leverages Ansible's `hcloud_server module`_, by mapping variables
from ``molecule.yml`` into ``create.yml`` and ``destroy.yml``.
.. important::
The ``hcloud_server`` module is only available in Ansible >= 2.8.
.. _`hcloud_server module`: https://docs.ansible.com/ansible/devel/modules/hcloud_server_module.html#hcloud-server-module
.. code-block:: yaml
driver:
name: hetznercloud
platforms:
- name: instance
server_type: cx11
image: debian-9
.. code-block:: bash
$ pip install 'molecule[hetznercloud]'
Change the options passed to the ssh client.
.. code-block:: yaml
driver:
name: hetznercloud
ssh_connection_options:
- '-o ControlPath=~/.ansible/cp/%r@%h-%p'
.. important::
The Hetzner Cloud driver implementation uses the Parmiko transport
provided by Ansible to avoid issues of connection hanging and
indefinite polling as experienced with the default OpenSSh based
transport.
.. important::
Molecule does not merge lists, when overriding the developer must
provide all options.
Provide the files Molecule will preserve upon each subcommand execution.
.. code-block:: yaml
driver:
name: hetznercloud
safe_files:
- foo
""" # noqa
def __init__(self, config=None):
super(HetznerCloud, self).__init__(config)
self._name = "hetznercloud"
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
@property
def login_cmd_template(self):
connection_options = " ".join(self.ssh_connection_options)
return (
"ssh {{address}} "
"-l {{user}} "
"-p {{port}} "
"-i {{identity_file}} "
"{}"
).format(connection_options)
@property
def default_safe_files(self):
return [self.instance_config]
@property
def default_ssh_connection_options(self):
return self._get_ssh_connection_options()
def login_options(self, instance_name):
d = {"instance": instance_name}
return util.merge_dicts(d, self._get_instance_config(instance_name))
def ansible_connection_options(self, instance_name):
try:
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),
}
except StopIteration:
return {}
except IOError:
# Instance has yet to be provisioned , therefore the
# instance_config is not on disk.
return {}
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
)
@lru_cache()
def sanity_checks(self):
"""Hetzner Cloud driver sanity checks."""
log.info("Sanity checks: '{}'".format(self._name))
try:
import hcloud # noqa
except ImportError:
msg = (
"Missing Hetzner Cloud driver dependency. Please "
"install via 'molecule[hetznercloud]' or refer to "
"your INSTALL.rst driver documentation file"
)
sysexit_with_message(msg)
if "HCLOUD_TOKEN" not in os.environ:
msg = (
"Missing Hetzner Cloud API token. Please expose "
"the HCLOUD_TOKEN environment variable with your "
"account API token value"
)
sysexit_with_message(msg)

View File

View File

@ -0,0 +1,104 @@
# 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.
import contextlib
import os
import random
import string
import pytest
from molecule import config, logger, util
from molecule.scenario import ephemeral_directory
LOG = logger.get_logger(__name__)
@pytest.helpers.register
def run_command(cmd, env=os.environ, log=True):
if log:
cmd = _rebake_command(cmd, env)
cmd = cmd.bake(_truncate_exc=False)
return util.run_command(cmd)
def _rebake_command(cmd, env, out=LOG.out, err=LOG.error):
return cmd.bake(_env=env, _out=out, _err=err)
@pytest.fixture
def random_string(length=5):
return "".join((random.choice(string.ascii_uppercase) for _ in range(length)))
@contextlib.contextmanager
def change_dir_to(dir_name):
cwd = os.getcwd()
os.chdir(dir_name)
yield
os.chdir(cwd)
@pytest.fixture
def temp_dir(tmpdir, random_string, request):
directory = tmpdir.mkdir(random_string)
with change_dir_to(directory.strpath):
yield directory
@pytest.fixture
def resources_folder_path():
resources_folder_path = os.path.join(os.path.dirname(__file__), "resources")
return resources_folder_path
@pytest.helpers.register
def molecule_project_directory():
return os.getcwd()
@pytest.helpers.register
def molecule_directory():
return config.molecule_directory(molecule_project_directory())
@pytest.helpers.register
def molecule_scenario_directory():
return os.path.join(molecule_directory(), "default")
@pytest.helpers.register
def molecule_file():
return get_molecule_file(molecule_scenario_directory())
@pytest.helpers.register
def get_molecule_file(path):
return config.molecule_file(path)
@pytest.helpers.register
def molecule_ephemeral_directory(_fixture_uuid):
project_directory = "test-project-{}".format(_fixture_uuid)
scenario_name = "test-instance"
return ephemeral_directory(
os.path.join("molecule_test", project_directory, scenario_name)
)

View File

@ -0,0 +1,9 @@
# 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: <field>"
- '703'

View File

@ -0,0 +1,246 @@
# Copyright (c) 2015-2018 Cisco Systems, Inc.
# Copyright (c) 2018 Red Hat, 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.
import os
import pkg_resources
import shutil
import pexpect
import pytest
import sh
from molecule import logger
from molecule import util
from ..conftest import change_dir_to
LOG = logger.get_logger(__name__)
IS_TRAVIS = os.getenv("TRAVIS") and os.getenv("CI")
def _env_vars_exposed(env_vars, env=os.environ):
"""Check if environment variables are exposed and populated."""
for env_var in env_vars:
if env_var not in os.environ:
return False
return os.environ[env_var] != ""
@pytest.fixture
def with_scenario(request, scenario_to_test, driver_name, scenario_name, skip_test):
scenario_directory = os.path.join(
os.path.dirname(util.abs_path(__file__)),
os.path.pardir,
"scenarios",
scenario_to_test,
)
with change_dir_to(scenario_directory):
yield
if scenario_name:
msg = "CLEANUP: Destroying instances for all scenario(s)"
LOG.out(msg)
options = {"driver_name": driver_name, "all": True}
cmd = sh.molecule.bake("destroy", **options)
pytest.helpers.run_command(cmd)
@pytest.fixture
def skip_test(request, driver_name):
msg_tmpl = (
"Ignoring '{}' tests for now"
if driver_name == "delegated"
else "Skipped '{}' not supported"
)
support_checks_map = {
"hetznercloud": lambda: min_ansible("2.8") and supports_hetznercloud()
}
try:
check_func = support_checks_map[driver_name]
if not check_func():
pytest.skip(msg_tmpl.format(driver_name))
except KeyError:
pass
@pytest.helpers.register
def idempotence(scenario_name):
options = {"scenario_name": scenario_name}
cmd = sh.molecule.bake("create", **options)
pytest.helpers.run_command(cmd)
options = {"scenario_name": scenario_name}
cmd = sh.molecule.bake("converge", **options)
pytest.helpers.run_command(cmd)
options = {"scenario_name": scenario_name}
cmd = sh.molecule.bake("idempotence", **options)
pytest.helpers.run_command(cmd)
@pytest.helpers.register
def init_role(temp_dir, driver_name):
role_directory = os.path.join(temp_dir.strpath, "test-init")
cmd = sh.molecule.bake(
"init", "role", {"driver-name": driver_name, "role-name": "test-init"}
)
pytest.helpers.run_command(cmd)
pytest.helpers.metadata_lint_update(role_directory)
with change_dir_to(role_directory):
options = {"all": True}
cmd = sh.molecule.bake("test", **options)
pytest.helpers.run_command(cmd)
@pytest.helpers.register
def init_scenario(temp_dir, driver_name):
role_directory = os.path.join(temp_dir.strpath, "test-init")
cmd = sh.molecule.bake(
"init", "role", {"driver-name": driver_name, "role-name": "test-init"}
)
pytest.helpers.run_command(cmd)
pytest.helpers.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"}
cmd = sh.molecule.bake("init", "scenario", **options)
pytest.helpers.run_command(cmd)
assert os.path.isdir(scenario_directory)
options = {"scenario_name": "test-scenario", "all": True}
cmd = sh.molecule.bake("test", **options)
pytest.helpers.run_command(cmd)
@pytest.helpers.register
def metadata_lint_update(role_directory):
ansible_lint_src = os.path.join(
os.path.dirname(util.abs_path(__file__)), ".ansible-lint"
)
shutil.copy(ansible_lint_src, role_directory)
with change_dir_to(role_directory):
cmd = sh.ansible_lint.bake(".")
pytest.helpers.run_command(cmd)
@pytest.helpers.register
def list(x):
cmd = sh.molecule.bake("list")
out = pytest.helpers.run_command(cmd, log=False)
out = out.stdout.decode("utf-8")
out = util.strip_ansi_color(out)
for l in x.splitlines():
assert l in out
@pytest.helpers.register
def list_with_format_plain(x):
cmd = sh.molecule.bake("list", {"format": "plain"})
out = pytest.helpers.run_command(cmd, log=False)
out = out.stdout.decode("utf-8")
out = util.strip_ansi_color(out)
for l in x.splitlines():
assert l in out
@pytest.helpers.register
def login(login_args, scenario_name="default"):
options = {"scenario_name": scenario_name}
cmd = sh.molecule.bake("destroy", **options)
pytest.helpers.run_command(cmd)
options = {"scenario_name": scenario_name}
cmd = sh.molecule.bake("create", **options)
pytest.helpers.run_command(cmd)
for instance, regexp in login_args:
if len(login_args) > 1:
child_cmd = "molecule login --host {} --scenario-name {}".format(
instance, scenario_name
)
else:
child_cmd = "molecule login --scenario-name {}".format(scenario_name)
child = pexpect.spawn(child_cmd)
child.expect(regexp)
child.sendline("exit")
@pytest.helpers.register
def test(driver_name, scenario_name="default", parallel=False):
options = {
"scenario_name": scenario_name,
"all": scenario_name is None,
"parallel": parallel,
}
if driver_name == "delegated":
options = {"scenario_name": scenario_name}
cmd = sh.molecule.bake("test", **options)
pytest.helpers.run_command(cmd)
@pytest.helpers.register
def verify(scenario_name="default"):
options = {"scenario_name": scenario_name}
cmd = sh.molecule.bake("create", **options)
pytest.helpers.run_command(cmd)
options = {"scenario_name": scenario_name}
cmd = sh.molecule.bake("converge", **options)
pytest.helpers.run_command(cmd)
options = {"scenario_name": scenario_name}
cmd = sh.molecule.bake("verify", **options)
pytest.helpers.run_command(cmd)
def min_ansible(version):
"""Ensure current Ansible is newer than a given a minimal one."""
try:
from ansible.release import __version__
return pkg_resources.parse_version(__version__) >= pkg_resources.parse_version(
version
)
except ImportError as exception:
LOG.error("Unable to parse Ansible version", exc_info=exception)
return False
@pytest.helpers.register
def supports_hetznercloud():
pytest.importorskip("hcloud")
env_vars = ("HCLOUD_TOKEN",)
return _env_vars_exposed(env_vars)

View File

@ -0,0 +1,312 @@
# 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.
import os
import pytest
import sh
from molecule.scenario import ephemeral_directory
@pytest.fixture
def scenario_to_test(request):
return request.param
@pytest.fixture
def scenario_name(request):
try:
return request.param
except AttributeError:
return None
@pytest.fixture
def driver_name(request):
return request.param
@pytest.mark.parametrize(
"scenario_to_test, driver_name, scenario_name",
[("driver/hetznercloud", "hetznercloud", "default")],
indirect=["scenario_to_test", "driver_name", "scenario_name"],
)
def test_command_check(scenario_to_test, with_scenario, scenario_name):
options = {"scenario_name": scenario_name}
cmd = sh.molecule.bake("check", **options)
pytest.helpers.run_command(cmd)
@pytest.mark.parametrize(
"scenario_to_test, driver_name, scenario_name",
[("driver/hetznercloud", "hetznercloud", "default")],
indirect=["scenario_to_test", "driver_name", "scenario_name"],
)
def test_command_cleanup(scenario_to_test, with_scenario, scenario_name):
options = {"scenario_name": scenario_name}
cmd = sh.molecule.bake("cleanup", **options)
pytest.helpers.run_command(cmd)
@pytest.mark.parametrize(
"scenario_to_test, driver_name, scenario_name",
[("driver/hetznercloud", "hetznercloud", "default")],
indirect=["scenario_to_test", "driver_name", "scenario_name"],
)
def test_command_converge(scenario_to_test, with_scenario, scenario_name):
options = {"scenario_name": scenario_name}
cmd = sh.molecule.bake("converge", **options)
pytest.helpers.run_command(cmd)
@pytest.mark.parametrize(
"scenario_to_test, driver_name, scenario_name",
[("driver/hetznercloud", "hetznercloud", "default")],
indirect=["scenario_to_test", "driver_name", "scenario_name"],
)
def test_command_create(scenario_to_test, with_scenario, scenario_name):
options = {"scenario_name": scenario_name}
cmd = sh.molecule.bake("create", **options)
pytest.helpers.run_command(cmd)
@pytest.mark.skip(
reason="Disabled due to https://github.com/ansible/galaxy/issues/2030"
)
@pytest.mark.parametrize(
"scenario_to_test, driver_name, scenario_name",
[("dependency", "hetznercloud", "ansible-galaxy")],
indirect=["scenario_to_test", "driver_name", "scenario_name"],
)
def test_command_dependency_ansible_galaxy(
request, scenario_to_test, with_scenario, scenario_name
):
options = {"scenario_name": scenario_name}
cmd = sh.molecule.bake("dependency", **options)
pytest.helpers.run_command(cmd)
dependency_role = os.path.join(
ephemeral_directory("molecule"),
"dependency",
"ansible-galaxy",
"roles",
"timezone",
)
assert os.path.isdir(dependency_role)
@pytest.mark.parametrize(
"scenario_to_test, driver_name, scenario_name",
[("dependency", "hetznercloud", "gilt")],
indirect=["scenario_to_test", "driver_name", "scenario_name"],
)
def test_command_dependency_gilt(
request, scenario_to_test, with_scenario, scenario_name
):
options = {"scenario_name": scenario_name}
cmd = sh.molecule.bake("dependency", **options)
pytest.helpers.run_command(cmd)
dependency_role = os.path.join(
ephemeral_directory("molecule"), "dependency", "gilt", "roles", "timezone"
)
assert os.path.isdir(dependency_role)
@pytest.mark.parametrize(
"scenario_to_test, driver_name, scenario_name",
[("dependency", "hetznercloud", "shell")],
indirect=["scenario_to_test", "driver_name", "scenario_name"],
)
def test_command_dependency_shell(
request, scenario_to_test, with_scenario, scenario_name
):
options = {"scenario_name": scenario_name}
cmd = sh.molecule.bake("dependency", **options)
pytest.helpers.run_command(cmd)
dependency_role = os.path.join(
ephemeral_directory("molecule"), "dependency", "shell", "roles", "timezone"
)
assert os.path.isdir(dependency_role)
@pytest.mark.parametrize(
"scenario_to_test, driver_name, scenario_name",
[("driver/hetznercloud", "hetznercloud", "default")],
indirect=["scenario_to_test", "driver_name", "scenario_name"],
)
def test_command_destroy(scenario_to_test, with_scenario, scenario_name):
options = {"scenario_name": scenario_name}
cmd = sh.molecule.bake("destroy", **options)
pytest.helpers.run_command(cmd)
@pytest.mark.parametrize(
"scenario_to_test, driver_name, scenario_name",
[("driver/hetznercloud", "hetznercloud", "default")],
indirect=["scenario_to_test", "driver_name", "scenario_name"],
)
def test_command_idempotence(scenario_to_test, with_scenario, scenario_name):
pytest.helpers.idempotence(scenario_name)
@pytest.mark.parametrize(
"driver_name",
[
("digitalocean"),
("docker"),
("ec2"),
("gce"),
("linode"),
("openstack"),
("vagrant"),
],
indirect=["driver_name"],
)
def test_command_init_role(temp_dir, driver_name, skip_test):
pytest.helpers.init_role(temp_dir, driver_name)
@pytest.mark.parametrize("driver_name", [("hetznercloud")], indirect=["driver_name"])
def test_command_init_scenario(temp_dir, driver_name, skip_test):
pytest.helpers.init_scenario(temp_dir, driver_name)
@pytest.mark.parametrize(
"scenario_to_test, driver_name, scenario_name",
[("driver/hetznercloud", "hetznercloud", "default")],
indirect=["scenario_to_test", "driver_name", "scenario_name"],
)
def test_command_lint(scenario_to_test, with_scenario, scenario_name):
options = {"scenario_name": scenario_name}
cmd = sh.molecule.bake("lint", **options)
pytest.helpers.run_command(cmd)
@pytest.mark.parametrize(
"scenario_to_test, driver_name, expected",
[
(
"driver/hetznercloud",
"hetznercloud",
"""
Instance Name Driver Name Provisioner Name Scenario Name Created Converged
--------------- ------------- ------------------ --------------- --------- -----------
instance hetznercloud ansible default false false
instance-1 hetznercloud ansible multi-node false false
instance-2 hetznercloud ansible multi-node false false
""".strip(), # noqa
)
],
indirect=["scenario_to_test", "driver_name"],
)
def test_command_list(scenario_to_test, with_scenario, expected):
pytest.helpers.list(expected)
@pytest.mark.parametrize(
"scenario_to_test, driver_name, expected",
[
(
"driver/hetznercloud",
"hetznercloud",
"""
instance hetznercloud ansible default false false
instance-1 hetznercloud ansible multi-node false false
instance-2 hetznercloud ansible multi-node false false
""".strip(),
) # noqa
],
indirect=["scenario_to_test", "driver_name"],
)
def test_command_list_with_format_plain(scenario_to_test, with_scenario, expected):
pytest.helpers.list_with_format_plain(expected)
@pytest.mark.parametrize(
"scenario_to_test, driver_name, login_args, scenario_name",
[
(
"driver/hetznercloud",
"hetznercloud",
[["instance-1", ".*instance-1.*"], ["instance-2", ".*instance-2.*"]],
"multi-node",
)
],
indirect=["scenario_to_test", "driver_name", "scenario_name"],
)
def test_command_login(scenario_to_test, with_scenario, login_args, scenario_name):
pytest.helpers.login(login_args, scenario_name)
@pytest.mark.parametrize(
"scenario_to_test, driver_name, scenario_name",
[("driver/hetznercloud", "hetznercloud", "default")],
indirect=["scenario_to_test", "driver_name", "scenario_name"],
)
def test_command_prepare(scenario_to_test, with_scenario, scenario_name):
options = {"scenario_name": scenario_name}
cmd = sh.molecule.bake("create", **options)
pytest.helpers.run_command(cmd)
cmd = sh.molecule.bake("prepare", **options)
pytest.helpers.run_command(cmd)
@pytest.mark.parametrize(
"scenario_to_test, driver_name, scenario_name",
[("driver/hetznercloud", "hetznercloud", "default")],
indirect=["scenario_to_test", "driver_name", "scenario_name"],
)
def test_command_side_effect(scenario_to_test, with_scenario, scenario_name):
options = {"scenario_name": scenario_name}
cmd = sh.molecule.bake("side-effect", **options)
pytest.helpers.run_command(cmd)
@pytest.mark.parametrize(
"scenario_to_test, driver_name, scenario_name",
[("driver/hetznercloud", "hetznercloud", "default")],
indirect=["scenario_to_test", "driver_name", "scenario_name"],
)
def test_command_syntax(scenario_to_test, with_scenario, scenario_name):
options = {"scenario_name": scenario_name}
cmd = sh.molecule.bake("syntax", **options)
pytest.helpers.run_command(cmd)
@pytest.mark.parametrize(
"scenario_to_test, driver_name, scenario_name",
[("driver/hetznercloud", "hetznercloud", None)],
indirect=["scenario_to_test", "driver_name", "scenario_name"],
)
def test_command_test(scenario_to_test, with_scenario, scenario_name, driver_name):
pytest.helpers.test(driver_name, scenario_name)
@pytest.mark.parametrize(
"scenario_to_test, driver_name, scenario_name",
[("driver/hetznercloud", "hetznercloud", "default")],
indirect=["scenario_to_test", "driver_name", "scenario_name"],
)
def test_command_verify(scenario_to_test, with_scenario, scenario_name):
pytest.helpers.verify(scenario_name)

View File

@ -0,0 +1,13 @@
---
extends: default
rules:
braces:
max-spaces-inside: 1
level: error
brackets:
max-spaces-inside: 1
level: error
line-length: disable
truthy: disable

View File

@ -0,0 +1,20 @@
---
dependency:
name: galaxy
driver:
name: docker
lint:
name: yamllint
platforms:
- name: instance
image: centos:${TEST_CENTOS_VERSION}
provisioner:
name: ansible
lint:
name: ansible-lint
scenario:
name: ansible-galaxy
verifier:
name: testinfra
lint:
name: flake8

View File

@ -0,0 +1,88 @@
---
- name: Create
hosts: localhost
connection: local
gather_facts: false
no_log: "{{ molecule_no_log }}"
vars:
ssh_port: 22
ssh_user: root
ssh_path: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/ssh_key"
tasks:
- name: Create SSH key
user:
name: "{{ lookup('env', 'USER') }}"
ssh_key_file: "{{ ssh_path }}"
generate_ssh_key: true
force: true
register: generated_ssh_key
- name: Register the SSH key name
set_fact:
ssh_key_name: "molecule-generated-{{ 12345 | random | to_uuid }}"
- name: Register SSH key for test instance(s)
hcloud_ssh_key:
name: "{{ ssh_key_name }}"
public_key: "{{ generated_ssh_key.ssh_public_key }}"
state: present
- name: Create molecule instance(s)
hcloud_server:
name: "{{ item.name }}"
server_type: "{{ item.server_type }}"
ssh_keys:
- "{{ ssh_key_name }}"
volumes: "{{ item.volumes | default(omit) }}"
image: "{{ item.image }}"
location: "{{ item.location | default(omit) }}"
datacenter: "{{ item.datacenter | default(omit) }}"
user_data: "{{ item.user_data | default(omit) }}"
api_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}"
state: present
register: server
with_items: "{{ molecule_yml.platforms }}"
async: 7200
poll: 0
- name: Wait for instance(s) creation to complete
async_status:
jid: "{{ item.ansible_job_id }}"
register: hetzner_jobs
until: hetzner_jobs.finished
retries: 300
with_items: "{{ server.results }}"
# Mandatory configuration for Molecule to function.
- name: Populate instance config dict
set_fact:
instance_conf_dict: {
'instance': "{{ item.hcloud_server.name }}",
'ssh_key_name': "{{ ssh_key_name }}",
'address': "{{ item.hcloud_server.ipv4_address }}",
'user': "{{ ssh_user }}",
'port': "{{ ssh_port }}",
'identity_file': "{{ ssh_path }}", }
with_items: "{{ hetzner_jobs.results }}"
register: instance_config_dict
when: server.changed | bool
- name: Convert instance config dict to a list
set_fact:
instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}"
when: server.changed | bool
- name: Dump instance config
copy:
content: "{{ instance_conf | to_json | from_json | molecule_to_yaml | molecule_header }}"
dest: "{{ molecule_instance_config }}"
when: server.changed | bool
- name: Wait for SSH
wait_for:
port: "{{ ssh_port }}"
host: "{{ item.address }}"
search_regex: SSH
delay: 10
with_items: "{{ lookup('file', molecule_instance_config) | molecule_from_yaml }}"

View File

@ -0,0 +1,57 @@
---
- name: Destroy
hosts: localhost
connection: local
gather_facts: false
no_log: "{{ molecule_no_log }}"
tasks:
- name: Populate the instance config
block:
- name: Populate instance config from file
set_fact:
instance_conf: "{{ lookup('file', molecule_instance_config) | molecule_from_yaml }}"
skip_instances: false
rescue:
- name: Populate instance config when file missing
set_fact:
instance_conf: {}
skip_instances: true
- name: Destroy molecule instance(s)
hcloud_server:
name: "{{ item.instance }}"
api_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}"
state: absent
register: server
with_items: "{{ instance_conf }}"
when: not skip_instances
async: 7200
poll: 0
- name: Wait for instance(s) deletion to complete
async_status:
jid: "{{ item.ansible_job_id }}"
register: hetzner_jobs
until: hetzner_jobs.finished
retries: 300
with_items: "{{ server.results }}"
- name: Remove registered SSH key
hcloud_ssh_key:
name: "{{ instance_conf[0].ssh_key_name }}"
state: absent
when:
- not skip_instances
- instance_conf # must contain at least one instance
# Mandatory configuration for Molecule to function.
- name: Populate instance config
set_fact:
instance_conf: {}
- name: Dump instance config
copy:
content: "{{ instance_conf | molecule_to_yaml | molecule_header }}"
dest: "{{ molecule_instance_config }}"
when: server.changed | bool

View File

@ -0,0 +1,40 @@
---
dependency:
name: galaxy
driver:
name: hetznercloud
lint:
name: yamllint
options:
config-file: ../../../resources/.yamllint
platforms:
- name: instance
server_type: cx11
image: debian-9
provisioner:
name: ansible
playbooks:
create: ../../../../../resources/playbooks/hetznercloud/create.yml
destroy: ../../../../../resources/playbooks/hetznercloud/destroy.yml
env:
ANSIBLE_ROLES_PATH: ../../../../../resources/roles/
lint:
name: ansible-lint
config_options:
defaults:
timeout: 100
ssh_connection:
scp_if_ssh: true
scenario:
name: default
verifier:
name: testinfra
lint:
name: flake8
enabled: false

View File

@ -0,0 +1,6 @@
---
- name: Converge
hosts: all
gather_facts: false
roles:
- molecule

View File

@ -0,0 +1,50 @@
---
dependency:
name: galaxy
driver:
name: hetznercloud
lint:
name: yamllint
options:
config-file: ../../../resources/.yamllint
platforms:
- name: instance-1
server_type: cx11
image: debian-9
groups:
- foo
- bar
- name: instance-2
server_type: cx11
image: debian-9
groups:
- foo
- baz
provisioner:
name: ansible
playbooks:
create: ../../../../../resources/playbooks/hetznercloud/create.yml
destroy: ../../../../../resources/playbooks/hetznercloud/destroy.yml
env:
ANSIBLE_ROLES_PATH: ../../../../../resources/roles/
lint:
name: ansible-lint
config_options:
defaults:
timeout: 100
ssh_connection:
scp_if_ssh: true
scenario:
name: multi-node
verifier:
name: testinfra
lint:
name: flake8
enabled: false

View File

@ -0,0 +1,24 @@
---
- name: Converge
hosts: all
gather_facts: false
roles:
- molecule
- name: Converge
hosts: bar
gather_facts: false
roles:
- molecule
- name: Converge
hosts: foo
gather_facts: false
roles:
- molecule
- name: Converge
hosts: baz
gather_facts: false
roles:
- molecule

View File

@ -0,0 +1,252 @@
# 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 uuid import uuid4
import copy
import functools
import os
import shutil
try:
from pathlib import Path
except ImportError:
from pathlib2 import Path
import pytest
from molecule import util
from molecule import config
@pytest.helpers.register
def write_molecule_file(filename, data):
util.write_file(filename, util.safe_dump(data))
@pytest.helpers.register
def os_split(s):
rest, tail = os.path.split(s)
if rest in ("", os.path.sep):
return (tail,)
return os_split(rest) + (tail,)
@pytest.fixture
def _molecule_dependency_galaxy_section_data():
return {"dependency": {"name": "galaxy"}}
@pytest.fixture
def _molecule_driver_section_data():
return {"driver": {"name": "docker", "options": {"managed": True}}}
@pytest.fixture
def _molecule_lint_section_data():
return {"lint": {"name": "yamllint"}}
@pytest.fixture
def _molecule_platforms_section_data():
return {
"platforms": [
{"name": "instance-1", "groups": ["foo", "bar"], "children": ["child1"]},
{"name": "instance-2", "groups": ["baz", "foo"], "children": ["child2"]},
]
}
@pytest.fixture
def _molecule_provisioner_section_data():
return {
"provisioner": {
"name": "ansible",
"options": {"become": True},
"lint": {"name": "ansible-lint"},
"config_options": {},
}
}
@pytest.fixture
def _molecule_scenario_section_data():
return {"scenario": {"name": "default"}}
@pytest.fixture
def _molecule_verifier_section_data():
return {"verifier": {"name": "testinfra", "lint": {"name": "flake8"}}}
@pytest.fixture
def molecule_data(
_molecule_dependency_galaxy_section_data,
_molecule_driver_section_data,
_molecule_lint_section_data,
_molecule_platforms_section_data,
_molecule_provisioner_section_data,
_molecule_scenario_section_data,
_molecule_verifier_section_data,
):
fixtures = [
_molecule_dependency_galaxy_section_data,
_molecule_driver_section_data,
_molecule_lint_section_data,
_molecule_platforms_section_data,
_molecule_provisioner_section_data,
_molecule_scenario_section_data,
_molecule_verifier_section_data,
]
return functools.reduce(lambda x, y: util.merge_dicts(x, y), fixtures)
@pytest.fixture
def molecule_directory_fixture(temp_dir):
return pytest.helpers.molecule_directory()
@pytest.fixture
def molecule_scenario_directory_fixture(molecule_directory_fixture):
path = pytest.helpers.molecule_scenario_directory()
if not os.path.isdir(path):
os.makedirs(path)
return path
@pytest.fixture
def molecule_ephemeral_directory_fixture(molecule_scenario_directory_fixture):
path = pytest.helpers.molecule_ephemeral_directory(str(uuid4()))
if not os.path.isdir(path):
os.makedirs(path)
yield
shutil.rmtree(str(Path(path).parent))
@pytest.fixture
def molecule_file_fixture(
molecule_scenario_directory_fixture, molecule_ephemeral_directory_fixture
):
return pytest.helpers.molecule_file()
@pytest.fixture
def config_instance(molecule_file_fixture, molecule_data, request):
mdc = copy.deepcopy(molecule_data)
if hasattr(request, "param"):
util.merge_dicts(mdc, request.getfixturevalue(request.param))
pytest.helpers.write_molecule_file(molecule_file_fixture, mdc)
c = config.Config(molecule_file_fixture)
c.command_args = {"subcommand": "test"}
return c
# Mocks
@pytest.fixture
def patched_print_debug(mocker):
return mocker.patch("molecule.util.print_debug")
@pytest.fixture
def patched_logger_info(mocker):
return mocker.patch("logging.Logger.info")
@pytest.fixture
def patched_logger_out(mocker):
return mocker.patch("molecule.logger.CustomLogger.out")
@pytest.fixture
def patched_logger_warn(mocker):
return mocker.patch("logging.Logger.warn")
@pytest.fixture
def patched_logger_error(mocker):
return mocker.patch("logging.Logger.error")
@pytest.fixture
def patched_logger_critical(mocker):
return mocker.patch("logging.Logger.critical")
@pytest.fixture
def patched_logger_success(mocker):
return mocker.patch("molecule.logger.CustomLogger.success")
@pytest.fixture
def patched_run_command(mocker):
m = mocker.patch("molecule.util.run_command")
m.return_value = mocker.Mock(stdout=b"patched-run-command-stdout")
return m
@pytest.fixture
def patched_ansible_converge(mocker):
m = mocker.patch("molecule.provisioner.ansible.Ansible.converge")
m.return_value = "patched-ansible-converge-stdout"
return m
@pytest.fixture
def patched_add_or_update_vars(mocker):
return mocker.patch("molecule.provisioner.ansible.Ansible._add_or_update_vars")
@pytest.fixture
def patched_yamllint(mocker):
return mocker.patch("molecule.lint.yamllint.Yamllint.execute")
@pytest.fixture
def patched_flake8(mocker):
return mocker.patch("molecule.verifier.lint.flake8.Flake8.execute")
@pytest.fixture
def patched_ansible_galaxy(mocker):
return mocker.patch("molecule.dependency.ansible_galaxy.AnsibleGalaxy.execute")
@pytest.fixture
def patched_testinfra(mocker):
return mocker.patch("molecule.verifier.testinfra.Testinfra.execute")
@pytest.fixture
def patched_scenario_setup(mocker):
mocker.patch("molecule.config.Config.env")
return mocker.patch("molecule.scenario.Scenario._setup")
@pytest.fixture
def patched_config_validate(mocker):
return mocker.patch("molecule.config.Config._validate")

View File

@ -0,0 +1,82 @@
# 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.
import os
import pytest
import sh
from molecule import util
from molecule.command.init import base
from molecule.model import schema_v2
class CommandBase(base.Base):
pass
@pytest.fixture
def _base_class():
return CommandBase
@pytest.fixture
def _instance(_base_class):
return _base_class()
@pytest.fixture
def _command_args():
return {
"dependency_name": "galaxy",
"driver_name": "docker",
"lint_name": "yamllint",
"provisioner_name": "ansible",
"scenario_name": "default",
"role_name": "test-role",
"verifier_name": "testinfra",
}
@pytest.fixture
def _role_directory():
return "."
@pytest.fixture
def _molecule_file(_role_directory):
return os.path.join(
_role_directory, "test-role", "molecule", "default", "molecule.yml"
)
@pytest.mark.parametrize("driver", [("hetznercloud")])
def test_drivers(
driver, temp_dir, _molecule_file, _role_directory, _command_args, _instance
):
_command_args["driver_name"] = driver
_instance._process_templates("molecule", _command_args, _role_directory)
data = util.safe_load_file(_molecule_file)
assert {} == schema_v2.validate(data)
cmd = sh.yamllint.bake("-s", _molecule_file)
pytest.helpers.run_command(cmd)

View File

@ -0,0 +1,200 @@
# Copyright (c) 2019 Red Hat, 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.
import os
import pytest
from molecule import config
from molecule.driver import hetznercloud
@pytest.fixture
def hetznercloud_instance(patched_config_validate, config_instance):
return hetznercloud.HetznerCloud(config_instance)
def test_hetznercloud_config_gives_config_object(hetznercloud_instance):
assert isinstance(hetznercloud_instance._config, config.Config)
def test_hetznercloud_testinfra_options_property(hetznercloud_instance):
assert {
"connection": "ansible",
"ansible-inventory": hetznercloud_instance._config.provisioner.inventory_file,
} == hetznercloud_instance.testinfra_options
def test_hetznercloud_name_property(hetznercloud_instance):
assert "hetznercloud" == hetznercloud_instance.name
def test_hetznercloud_options_property(hetznercloud_instance):
assert {"managed": True} == hetznercloud_instance.options
def test_hetznercloud_login_cmd_template_property(hetznercloud_instance):
template = "ssh {address} -l {user} -p {port}"
assert template in hetznercloud_instance.login_cmd_template
def test_hetznercloud_safe_files_property(hetznercloud_instance):
expected_safe_files = [
os.path.join(
hetznercloud_instance._config.scenario.ephemeral_directory,
"instance_config.yml",
)
]
assert expected_safe_files == hetznercloud_instance.safe_files
def test_hetznercloud_default_safe_files_property(hetznercloud_instance):
expected_default_safe_files = [
os.path.join(
hetznercloud_instance._config.scenario.ephemeral_directory,
"instance_config.yml",
)
]
assert expected_default_safe_files == hetznercloud_instance.default_safe_files
def test_hetznercloud_delegated_property(hetznercloud_instance):
assert not hetznercloud_instance.delegated
def test_hetznercloud_managed_property(hetznercloud_instance):
assert hetznercloud_instance.managed
def test_hetznercloud_default_ssh_connection_options_property(hetznercloud_instance):
expected_options = [
"-o UserKnownHostsFile=/dev/null",
"-o ControlMaster=auto",
"-o ControlPersist=60s",
"-o IdentitiesOnly=yes",
"-o StrictHostKeyChecking=no",
]
assert expected_options == (hetznercloud_instance.default_ssh_connection_options)
def test_hetznercloud_login_options(hetznercloud_instance, mocker):
target = "molecule.driver.hetznercloud.HetznerCloud._get_instance_config"
get_instance_config_patch = mocker.patch(target)
get_instance_config_patch.return_value = {
"instance": "hetznercloud",
"address": "172.16.0.2",
"user": "hetzner-admin",
"port": 22,
}
get_instance_config_patch = {
"instance": "hetznercloud",
"address": "172.16.0.2",
"user": "hetzner-admin",
"port": 22,
}
assert get_instance_config_patch == hetznercloud_instance.login_options(
"hetznercloud"
)
def test_hetznercloud_ansible_connection_opts(hetznercloud_instance, mocker):
target = "molecule.driver.hetznercloud.HetznerCloud._get_instance_config"
get_instance_config_patch = mocker.patch(target)
get_instance_config_patch.return_value = {
"instance": "hetznercloud",
"address": "172.16.0.2",
"user": "hetzner-admin",
"port": 22,
"identity_file": "/foo/bar",
}
get_instance_config_patch = {
"ansible_host": "172.16.0.2",
"ansible_port": 22,
"ansible_user": "hetzner-admin",
"ansible_private_key_file": "/foo/bar",
"connection": "ssh",
"ansible_ssh_common_args": (
"-o UserKnownHostsFile=/dev/null "
"-o ControlMaster=auto "
"-o ControlPersist=60s "
"-o IdentitiesOnly=yes "
"-o StrictHostKeyChecking=no"
),
}
connection_options = hetznercloud_instance.ansible_connection_options(
"hetznercloud"
)
assert get_instance_config_patch == connection_options
def test_hetznercloud_instance_config_property(hetznercloud_instance):
instance_config_path = os.path.join(
hetznercloud_instance._config.scenario.ephemeral_directory,
"instance_config.yml",
)
assert instance_config_path == hetznercloud_instance.instance_config
def test_hetznercloud_ssh_connection_options_property(hetznercloud_instance):
expected_options = [
"-o UserKnownHostsFile=/dev/null",
"-o ControlMaster=auto",
"-o ControlPersist=60s",
"-o IdentitiesOnly=yes",
"-o StrictHostKeyChecking=no",
]
assert expected_options == hetznercloud_instance.ssh_connection_options
def test_hetznercloud_status(mocker, hetznercloud_instance):
hetzner_status = hetznercloud_instance.status()
assert 2 == len(hetzner_status)
assert hetzner_status[0].instance_name == "instance-1"
assert hetzner_status[0].driver_name == "hetznercloud"
assert hetzner_status[0].provisioner_name == "ansible"
assert hetzner_status[0].scenario_name == "default"
assert hetzner_status[0].created == "false"
assert hetzner_status[0].converged == "false"
assert hetzner_status[1].instance_name == "instance-2"
assert hetzner_status[1].driver_name == "hetznercloud"
assert hetzner_status[1].provisioner_name == "ansible"
assert hetzner_status[1].scenario_name == "default"
assert hetzner_status[1].created == "false"
assert hetzner_status[1].converged == "false"
def test_created(hetznercloud_instance):
assert "false" == hetznercloud_instance._created()
def test_converged(hetznercloud_instance):
assert "false" == hetznercloud_instance._converged()

View File

@ -0,0 +1,46 @@
# 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.
import os
import pytest
from molecule import util
@pytest.fixture
def _molecule_file():
return os.path.join(
os.path.dirname(__file__),
os.path.pardir,
os.path.pardir,
os.path.pardir,
"resources",
"molecule_docker.yml",
)
@pytest.fixture
def _config(_molecule_file, request):
d = util.safe_load(open(_molecule_file))
if hasattr(request, "param"):
d = util.merge_dicts(d, request.getfixturevalue(request.param))
return d

View File

@ -0,0 +1,101 @@
# 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.
import pytest
from molecule.model import schema_v2
@pytest.fixture
def _model_platform_hetznercloud_section_data():
return {
"driver": {"name": "hetznercloud"},
"platforms": [
{
"name": "instance",
"server_type": "",
"volumes": [""],
"image": "",
"location": "",
"datacenter": "",
"user_data": "",
}
],
}
@pytest.mark.parametrize(
"_config", ["_model_platform_hetznercloud_section_data"], indirect=True
)
def test_platforms_hetznercloud(_config):
assert {} == schema_v2.validate(_config)
@pytest.fixture
def _model_platforms_hetznercloud_errors_section_data():
return {
"driver": {"name": "hetznercloud"},
"platforms": [
{
"name": 0,
"server_type": 0,
"volumes": {},
"image": 0,
"location": 0,
"datacenter": 0,
"user_data": 0,
}
],
}
@pytest.mark.parametrize(
"_config", ["_model_platforms_hetznercloud_errors_section_data"], indirect=True
)
def test_platforms_hetznercloud_has_errors(_config):
expected_config = {
"platforms": [
{
0: [
{
"name": ["must be of string type"],
"server_type": ["must be of string type"],
"volumes": ["must be of list type"],
"image": ["must be of string type"],
"location": ["must be of string type"],
"datacenter": ["must be of string type"],
"user_data": ["must be of string type"],
}
]
}
]
}
assert expected_config == schema_v2.validate(_config)
@pytest.mark.parametrize(
"_config", ["_model_platform_hetznercloud_section_data"], indirect=True
)
@pytest.mark.parametrize("_required_field", ("server_type", "image"))
def test_platforms_hetznercloud_fields_required(_config, _required_field):
del _config["platforms"][0][_required_field]
expected_config = {"platforms": [{0: [{_required_field: ["required field"]}]}]}
assert expected_config == schema_v2.validate(_config)