Ansible role testing — Molecule

Lets have a look at Molecule 1.x and what it can do

Ansible role testing — Molecule

When i started working on a new project, the choice of configuration management was Ansible and off I went to get to grips with it. Writing roles seemed easy but how do you test ansible roles…Molecule.

Molecule allows you to run with a few different providers locally to test that your roles carries out the desired changes and has a two or three other features.

This project on has 2 linux instances and the rest are solely windows server so it was a bit different to what i was used to. what follows will be one example of how to get molecule running for a linux instance and also one for a windows instance.

Linux.

With linux images if you have a Mac you can run docker images, this is the easiest of options, if not you can revert to virtualbox.

So lets start, for this blog post ill use docker for linux and virtualbox for windows.

Pre requisite

Download docker and install it.

Lets install molecule

pip install molecule

Now molecule is installed, it will also install any dependencies

Change into the directory of your role, this is where all the magic happens.

Now its time to start setting up molecule

molecule init --driver docker

This will create a few extra files/directories.

molecule.yml | configuration file for test framework

playbook.yml | playbopok for executing the role in docker

tests/test_default.py | test infra unit test cases or server spec.

Ill put below the molecule.yml for my linux role tests.

---
dependency:
  name: galaxy
driver:
  name: docker
docker:
  containers:
    - name: ansible-jenkins
      image: geerlingguy/docker-centos7-ansible
      image_version: latest
      privileged: true
#      command: docker run --detach --privileged --volume=/sys/fs/cgroup:/sys/fs/cgroup:ro geerlingguy/docker-centos7-ansible:latest /usr/lib/systemd/systemd
      cap_add:
        - SYS_ADMIN
        - MKNOD
      ansible_groups:
        - group1
verifier:
  name: testinfra

ansible:
  group_vars:
    group1:
      dev_environment: devtest
      jenkins_jobs:
        - Run_ansible_custom
        - Deploy_terraform
      gitlab_external_servername: gitlab.test
      gitlab_external_url: "https://{{ gitlab_external_servername }}"

dependency: This is a dependency on ansible-galaxy for this role

driver: This says it is using docker as the driver for the images

docker: This is where you can configure all the options (image name etc) of the docker image. Along with any extra capabilities and the group vars name you will be using in your ansible role tests.

ansible: Here you will list any group_vars that do not have adefault. These can all be test vars.

When you have this you will be able to see if molecule spins up the docker image. This can be done by running the following command.

molecule create

This will only create the docker image and will NOT run ansible. Output looks like this:

molecule create
--> Creating instances...
--> Creating Ansible compatible image of geerlingguy/docker-centos7-ansible:latest ...
--> Starting container ansible-jenkins...

You can see it is getting the docker image we specified and just spinning it up locally.

If you now want to see if your ansible role runs on the docker image you can run

molecule converge

Converge basically runs both create of the docker image and runs ansible and should provide you with output like you had just run your role on a server.

molecule converge
--> Starting Ansible Run...
 [WARNING]: Found variable using reserved name: port

PLAY [all] *********************************************************************

TASK [Gathering Facts] *********************************************************
ok: [ansible-jenkins]

TASK [ansible-jenkins : Install python-pycurl] *********************************
ok: [ansible-jenkins]

TASK [ansible-jenkins : Import jenkins key] ************************************
ok: [ansible-jenkins]

TASK [ansible-jenkins : Get jenkins repo for ansible] **************************
ok: [ansible-jenkins]

TASK [ansible-jenkins : Install dependencies] **********************************
ok: [ansible-jenkins] => (item=[u'libselinux-python', u'java', u'git', u'curl'])

TASK [ansible-jenkins : Install Jenkins] ***************************************
changed: [ansible-jenkins]

TASK [ansible-jenkins : Restart jenkins now] ***********************************
ok: [ansible-jenkins]

TASK [ansible-jenkins : Ensure that jenkins starts on boot] ********************
ok: [ansible-jenkins]

TASK [ansible-jenkins : Create jenkins configuration] **************************
ok: [ansible-jenkins]

TASK [ansible-jenkins : Configure Jenkins Port] ********************************
ok: [ansible-jenkins]

TASK [ansible-jenkins : Restart jenkins now] ***********************************
skipping: [ansible-jenkins]

TASK [ansible-jenkins : Configure Jenkins Prefix] ******************************
ok: [ansible-jenkins]

TASK [ansible-jenkins : Restart jenkins now] ***********************************
skipping: [ansible-jenkins]

TASK [ansible-jenkins : 10s delay while starting Jenkins] **********************
ok: [ansible-jenkins]

TASK [ansible-jenkins : Create Jenkins CLI destination directory: /opt/jenkins] ***
ok: [ansible-jenkins]

TASK [ansible-jenkins : Get Jenkins CLI] ***************************************
ok: [ansible-jenkins]

TASK [ansible-jenkins : template] **********************************************
skipping: [ansible-jenkins]

TASK [ansible-jenkins : add proxy settings configuration] **********************
skipping: [ansible-jenkins]

TASK [ansible-jenkins : Create job directories] ********************************
ok: [ansible-jenkins] => (item=Run_ansible_custom)
ok: [ansible-jenkins] => (item=Deploy_terraform)

TASK [ansible-jenkins : Create job configurations] *****************************
ok: [ansible-jenkins] => (item=Run_ansible_custom)
ok: [ansible-jenkins] => (item=Deploy_terraform)

TASK [ansible-jenkins : Get Jenkins CLI] ***************************************
ok: [ansible-jenkins]

TASK [ansible-jenkins : Restart jenkins now last] ******************************
ok: [ansible-jenkins]

PLAY RECAP *********************************************************************
ansible-jenkins            : ok=18   changed=1    unreachable=0    failed=0

This shows the role has finished running and changes were made as expected. The next thing we will check is if the ansible role is idempotent. This can be done with the idempotence option

molecule idempotence
--> Idempotence test in progress (can take a few minutes)...
--> Starting Ansible Run...
Idempotence test passed.

As you can see the role is idempotent, if it was not it would show exactly the tasks that are not.

and lastly you can write assertions to test the infrastructure is as expected. This is an example of 2 or 3 simple tests

import testinfra.utils.ansible_runner

testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
    '.molecule/ansible_inventory').get_hosts('all')

def test_jenkins_package(Package):
    jenkinsservice = Package('jenkins.noarch')
    assert jenkinsservice.is_installed
    assert jenkinsservice.version.startswith("2.54")


def test_jenkins_running_and_enabled(Service):
    jenkinsservice = Service("jenkins.service")
    assert jenkinsservice.is_running
    assert jenkinsservice.is_enabled

The above tests just check to see whether jenkins is installed and if it is running as a service. But you can write others as you wish. You can run these tests by running molecule with the options test. This will destroy the instance and start everything on a clean slate with the output from start to finish

molecule test
--> Destroying instances...
Stopping container ansible-jenkins...
Removed container ansible-jenkins.
--> Checking playbook's syntax...

playbook: playbook.yml
--> Creating instances...
--> Creating Ansible compatible image of geerlingguy/docker-centos7-ansible:latest ...
Creating container ansible-jenkins with base image geerlingguy/docker-centos7-ansible:latest...
Container created.
--> Starting Ansible Run...
 [WARNING]: Found variable using reserved name: port

PLAY [all] *********************************************************************

TASK [Gathering Facts] *********************************************************
ok: [ansible-jenkins]

TASK [ansible-jenkins : Install python-pycurl] *********************************
ok: [ansible-jenkins]

TASK [ansible-jenkins : Import jenkins key] ************************************
changed: [ansible-jenkins]

TASK [ansible-jenkins : Get jenkins repo for ansible] **************************
changed: [ansible-jenkins]

TASK [ansible-jenkins : Install dependencies] **********************************
changed: [ansible-jenkins] => (item=[u'libselinux-python', u'java', u'git', u'curl'])

TASK [ansible-jenkins : Install Jenkins] ***************************************
changed: [ansible-jenkins]

TASK [ansible-jenkins : Restart jenkins now] ***********************************
ok: [ansible-jenkins]

TASK [ansible-jenkins : Ensure that jenkins starts on boot] ********************
ok: [ansible-jenkins]

TASK [ansible-jenkins : Create jenkins configuration] **************************
ok: [ansible-jenkins]

TASK [ansible-jenkins : Configure Jenkins Port] ********************************
--- before: /etc/sysconfig/jenkins (content)
+++ after: /etc/sysconfig/jenkins (content)
@@ -53,7 +53,7 @@
 # Port Jenkins is listening on.
 # Set to -1 to disable
 #
-JENKINS_PORT="8080"
+JENKINS_PORT=8080

## Type:        string
 ## Default:     ""

changed: [ansible-jenkins]

TASK [ansible-jenkins : Restart jenkins now] ***********************************
changed: [ansible-jenkins]

TASK [ansible-jenkins : Configure Jenkins Prefix] ******************************
--- before: /etc/sysconfig/jenkins (content)
+++ after: /etc/sysconfig/jenkins (content)
@@ -142,3 +142,4 @@
 # Full option list: java -jar jenkins.war --help
 #
 JENKINS_ARGS=""
+PREFIX=/

changed: [ansible-jenkins]

TASK [ansible-jenkins : Restart jenkins now] ***********************************
changed: [ansible-jenkins]

TASK [ansible-jenkins : 10s delay while starting Jenkins] **********************
ok: [ansible-jenkins]

TASK [ansible-jenkins : Create Jenkins CLI destination directory: /opt/jenkins] ***
--- before
+++ after
@@ -1,4 +1,4 @@
 {
     "path": "/opt/jenkins",
-    "state": "absent"
+    "state": "directory"
 }

changed: [ansible-jenkins]

TASK [ansible-jenkins : Get Jenkins CLI] ***************************************
FAILED - RETRYING: Get Jenkins CLI (5 retries left).
changed: [ansible-jenkins]

TASK [ansible-jenkins : template] **********************************************
skipping: [ansible-jenkins]

TASK [ansible-jenkins : add proxy settings configuration] **********************
skipping: [ansible-jenkins]

TASK [ansible-jenkins : Create job directories] ********************************
--- before
+++ after
@@ -1,6 +1,6 @@
 {
-    "group": 0,
-    "owner": 0,
+    "group": 996,
+    "owner": 998,
     "path": "/var/lib/jenkins/jobs/Run_ansible_custom",
-    "state": "absent"
+    "state": "directory"
 }

changed: [ansible-jenkins] => (item=Run_ansible_custom)
--- before
+++ after
@@ -1,6 +1,6 @@
 {
-    "group": 0,
-    "owner": 0,
+    "group": 996,
+    "owner": 998,
     "path": "/var/lib/jenkins/jobs/Deploy_terraform",
-    "state": "absent"
+    "state": "directory"
 }

changed: [ansible-jenkins] => (item=Deploy_terraform)

TASK [ansible-jenkins : Create job configurations] *****************************
--- before
+++ after: dynamically generated
@@ -0,0 +1,37 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<flow-definition plugin="[email protected]">
+  <actions/>
+  <description></description>
+  <keepDependencies>false</keepDependencies>
+  <properties>
+    <org.jenkinsci.plugins.workflow.job.properties.PipelineTriggersJobProperty>
+      <triggers>
+        <hudson.triggers.SCMTrigger>
+          <spec>* * * * *</spec>
+          <ignorePostCommitHooks>false</ignorePostCommitHooks>
+        </hudson.triggers.SCMTrigger>
+      </triggers>
+    </org.jenkinsci.plugins.workflow.job.properties.PipelineTriggersJobProperty>
+  </properties>
+  <definition class="org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition" plugin="[email protected]">
+    <scm class="hudson.plugins.git.GitSCM" plugin="[email protected]">
+      <configVersion>2</configVersion>
+      <userRemoteConfigs>
+        <hudson.plugins.git.UserRemoteConfig>
+          <url>git@git:oops/automation.git</url>
+        </hudson.plugins.git.UserRemoteConfig>
+      </userRemoteConfigs>
+      <branches>
+        <hudson.plugins.git.BranchSpec>
+          <name>*/master</name>
+        </hudson.plugins.git.BranchSpec>
+      </branches>
+      <doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
+      <submoduleCfg class="list"/>
+      <extensions/>
+    </scm>
+    <scriptPath>ansible/Jenkinsfile_run_ansible_custom</scriptPath>
+    <lightweight>true</lightweight>
+  </definition>
+  <triggers/>
+</flow-definition>

changed: [ansible-jenkins] => (item=Run_ansible_custom)
--- before
+++ after: dynamically generated
@@ -0,0 +1,37 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<flow-definition plugin="[email protected]">
+  <actions/>
+  <description></description>
+  <keepDependencies>false</keepDependencies>
+  <properties>
+    <org.jenkinsci.plugins.workflow.job.properties.PipelineTriggersJobProperty>
+      <triggers>
+        <hudson.triggers.SCMTrigger>
+          <spec>* * * * *</spec>
+          <ignorePostCommitHooks>false</ignorePostCommitHooks>
+        </hudson.triggers.SCMTrigger>
+      </triggers>
+    </org.jenkinsci.plugins.workflow.job.properties.PipelineTriggersJobProperty>
+  </properties>
+  <definition class="org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition" plugin="[email protected]">
+    <scm class="hudson.plugins.git.GitSCM" plugin="[email protected]">
+      <configVersion>2</configVersion>
+      <userRemoteConfigs>
+        <hudson.plugins.git.UserRemoteConfig>
+          <url>git@git:oops/automation.git</url>
+        </hudson.plugins.git.UserRemoteConfig>
+      </userRemoteConfigs>
+      <branches>
+        <hudson.plugins.git.BranchSpec>
+          <name>*/master</name>
+        </hudson.plugins.git.BranchSpec>
+      </branches>
+      <doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
+      <submoduleCfg class="list"/>
+      <extensions/>
+    </scm>
+    <scriptPath>terraform/Jenkinsfile_devtest</scriptPath>
+    <lightweight>true</lightweight>
+  </definition>
+  <triggers/>
+</flow-definition>

changed: [ansible-jenkins] => (item=Deploy_terraform)

TASK [ansible-jenkins : Get Jenkins CLI] ***************************************
changed: [ansible-jenkins]

TASK [ansible-jenkins : Restart jenkins now last] ******************************
ok: [ansible-jenkins]

PLAY RECAP *********************************************************************
ansible-jenkins            : ok=20   changed=13   unreachable=0    failed=0

--> Idempotence test in progress (can take a few minutes)...
--> Starting Ansible Run...
Idempotence test passed.
--> Executing ansible-lint...
[ANSIBLE0016] Tasks that run when changed should likely be handlers
/Users/simonga/PycharmProjects/oops/automation/ansible/roles/ansible-jenkins/tasks/cli.yml:2
Task/Handler: {{ startup_delay_s | default(10) }}s delay while starting Jenkins

[ANSIBLE0016] Tasks that run when changed should likely be handlers
/Users/simonga/PycharmProjects/oops/automation/ansible/roles/ansible-jenkins/tasks/config.yml:27
Task/Handler: Restart jenkins now

[ANSIBLE0016] Tasks that run when changed should likely be handlers
/Users/simonga/PycharmProjects/oops/automation/ansible/roles/ansible-jenkins/tasks/config.yml:37
Task/Handler: Restart jenkins now

[ANSIBLE0010] Package installs should not use latest
/Users/simonga/PycharmProjects/oops/automation/ansible/roles/ansible-jenkins/tasks/jenkins.yml:7
Task/Handler: Install Jenkins

[ANSIBLE0002] Trailing whitespace
/Users/simonga/PycharmProjects/oops/automation/ansible/roles/ansible-jenkins/tasks/main.yml:2
- include: jenkins.yml

[ANSIBLE0011] All tasks should be named
/Users/simonga/PycharmProjects/oops/automation/ansible/roles/ansible-jenkins/tasks/plugins.yml:2
Task/Handler: template src=proxy.groovy dest={{ jenkins_dest }}/proxy.groovy __file__=/Users/simonga/PycharmProjects/oops/automation/ansible/roles/ansible-jenkins/tasks/plugins.yml __line__=2

And that is all you need for the linux set up.

Windows

Windows was abit more tricky. You can not use windows docker images on OS X as they are not cross platform compatible.

So you need to do a little bit of a workaround and this would be using vagrant to provision a windows machine in Virtualbox. which in turn molecule can communicate with.

Pre Requisites.

Applications:

  • Virtualbox
  • Vagrant
  • Molecule

Libraries

  • pywinrm
  • requests
  • pyopenssl
  • python-vagrant
  • libvirt-python

To install these do the following

pip install pywinrm requests pyopenssl python-vagrant

brew install libvirt-python

vagrant plugin install vagrant-winrm vagrant-vbguest vagrant-libvirt

Now on to the fun bit. I recommend downloading the image of windows you want before hand as they are quite large, the one I was using is 12GB and it took a while. Download it with vagrant as below:

vagrant box add atomia/windows-2012r2

This will downlaod the box locally. Now we go to the fun bit

Lets start with getting molecule directories up inside the directory of the role we would like to test

molecule init --driver vagrant

As you can see we are stating the driver as vagrant even though vagrant will be a wrapper for virtual box and molecule will be a wrapper for vagrant.

You should have the same files as with the linux tutorial

molecule.yml | configuration file for test framework

playbook.yml | playbopok for executing the role in docker

tests/test_default.py | test infra unit test cases or server spec.

Lets take a look at the *molecule.yml* file

---
dependency:
  name: galaxy
driver:
  name: vagrant
vagrant:
#Config options not supported by Molecule but can still be injected
  raw_config_args:
    - "vm.communicator = 'winrm'"
    - "vm.guest = :windows"
    - "winrm.username = 'vagrant'"
    - "winrm.password = 'vagrant'"
    - "vbguest.auto_update = false"
    - "winrm.timeout = 120"
    - "winrm.retry_limit = 5"
  platforms:
    - name: name-of-vm
      box: windows_server-2012r2-standard-amd64-nocm-ansible.box
      box_url: https://10.43.72.90:8081/windows_server-2012r2-standard-amd64-nocm/versions/1.6.0/providers/virtualbox.box
  providers:
    - name: virtualbox
      type: virtualbox
      options:
        memory: 2048
        cpus: 2
  instances:
    - name: AppyApp
      ansible_groups:
        - group1

verifier:
  name: serverspec

ansible:
  sudo: False
  connection: winrm
  group_vars:
    group1:
      ansible_user: vagrant
      ansible_password: vagrant

#Port is required for ansible to use winrm port and not default to ssh
      ansible_ssh_port: 55986
      ansible_connection: winrm
      ansible_winrm_server_cert_validation: ignore

      jump1_ip: 192.168.0.6
      integration1_ip: 192.168.0.6

      Vars_For_App4:
        Url:  "http://{{ integration1_ip }}/random"
        User: Test123
        UserPassword: Test123==
        Domain: test-1
        InstanceNames:
          - Test01
          - Test02
        SMTPServer: TODO
        SMTPUser: TODO
        SMTPPassword: TODO
        EmailTo: [email protected]
        EmailFrom: [email protected]
        EmailMinMinutes: 5
        KeepLogDays: 7

      static_hosts:
        web-1:
          ip: 192.168.0.6
        integration-1:
          ip: "{{ integration1_ip }}"
        jenkins-1:
          ip: 192.168.0.6
        vm-1:
          ip: 10.43.72.14
      Vars_For_App3:
        ProcessMinutes: 30

      Vars_For_App2:
        StaggerDelaySeconds: 15

      Vars_For_App:
        Number01:
          Instance: Test01

        Number02:
          Instance: Test02

A little walk through the above config file for molecule

dependency:
  name: galaxy
driver:
  name: vagrant
vagrant:
#Config options not supported by Molecule but can still be injected
  raw_config_args:
    - "vm.communicator = 'winrm'"
    - "vm.guest = :windows"
    - "winrm.username = 'vagrant'"
    - "winrm.password = 'vagrant'"
    - "vbguest.auto_update = false"
    - "winrm.timeout = 120"
    - "winrm.retry_limit = 5"

The driver this time is vagrant and below you can see “raw_config_args”, molecule supports some args for vagrant/virtualbox but the ones it does not support can be hard coded at the top. Here I am just telling vagrant that it should communicate with the instance with winrm and that is a windows machine. The args below are more for tweaking and not necessary unless you have issues.

Below that you shall see configuration for the actual virtual machine it will spin up in VirtualBox

platforms:
    - name: name-of-vm
      box: windows_server-2012r2-standard-amd64-nocm-ansible.box
      box_url: https://10.43.72.90:8081/windows_server-2012r2-standard-amd64-nocm/versions/1.6.0/providers/virtualbox.box
  providers:
    - name: virtualbox
      type: virtualbox
      options:
        memory: 2048
        cpus: 2

Name: The name you want to give your VM

Box: the name of the box if it is hosted on vagrants library and if not then below you can place the URL to the box.

Providers: is just stating that you will be using virtualbox along with hardware options (memory and cpu)

In the ansible section there is a number of important options:

ansible:
  sudo: False
  connection: winrm
  group_vars:
    group1:
      ansible_user: vagrant
      ansible_password: vagrant

#Port is required for ansible to use winrm port and not default to ssh
      ansible_ssh_port: 55986
      ansible_connection: winrm
      ansible_winrm_server_cert_validation: ignore

For ansible to use winrm over the correct port you need to tell it what port it is, and you can only do this by using the arg “ansible_ssh_port”, if you do not do this it will default to ssh port 22.

55986 is the port forward that Virtualbox sets up locally when it creates the instance, (https = 5986, http = 5985). If you are using a version of python newer than 2.7.9 then you need to have the ignore certificate config as it will throw 500 errors other wise.

The options for molecule in windows are exactly the same, the only difference is that it spins up the machine in virtualbox. There is a bit of set up you need to do to the windows box to allow it for ansible use and this can be found on the ansible documentation. For this run a molecule create to just create the instance, when done use virtualbox to get the server gui up. Download the script and run the commands listed. When done export the image from virtualbox and reimport to vagrant.

vagrant package --base 8cb6994d-2b6b-42bb-a51b-0b00ce074ad9 --output windows_server-2012r2-standard-amd64-nocm-ansible.box

vagrant box add ./windows_server-2012r2-standard-amd64-nocm-ansible.box --name windows_server-2012r2-standard-amd64-nocm-ansible

— base is the name of the machine in virtualbox and can be found by running VBoxManage list vms, and output is what you want it to be called.

With this done you should be able to run the same commands as we did with the linux machine.

Example of a molecule test for the windows VM is below.

molecule test                             
--> Destroying instances...
==> test-services: Forcing shutdown of VM...
==> test-services: Destroying VM and associated drives...
WARNING: Nokogiri was built against LibXML version 2.9.0, but has dynamically loaded 2.9.2
--> Checking playbook's syntax...

playbook: playbook.yml
--> Creating instances...
Bringing machine 'test-services' up with 'virtualbox' provider...
==> test-services: Cloning VM...
==> test-services: Matching MAC address for NAT networking...
==> test-services: Setting the name of the VM: test-services_1493043969336_4686
WARNING: Nokogiri was built against LibXML version 2.9.0, but has dynamically loaded 2.9.2
==> test-services: Clearing any previously set network interfaces...
==> test-services: Preparing network interfaces based on configuration...
    test-services: Adapter 1: nat
==> test-services: Forwarding ports...
    test-services: 5985 (guest) => 55985 (host) (adapter 1)
    test-services: 5986 (guest) => 55986 (host) (adapter 1)
    test-services: 22 (guest) => 2222 (host) (adapter 1)
==> test-services: Running 'pre-boot' VM customizations...
==> test-services: Booting VM...
==> test-services: Waiting for machine to boot. This may take a few minutes...
    test-services: WinRM address: 127.0.0.1:55985
    test-services: WinRM username: vagrant
    test-services: WinRM execution_time_limit: PT2H
    test-services: WinRM transport: negotiate
==> test-services: Machine booted and ready!
==> test-services: Checking for guest additions in VM...
==> test-services: Setting hostname...
==> test-services: Mounting shared folders...
    test-services: /vagrant => /Users/simonga/PycharmProjects/oops/automation/ansible/roles/test-services
    test-services: /tmp/vagrant-cache => /Users/simonga/PycharmProjects/oops/automation/ansible/roles/test-services/.vagrant/machines/test-services/cache
==> test-services: Machine not provisioned because `--no-provision` is specified.
--> Starting Ansible Run...

PLAY [all] *********************************************************************

TASK [Gathering Facts] *********************************************************
ok: [test-services]

TASK [test-services : Create temporary directory structure for install scripts] ***
changed: [test-services]

TASK [test-services : Download Installer files] *****************************
changed: [test-services] => (item=test24.zip)

TASK [test-services : Unzip Installers] *************************************
changed: [test-services]

TASK [test-services : Install Test1 Service] *****************************
changed: [test-services]

TASK [test-services : Install Test2 Service] *******************************
changed: [test-services]

TASK [test-services : Copy Test3.exe to install folder] ********************
changed: [test-services]

TASK [test-services : Generate test.xml file] ************
changed: [test-services]

TASK [test-services : Generate test3.xml file for Payments] ************
changed: [test-services]

TASK [test-services : Generate test4.xml file] ************
changed: [test-services]

TASK [test-services : Generate test5.xml file for Events] **************
changed: [test-services]

PLAY RECAP *********************************************************************
test-services           : ok=11   changed=10   unreachable=0    failed=0

--> Idempotence test in progress (can take a few minutes)...
--> Starting Ansible Run...
Idempotence test passed.
--> Executing ansible-lint...
[ANSIBLE0002] Trailing whitespace
/Users/simonga/PycharmProjects/oops/automation/ansible/roles/test-services/tasks/main.yml:66
  template:

[ANSIBLE0002] Trailing whitespace
/Users/simonga/PycharmProjects/oops/automation/ansible/roles/test-services/tasks/main.yml:67
    src: test.xml.j2

[ANSIBLE0002] Trailing whitespace
/Users/simonga/PycharmProjects/oops/automation/ansible/roles/test-services/tasks/main.yml:71
  template:

[ANSIBLE0002] Trailing whitespace
/Users/simonga/PycharmProjects/oops/automation/ansible/roles/test-services/tasks/main.yml:72
    src: test.xml.j2

[ANSIBLE0002] Trailing whitespace
/Users/simonga/PycharmProjects/oops/automation/ansible/roles/test-services/tasks/main.yml:76
  template:

[ANSIBLE0002] Trailing whitespace
/Users/simonga/PycharmProjects/oops/automation/ansible/roles/test-services/tasks/main.yml:77
    src: test1.xml.j2

[ANSIBLE0002] Trailing whitespace
/Users/simonga/PycharmProjects/oops/automation/ansible/roles/test-services/tasks/main.yml:81
  template:

[ANSIBLE0002] Trailing whitespace
/Users/simonga/PycharmProjects/oops/automation/ansible/roles/test-services/tasks/main.yml:82
    src: test.xml.j2