Molecule test Ansible

Loadays, Antwerp, 2019-05-04

Jeudis du Libre, Mons, 2019-03-21

About the author

243761
  • User & contributor in Molecule / Ansible ecosystem.

  • Free Software Infrastructure Automation.

  • Technical writer @ Red Hat.

Culture, Automation, Measurement, Sharing.

Ansible User stories

  • Day 2 routines: system patches, audit, inventory.

  • Reproductible provisioning, from hypervisor to apps.

  • Automated backup & restore data.

  • Maintain environments on shared hosting platforms.

  • Deploy software.

  • Build CI pipelines.

  • Manage everything API: network, cloud, kubernetes.

Common problem

Credit: National Library of Sweden, shelfmark KoB 1 ab
  • Validate roles and playbooks before production

  • Instantiate temporary infrastructure

Solution

Disclaimer

Tooling lanscape

Molecule has many friends in the toolbox.

  • Ansible ecosystem

  • Platforms backends

  • Dependency backends

  • Verifiers

Ansible

Ansible is an IT automation tool. It can configure systems, deploy software, and orchestrate more advanced IT tasks such as continuous deployments or zero downtime rolling updates. Ansible’s main goals are simplicity and ease-of-use.
— https://docs.ansible.com/ansible

Molecule

Molecule is designed to aid in the development and testing of Ansible roles. […​] Molecule is opinionated in order to encourage an approach that results in consistently developed roles that are well-written, easily understood and maintained.
— https://molecule.readthedocs.io

Ansible-lint

  • Improve the roles quality.

  • Kill opinion wars.

Ansible Lint is a commandline tool for linting playbooks. Use it to detect behaviors and practices that could potentially be improved.
— https://docs.ansible.com/ansible-lint

Backends

  • (local) Virtualization

  • Cloud provider

  • Bake your own

(local) Virtualization

  • Docker

  • LXC

  • LXD

  • Vagrant

Cloud provider

  • Azure

  • EC2

  • GCE

  • Linode

  • Openstack

Slow! Keep it for specific cloud features, Windows.

Bake your own

  • Delegated

Verifier

Audit the state of the tested platform after role execution with an independant tool.

  • Testinfra

  • Goss

  • Inspec

Testinfra

  • Default verifier.

  • Write tests in python.

  • Public == python developers.

With Testinfra you can write unit tests in Python to test actual state of your servers configured by management tools.

— https://testinfra.readthedocs.io

Goss

  • Easy. YAML syntax, fit well in the Ansible ecosystem.

  • Fast. Near instantaneous.

  • Small. <10MB single self-contained binary.

  • Linux only.

Goss is a YAML based serverspec alternative tool for validating a server’s configuration.
— https://goss.rocks

Inspec

  • Complex, ruby based, with a feature full DSL.

  • Linux, MacOS and Windows support.

  • Public == ruby developers.

InSpec is compliance as code. Turn your compliance, security, and other policy requirements into automated tests.
— https://www.inspec.io/

Usage

Usage: molecule [OPTIONS] COMMAND [ARGS]...

   _____     _             _
  |     |___| |___ ___ _ _| |___
  | | | | . | | -_|  _| | | | -_|
  |_|_|_|___|_|___|___|___|_|___|

  Molecule aids in the development and testing of Ansible roles.

  Enable autocomplete issue:

    eval "$(_MOLECULE_COMPLETE=source molecule)"

Options:
  --debug / --no-debug    Enable or disable debug mode. Default is disabled.
  -c, --base-config TEXT  Path to a base config.  If provided Molecule will
                          load this config first, and deep merge each
                          scenario's molecule.yml on top.
                          (/home/fab/.config/molecule/config.yml)
  -e, --env-file TEXT     The file to read variables from when rendering
                          molecule.yml. (.env.yml)
  --version               Show the version and exit.
  --help                  Show this message and exit.

Commands:
  check        Use the provisioner to perform a Dry-Run...
  cleanup      Use the provisioner to cleanup any changes...
  converge     Use the provisioner to configure instances...
  create       Use the provisioner to start the instances.
  dependency   Manage the role's dependencies.
  destroy      Use the provisioner to destroy the instances.
  idempotence  Use the provisioner to configure the...
  init         Initialize a new role or scenario.
  lint         Lint the role.
  list         Lists status of instances.
  login        Log in to one instance.
  matrix       List matrix of steps used to test instances.
  prepare      Use the provisioner to prepare the instances...
  side-effect  Use the provisioner to perform side-effects...
  syntax       Use the provisioner to syntax check the role.
  test         Test (lint, destroy, dependency, syntax,...
  verify       Run automated tests against instances.

Molecule scenario dissected

$ molecule matrix -s default test
--> Test matrix

└── default
    ├── lint
    ├── cleanup
    ├── destroy
    ├── dependency
    ├── syntax
    ├── create
    ├── prepare
    ├── converge
    ├── idempotence
    ├── side_effect
    ├── verify
    ├── cleanup
    └── destroy

lint

Enforce syntax rules (ansible-lint, yamllint).
Kill opinion wars and improve the roles quality.
--> Executing Ansible Lint on molecule/default/playbook.yml...
    [701] No 'galaxy_info' found
    meta/main.yml:1

    [306] Shells that use pipes should set the pipefail option
    molecule/default/playbook.yml:20
    Task/Handler: shell | get version of common_linux

    [206] Variables should have spaces before and after: {{ var_name }}
    tasks/task_60_cron.yml:19
        path: "/etc/cron.{{cron_item}}"

cleanup

To be used in conjunction with prepare
Cleanup changes that were made outside of Molecule’s test platforms.
remote database connections
user accounts
--> Action: 'cleanup'
Skipping, cleanup playbook not configured.

destroy

Destroy the temporary platforms.
molecule destroy
--> Scenario: 'default'
--> Action: 'destroy'

    PLAY [Destroy] *****************************************************************

    TASK [Destroy molecule instance(s)] ********************************************
    changed: [localhost] => (item={'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True})

    TASK [Wait for instance(s) deletion to complete] *******************************
    FAILED - RETRYING: Wait for instance(s) deletion to complete (300 retries left).
    changed: [localhost] => (item={'started': 1, 'finished': 0, 'ansible_job_id': '498157453262.13184', 'results_file': '/home/fab/.ansible_async/498157453262.13184', '_ansible_parsed': True, 'changed': True, '_ansible_no_log': False, 'failed': False, 'item': {'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True}, '_ansible_item_result': True, '_ansible_ignore_errors': None, '_ansible_item_label': {'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True}})

    TASK [Delete docker network(s)] ************************************************

    PLAY RECAP *********************************************************************
    localhost                  : ok=2    changed=2    unreachable=0    failed=0

dependency

Install external requirements with supported backends:
galaxy: default. Lots of WIP in the air.
gilt: nice overlays on top of git.
shell: your own.
--> Action: 'dependency'
    - downloading role 'repo-remi', owned by geerlingguy
    - downloading role from https://github.com/geerlingguy/ansible-role-repo-remi/archive/1.2.0.tar.gz
    - extracting geerlingguy.repo-remi to /tmp/molecule/ansible-role-phpmyadmin/default/roles/geerlingguy.repo-remi
    - geerlingguy.repo-remi (1.2.0) was installed successfully
    - downloading role 'apache', owned by geerlingguy
    - downloading role from https://github.com/geerlingguy/ansible-role-apache/archive/3.0.3.tar.gz
    - extracting geerlingguy.apache to /tmp/molecule/ansible-role-phpmyadmin/default/roles/geerlingguy.apache
    - geerlingguy.apache (3.0.3) was installed successfully
    - downloading role 'mysql', owned by geerlingguy
    - downloading role from https://github.com/geerlingguy/ansible-role-mysql/archive/2.9.4.tar.gz
    - extracting geerlingguy.mysql to /tmp/molecule/ansible-role-phpmyadmin/default/roles/geerlingguy.mysql
    - geerlingguy.mysql (2.9.4) was installed successfully
    - downloading role 'php-versions', owned by geerlingguy
    - downloading role from https://github.com/geerlingguy/ansible-role-php-versions/archive/3.0.0.tar.gz
    - extracting geerlingguy.php-versions to /tmp/molecule/ansible-role-phpmyadmin/default/roles/geerlingguy.php-versions
    - geerlingguy.php-versions (3.0.0) was installed successfully
    - downloading role 'php', owned by geerlingguy
    - downloading role from https://github.com/geerlingguy/ansible-role-php/archive/3.7.0.tar.gz
    - extracting geerlingguy.php to /tmp/molecule/ansible-role-phpmyadmin/default/roles/geerlingguy.php
    - geerlingguy.php (3.7.0) was installed successfully
    - downloading role 'php-mysql', owned by geerlingguy
    - downloading role from https://github.com/geerlingguy/ansible-role-php-mysql/archive/2.0.2.tar.gz
    - extracting geerlingguy.php-mysql to /tmp/molecule/ansible-role-phpmyadmin/default/roles/geerlingguy.php-mysql
    - geerlingguy.php-mysql (2.0.2) was installed successfully

syntax

ansible-playbook --syntax-check
Complementary to lint, but need the external roles to work.
molecule syntax
--> Scenario: 'default'
--> Action: 'syntax'

    playbook: molecule/default/playbook.yml

create

Create the temporary platforms.
Local backends are faster and more reliable.
Cloud backends: abuse of retry, be patient
--> Scenario: 'default'
--> Action: 'create'

    PLAY [Create] ******************************************************************

    TASK [Log into a Docker registry] **********************************************
    skipping: [localhost] => (item={'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True})

    TASK [Create Dockerfiles from image names] *************************************
    skipping: [localhost] => (item={'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True})

    TASK [Discover local Docker images] ********************************************
    ok: [localhost] => (item={'changed': False, 'skipped': True, 'skip_reason': 'Conditional result was False', '_ansible_no_log': False, 'item': {'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True}, '_ansible_item_result': True, '_ansible_ignore_errors': None, '_ansible_item_label': {'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True}})

    TASK [Build an Ansible compatible image] ***************************************
    skipping: [localhost] => (item={'changed': False, 'skipped': True, 'skip_reason': 'Conditional result was False', '_ansible_no_log': False, 'item': {'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True}, '_ansible_item_result': True, '_ansible_ignore_errors': None, '_ansible_item_label': {'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True}})

    TASK [Create docker network(s)] ************************************************

    TASK [Determine the CMD directives] ********************************************
    skipping: [localhost] => (item={'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True})

    TASK [Create molecule instance(s)] *********************************************
    changed: [localhost] => (item={'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True})

    TASK [Wait for instance(s) creation to complete] *******************************
    FAILED - RETRYING: Wait for instance(s) creation to complete (300 retries left).
    changed: [localhost] => (item={'started': 1, 'finished': 0, 'ansible_job_id': '57155955616.7787', 'results_file': '/home/fab/.ansible_async/57155955616.7787', '_ansible_parsed': True, 'changed': True, '_ansible_no_log': False, 'failed': False, 'item': {'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True}, '_ansible_item_result': True, '_ansible_ignore_errors': None, '_ansible_item_label': {'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True}})

    PLAY RECAP *********************************************************************
    localhost                  : ok=3    changed=2    unreachable=0    failed=0

prepare

Optional actions which bring the system to a given state prior to converge.
remote database connections
user accounts
kubeconfig
--> Scenario: 'default'
--> Action: 'prepare'

    PLAY [Prepare] *****************************************************************

    TASK [delete the kubeconfig if present] ****************************************
    ok: [kind-default -> localhost]

    TASK [Fetch the kubeconfig] ****************************************************
    changed: [kind-default]

    TASK [Change the kubeconfig port to the proper value] **************************
    changed: [kind-default -> localhost]

    TASK [Wait for the Kubernetes API to become available (this could take a minute)] ***
    FAILED - RETRYING: Wait for the Kubernetes API to become available (this could take a minute) (60 retries left).
    FAILED - RETRYING: Wait for the Kubernetes API to become available (this could take a minute) (59 retries left).
    FAILED - RETRYING: Wait for the Kubernetes API to become available (this could take a minute) (58 retries left).
    FAILED - RETRYING: Wait for the Kubernetes API to become available (this could take a minute) (57 retries left).
    FAILED - RETRYING: Wait for the Kubernetes API to become available (this could take a minute) (56 retries left).
    ok: [kind-default]

    PLAY RECAP *********************************************************************
    kind-default               : ok=4    changed=2    unreachable=0    failed=0

converge

Execute the main role.
--> Scenario: 'default'
--> Action: 'converge'

    PLAY [Converge] ****************************************************************

    TASK [Gathering Facts] *********************************************************
    ok: [localhost]

    PLAY [Verify] ******************************************************************

    TASK [Gathering Facts] *********************************************************
    ok: [localhost]

    TASK [Get all pods in osdk-test] ***********************************************
    ok: [localhost]

    TASK [Output pods] *************************************************************
    ok: [localhost] => {
        "pods": {
            "changed": false,
            "failed": false,
            "resources": []
        }
    }

    PLAY RECAP *********************************************************************
    localhost                  : ok=4    changed=0    unreachable=0    failed=0


--> Scenario: 'default'
--> Action: 'idempotence'
Idempotence completed successfully.

idempotence

The main role is executed again; the result should change nothing in order to achieve idempotence.
Key feature!
Idempotence is a goal difficult to achieve, particularly on Windows.
--> Action: 'idempotence'
Idempotence completed successfully.

side_effect

Optional actions which are not in the role, after converge.
--> Scenario: 'default'
--> Action: 'side_effect'
Skipping, side_effect playbook not configured.

verify

Execute an audit tool to verify that the final state is meeting expectations.
testinfra - default, python based.
Goss - Linux only. Easy and fast.
Inspec - ruby DSL, works well for Windows targets.
--> Scenario: 'default'
--> Action: 'verify'
--> Executing Testinfra tests found in /home/fab/src/themr0c/talk-jdl2019/examples/geerlingguy.phpmyadmin/molecule/default/tests/...
    ============================= test session starts ==============================
    platform linux2 -- Python 2.7.16, pytest-4.3.1, py-1.8.0, pluggy-0.9.0
    rootdir: /home/fab/src/themr0c/talk-jdl2019/examples/geerlingguy.phpmyadmin/molecule/default, inifile:
    plugins: testinfra-1.16.0
collected 1 item

    tests/test_default.py .                                                  [100%]

    ========================== 1 passed in 12.76 seconds ===========================
Verifier completed successfully.

destroy

Destroy the temporary platforms.
molecule destroy
--> Scenario: 'default'
--> Action: 'destroy'

    PLAY [Destroy] *****************************************************************

    TASK [Destroy molecule instance(s)] ********************************************
    changed: [localhost] => (item={'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True})

    TASK [Wait for instance(s) deletion to complete] *******************************
    FAILED - RETRYING: Wait for instance(s) deletion to complete (300 retries left).
    changed: [localhost] => (item={'started': 1, 'finished': 0, 'ansible_job_id': '498157453262.13184', 'results_file': '/home/fab/.ansible_async/498157453262.13184', '_ansible_parsed': True, 'changed': True, '_ansible_no_log': False, 'failed': False, 'item': {'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True}, '_ansible_item_result': True, '_ansible_ignore_errors': None, '_ansible_item_label': {'name': 'kind-default', 'groups': ['k8s'], 'image': 'bsycorp/kind:latest-1.12', 'privileged': True, 'override_command': False, 'exposed_ports': ['8443/tcp', '10080/tcp'], 'published_ports': ['0.0.0.0:9443:8443/tcp'], 'pre_build_image': True}})

    TASK [Delete docker network(s)] ************************************************

    PLAY RECAP *********************************************************************
    localhost                  : ok=2    changed=2    unreachable=0    failed=0

From molecule to delivery pipeline

  • Objective: validate that all roles are in a good shape, ready to deliver.

Scope definition

  • Test one role with molecule

  • Test multiple roles with tox

  • Automate on a Continuous Integration platform

Test one role at user request, locally

  • Before committing any changes to Git

  • Tradeoffs: blocking, slow, antivirus

  • Run all tests on the named role ${rolename}:

    cd ${rolename}
    molecule test --all

Tox

  • Orchestrate tests on a collection of roles.

  • Isolated python virtual environments.

Tox aims to automate and standardize testing in Python. It is part of a larger vision of easing the packaging, testing and release process of Python software.
— https://tox.readthedocs.io
$ pip install tox virtualenv

Configure tox.ini

  • Molecule role == Tox named environment

  • Running platforms are not isolated!

[tox]
envlist =
  my_example_role
  another_role
skipsdist = true
[testenv]
basepython = python3
commands = bash -c "(cd {toxinidir}/roles/{envname} && molecule --debug
test --all)"
description = molecule test role {envname}
deps = -r {toxinidir}/requirements.txt
setenv = MOLECULE_EPHEMERAL_DIRECTORY={envname}
sitepackages = true
whitelist_externals =
  /bin/bash
  /usr/bin/rubocop

Run tox tests

$ tox (1)
$ tox -e ${rolename} (2)
1Run all molecule tests on all roles
2Same, limited to the named role ${rolename}

Automation on a continuous integration server

  • On pull request. Never merge broken code!

  • At commit time on release branches / tags.

  • Foresee long compute time!

  • Need to run privileged Docker containers

Use molecule docker image

docker_image_name="quay.io/ansible/molecule:2.19" (1)
gid_map="$(grep docker /etc/group |cut -d: -f3,3)" (2)
local_playbooks_absolute_path="$(readlink -f "$(dirname "${BASH_SOURCE[0]}")")" (3)
docker run \
  -e "WORKDIR=${molecule_workdir}" \ (3)
  -v /var/run/docker.sock:/var/run/docker.sock \ (4)
  -v "${local_playbooks_absolute_path}:${molecule_workdir}" (3)
  "${docker_image_name}" bash "molecule test"
1Docker image from https://quay.io/repository/ansible/molecule
2Map local user
3Mount the code
4Share docker socket

Orchestration pitfalls

1 repository == 1 role
multiple roles ⇒ external orchestration
  • scenarios ⇒ sequential

  • platforms ⇒ parallel

Implementation examples

  • Galaxy role example: geerlingguy.phpmyadmin

  • Initialize our own role

  • molecule in Kubernetes Operator SDK

Example: Galaxy role geerlingguy.phpmyadmin

# sudo apt install python3 python3-dev python3-psutil
sudo dnf install python3 python3-devel python3-psutil
mkvirtualenv -p /usr/bin/python3 molecule-python3
pip install molecule docker
git clone git@github.com:geerlingguy/ansible-role-phpmyadmin.git geerlingguy.phpmyadmin
cd geerlingguy.phpmyadmin
molecule test

Create a new role

Usage: molecule init [OPTIONS] COMMAND [ARGS]...

  Initialize a new role or scenario.

Options:
  --help  Show this message and exit.

Commands:
  role      Initialize a new role for use with Molecule.
  scenario  Initialize a new scenario for use with...
  template  Initialize a new role from a Cookiecutter...

Molecule init demo

molecule  init role --driver-name docker --verifier-name goss  --role-name jdl
--> Initializing new role jdl...
Initialized role in /home/fab/src/jdl-2019/jdl successfully.

Default values need to be changed

--> Executing Ansible Lint on /home/fab/src/jdl-2019/jdl/molecule/default/playbook.yml...
    [701] Role info should contain platforms
    /home/fab/src/jdl-2019/jdl/meta/main.yml:1
    {'meta/main.yml': {'galaxy_info': {'author': 'your name', 'description': 'your description', 'company': 'your company (optional)', 'license': 'license (GPLv2, CC-BY, etc)', 'min_ansible_version': 1.2, 'galaxy_tags': [], '__line__': 2, '__file__': '/home/fab/src/jdl-2019/jdl/meta/main.yml'}, 'dependencies': [], '__line__': 1, '__file__': '/home/fab/src/jdl-2019/jdl/meta/main.yml'}}

    [703] Should change default metadata: author
    /home/fab/src/jdl-2019/jdl/meta/main.yml:1
    {'meta/main.yml': {'galaxy_info': {'author': 'your name', 'description': 'your description', 'company': 'your company (optional)', 'license': 'license (GPLv2, CC-BY, etc)', 'min_ansible_version': 1.2, 'galaxy_tags': [], '__line__': 2, '__file__': '/home/fab/src/jdl-2019/jdl/meta/main.yml'}, 'dependencies': [], '__line__': 1, '__file__': '/home/fab/src/jdl-2019/jdl/meta/main.yml'}}

    [703] Should change default metadata: description
    /home/fab/src/jdl-2019/jdl/meta/main.yml:1
    {'meta/main.yml': {'galaxy_info': {'author': 'your name', 'description': 'your description', 'company': 'your company (optional)', 'license': 'license (GPLv2, CC-BY, etc)', 'min_ansible_version': 1.2, 'galaxy_tags': [], '__line__': 2, '__file__': '/home/fab/src/jdl-2019/jdl/meta/main.yml'}, 'dependencies': [], '__line__': 1, '__file__': '/home/fab/src/jdl-2019/jdl/meta/main.yml'}}

    [703] Should change default metadata: company
    /home/fab/src/jdl-2019/jdl/meta/main.yml:1
    {'meta/main.yml': {'galaxy_info': {'author': 'your name', 'description': 'your description', 'company': 'your company (optional)', 'license': 'license (GPLv2, CC-BY, etc)', 'min_ansible_version': 1.2, 'galaxy_tags': [], '__line__': 2, '__file__': '/home/fab/src/jdl-2019/jdl/meta/main.yml'}, 'dependencies': [], '__line__': 1, '__file__': '/home/fab/src/jdl-2019/jdl/meta/main.yml'}}

    [703] Should change default metadata: license
    /home/fab/src/jdl-2019/jdl/meta/main.yml:1
    {'meta/main.yml': {'galaxy_info': {'author': 'your name', 'description': 'your description', 'company': 'your company (optional)', 'license': 'license (GPLv2, CC-BY, etc)', 'min_ansible_version': 1.2, 'galaxy_tags': [], '__line__': 2, '__file__': '/home/fab/src/jdl-2019/jdl/meta/main.yml'}, 'dependencies': [], '__line__': 1, '__file__': '/home/fab/src/jdl-2019/jdl/meta/main.yml'}}

Successful test

molecule test
--> Validating schema /home/fab/src/jdl-2019/jdl/molecule/default/molecule.yml.
Validation completed successfully.
--> Test matrix

└── default
    ├── lint
    ├── cleanup
    ├── destroy
    ├── dependency
    ├── syntax
    ├── create
    ├── prepare
    ├── converge
    ├── idempotence
    ├── side_effect
    ├── verify
    ├── cleanup
    └── destroy

--> Scenario: 'default'
--> Action: 'lint'
--> Executing Yamllint on files found in /home/fab/src/jdl-2019/jdl/...
Lint completed successfully.
--> Executing Yamllint on files found in /home/fab/src/jdl-2019/jdl/molecule/default/tests/...
Lint completed successfully.
--> Executing Ansible Lint on /home/fab/src/jdl-2019/jdl/molecule/default/playbook.yml...
Lint completed successfully.
--> Scenario: 'default'
--> Action: 'cleanup'
Skipping, cleanup playbook not configured.
--> Scenario: 'default'
--> Action: 'destroy'

    PLAY [Destroy] *****************************************************************

    TASK [Destroy molecule instance(s)] ********************************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    TASK [Wait for instance(s) deletion to complete] *******************************
    ok: [localhost] => (item=None)
    ok: [localhost]

    TASK [Delete docker network(s)] ************************************************

    PLAY RECAP *********************************************************************
    localhost                  : ok=2    changed=1    unreachable=0    failed=0


--> Scenario: 'default'
--> Action: 'dependency'
Skipping, missing the requirements file.
--> Scenario: 'default'
--> Action: 'syntax'

    playbook: /home/fab/src/jdl-2019/jdl/molecule/default/playbook.yml

--> Scenario: 'default'
--> Action: 'create'

    PLAY [Create] ******************************************************************

    TASK [Log into a Docker registry] **********************************************
    skipping: [localhost] => (item=None)

    TASK [Create Dockerfiles from image names] *************************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    TASK [Discover local Docker images] ********************************************
    ok: [localhost] => (item=None)
    ok: [localhost]

    TASK [Build an Ansible compatible image] ***************************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    TASK [Create docker network(s)] ************************************************

    TASK [Determine the CMD directives] ********************************************
    ok: [localhost] => (item=None)
    ok: [localhost]

    TASK [Create molecule instance(s)] *********************************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    TASK [Wait for instance(s) creation to complete] *******************************
    FAILED - RETRYING: Wait for instance(s) creation to complete (300 retries left).
    changed: [localhost] => (item=None)
    changed: [localhost]

    PLAY RECAP *********************************************************************
    localhost                  : ok=6    changed=4    unreachable=0    failed=0


--> Scenario: 'default'
--> Action: 'prepare'
Skipping, prepare playbook not configured.
--> Scenario: 'default'
--> Action: 'converge'

    PLAY [Converge] ****************************************************************

    TASK [Gathering Facts] *********************************************************
    ok: [instance]

    PLAY RECAP *********************************************************************
    instance                   : ok=1    changed=0    unreachable=0    failed=0


--> Scenario: 'default'
--> Action: 'idempotence'
Idempotence completed successfully.
--> Scenario: 'default'
--> Action: 'side_effect'
Skipping, side effect playbook not configured.
--> Scenario: 'default'
--> Action: 'verify'
--> Executing Goss tests found in /home/fab/src/jdl-2019/jdl/molecule/default/tests/...

    PLAY [Verify] ******************************************************************

    TASK [Gathering Facts] *********************************************************
    ok: [instance]

    TASK [Download and install Goss] ***********************************************
    changed: [instance]

    TASK [Copy Goss tests to remote] ***********************************************
    changed: [instance] => (item=/home/fab/src/jdl-2019/jdl/molecule/default/tests/test_default.yml)

    TASK [Register test files] *****************************************************
    changed: [instance]

    TASK [Execute Goss tests] ******************************************************
    changed: [instance] => (item=/tmp/test_default.yml)

    TASK [Display details about the Goss results] **********************************
    ok: [instance] => (item={'changed': True, 'end': '2019-03-20 22:08:15.317017', 'stdout': 'File: /etc/hosts: exists: matches expectation: [true]\nFile: /etc/hosts: owner: matches expectation: ["root"]\nFile: /etc/hosts: group: matches expectation: ["root"]\n\n\nTotal Duration: 0.000s\nCount: 3, Failed: 0, Skipped: 0', 'cmd': ['/usr/local/bin/goss', '-g', '/tmp/test_default.yml', 'validate', '--format', 'documentation'], 'rc': 0, 'start': '2019-03-20 22:08:14.710863', 'stderr': '', 'delta': '0:00:00.606154', 'invocation': {'module_args': {'creates': None, 'executable': None, '_uses_shell': False, '_raw_params': '/usr/local/bin/goss -g /tmp/test_default.yml validate --format documentation', 'removes': None, 'argv': None, 'warn': True, 'chdir': None, 'stdin': None}}, '_ansible_parsed': True, 'stdout_lines': ['File: /etc/hosts: exists: matches expectation: [true]', 'File: /etc/hosts: owner: matches expectation: ["root"]', 'File: /etc/hosts: group: matches expectation: ["root"]', '', '', 'Total Duration: 0.000s', 'Count: 3, Failed: 0, Skipped: 0'], 'stderr_lines': [], '_ansible_no_log': False, 'failed': False, 'item': '/tmp/test_default.yml', '_ansible_item_result': True, '_ansible_ignore_errors': None, '_ansible_item_label': '/tmp/test_default.yml'}) => {
        "msg": [
            "File: /etc/hosts: exists: matches expectation: [true]",
            "File: /etc/hosts: owner: matches expectation: [\"root\"]",
            "File: /etc/hosts: group: matches expectation: [\"root\"]",
            "",
            "",
            "Total Duration: 0.000s",
            "Count: 3, Failed: 0, Skipped: 0"
        ]
    }

    TASK [Fail when tests fail] ****************************************************
    skipping: [instance] => (item={'changed': True, 'end': '2019-03-20 22:08:15.317017', 'stdout': 'File: /etc/hosts: exists: matches expectation: [true]\nFile: /etc/hosts: owner: matches expectation: ["root"]\nFile: /etc/hosts: group: matches expectation: ["root"]\n\n\nTotal Duration: 0.000s\nCount: 3, Failed: 0, Skipped: 0', 'cmd': ['/usr/local/bin/goss', '-g', '/tmp/test_default.yml', 'validate', '--format', 'documentation'], 'rc': 0, 'start': '2019-03-20 22:08:14.710863', 'stderr': '', 'delta': '0:00:00.606154', 'invocation': {'module_args': {'creates': None, 'executable': None, '_uses_shell': False, '_raw_params': '/usr/local/bin/goss -g /tmp/test_default.yml validate --format documentation', 'removes': None, 'argv': None, 'warn': True, 'chdir': None, 'stdin': None}}, '_ansible_parsed': True, 'stdout_lines': ['File: /etc/hosts: exists: matches expectation: [true]', 'File: /etc/hosts: owner: matches expectation: ["root"]', 'File: /etc/hosts: group: matches expectation: ["root"]', '', '', 'Total Duration: 0.000s', 'Count: 3, Failed: 0, Skipped: 0'], 'stderr_lines': [], '_ansible_no_log': False, 'failed': False, 'item': '/tmp/test_default.yml', '_ansible_item_result': True, '_ansible_ignore_errors': None, '_ansible_item_label': '/tmp/test_default.yml'})

    PLAY RECAP *********************************************************************
    instance                   : ok=6    changed=4    unreachable=0    failed=0


Verifier completed successfully.
--> Scenario: 'default'
--> Action: 'cleanup'
Skipping, cleanup playbook not configured.
--> Scenario: 'default'
--> Action: 'destroy'

    PLAY [Destroy] *****************************************************************

    TASK [Destroy molecule instance(s)] ********************************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    TASK [Wait for instance(s) deletion to complete] *******************************
    changed: [localhost] => (item=None)
    changed: [localhost]

    TASK [Delete docker network(s)] ************************************************

    PLAY RECAP *********************************************************************
    localhost                  : ok=2    changed=2    unreachable=0    failed=0

Example: Kubernetes Operator SDK

Building a simple memcached-operator powered by Ansible using tools and libraries provided by the Operator SDK

Enables developers to build Operators based on their expertise without requiring knowledge of Kubernetes API complexities.

— https://coreos.com/operators/

Demo

Install Operator SDK

$ export GOPATH=$HOME/.go
$ mkdir -p $GOPATH/src/github.com/operator-framework
$ cd $GOPATH/src/github.com/operator-framework
$ git clone https://github.com/operator-framework/operator-sdk
$ cd operator-sdk
$ git checkout master
$ make dep
$ make install
$ mkvirtualenv -p /usr/bin/python3 operator-sdk
$ pip install docker molecule openshift docker

Create a new operator-sdk project

$ export PATH="$HOME/.go/bin:$PATH"
$ workon operator-sdk
$ operator-sdk new memcached-operator --api-version=cache.example.com/v1alpha1 --kind=Memcached --type=ansible
$ cd memcached-operator
$ molecule test

Molecule community update

ansible-lint and molecule are great tools. The community that we see as essential parts of enhancing development of Ansible automation has built and tested them. By adopting these tools, Red Hat intends to invest resources working with the community to make them even better.

2018-09-26
— @tima (Timothy Appnel)

Release 2.20.0

release pip
$ pip install molecule==2.20.0

Docker images on quay

release quay
$ docker pull quay.io/ansible/molecule:2.20 (1)
1Use named tags
latest == master == unstable

Changelog

release changelog

Molecule working group

Continuous integration

Docker images

No longer requires sudo.
No longer specifies USER molecule.
And more.

Lint

ansible-lint >=4.0.2,<5
Major release of ansible-lint

Backends

New backend: Linode
Customise the location of the Dockerfile.j2
New options for docker: purge_networks, pid_mode, buildargs, override_command

Python module

You can call Molecule as a Python module.
$ python -m molecule

Documentation

Lots of improvements.
New ‘Getting Started’ guide.
Example for using systemd enabled Docker images

Next: Ansible Collections

We want molecule to become the de facto standard tool, or SDK if you will, for content creators.

2019-03-13
— @thaumos (Dylan Sylva)