Using Ansible to verify configurations
Ansible is a great tool for configuring servers to the state you desire. You can create a playbook, and if correctly written, it always yields the same state no matter how many times you run it. This is called idempotency. You can also run an Ansible playbook with the --check
option and verify what the playbook would change if it were run so that you don't unexpectedly make a change when you do not want to.
But what if you need to verify the configuration of existing servers that were not originally configured with Ansible? There are tools out there for this purpose, such as Chef's InSpec, testinfra, and serverspec. But, if you already know Ansible, you can use some of its built-in functionality to do this.
Example 1
In this example, I have six hosts of various Linux distributions and versions. Because the actual memory reported can vary, I'm going to check for memory between 800 MB and 1100 MB. The goal is for hosts that meet the criteria to pass with changed=0
and hosts that fail the criteria to output changed=1
:
- name: assert
hosts: all
tasks:
- name: check if memory is between 800 and 1100MB
assert:
that:
- ansible_memtotal_mb | int >= 800
- ansible_memtotal_mb | int <= 1100
fail_msg: "Memory is {{ ansible_memtotal_mb }}MB not 1024MB"
register: result
changed_when:
- result.evaluated_to is defined
- result.evaluated_to == False
failed_when: False
The results:
[kpirkle@defiant config-verify]$ ansible-playbook assert.yml
PLAY [assert] ****************************************************************************************************************************************************************************************************
TASK [Gathering Facts] *******************************************************************************************************************************************************************************************
ok: [wookie]
ok: [ewok]
ok: [centurion]
ok: [web]
ok: [venture]
ok: [c3po]
TASK [check if memory is between 800 and 1100MB] *****************************************************************************************************************************************************************
ok: [venture] => {
"changed": false,
"failed_when_result": false,
"msg": "All assertions passed"
}
ok: [ewok] => {
"changed": false,
"failed_when_result": false,
"msg": "All assertions passed"
}
ok: [web] => {
"changed": false,
"failed_when_result": false,
"msg": "All assertions passed"
}
changed: [centurion] => {
"assertion": "ansible_memtotal_mb | int <= 1100",
"changed": true,
"evaluated_to": false,
"failed_when_result": false,
"msg": "Memory is 7737MB not 1024MB"
}
ok: [wookie] => {
"changed": false,
"failed_when_result": false,
"msg": "All assertions passed"
}
ok: [c3po] => {
"changed": false,
"failed_when_result": false,
"msg": "All assertions passed"
}
PLAY RECAP *******************************************************************************************************************************************************************************************************
c3po : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
centurion : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ewok : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
venture : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
wookie : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
assert
The function of the assert
module, per the documentation, is to "assert given expressions are true."
In this case, five of the six passed - one failed because host centurion had more memory than the range I was checking. I used the fail_msg
option of the assert
module to provide useful information as to why it failed. There are also options to always display a custom message with msg
and to pass a custom message when the assertion passes called success_msg
.
changed_when
The changed_when
option is not a module but rather a built-in function that alters how error handling is done for a task. In this case, we are saving the results from the assert
test to the result
variable. We then alter the changed
value based on two tests: 1) does the evaluated_to
variable exist, and 2) is it false? If it exists and is false, we know the test failed, and we return the result as "changed."
failed_when
To prevent the play from failing and ending the run of the playbook, set the failed_when
option to false.
Example 2
Another useful configuration comparison strategy is to use a checksum tool like md5sum
to compare files.
---
- name: compare
hosts: all
gather_facts: no
vars:
std_conf_crontab: 'c39252b11aad842fcb75e05c6a27eef8'
std_conf_lvm: '2d90187abd40dbcb6fc6de41640fd022'
std_conf_resolv: 'db323688118c844a76ebd6c70508b434'
tasks:
- name: compare config files
stat:
path: '{{ item.file }}'
checksum_algorithm: md5
register: result
changed_when: item.md5sum != result.stat.checksum
failed_when: False
loop:
- { file: /etc/crontab, md5sum: '{{ std_conf_crontab }}' }
- { file: /etc/lvm/lvm.conf, md5sum: '{{ std_conf_lvm }}' }
- { file: /etc/resolv.conf, md5sum: '{{ std_conf_resolv }}'}
The result:
[kpirkle@defiant config-verify]$ ansible-playbook compare.yml
PLAY [compare] ****************************************************************************************************************************
TASK [compare config files] ***************************************************************************************************************
changed: [wookie] => (item={'file': '/etc/crontab', 'md5sum': 'c39252b11aad842fcb75e05c6a27eef8'})
ok: [centurion] => (item={'file': '/etc/crontab', 'md5sum': 'c39252b11aad842fcb75e05c6a27eef8'})
ok: [web] => (item={'file': '/etc/crontab', 'md5sum': 'c39252b11aad842fcb75e05c6a27eef8'})
changed: [wookie] => (item={'file': '/etc/lvm/lvm.conf', 'md5sum': '2d90187abd40dbcb6fc6de41640fd022'})
ok: [centurion] => (item={'file': '/etc/lvm/lvm.conf', 'md5sum': '2d90187abd40dbcb6fc6de41640fd022'})
ok: [venture] => (item={'file': '/etc/crontab', 'md5sum': 'c39252b11aad842fcb75e05c6a27eef8'})
ok: [wookie] => (item={'file': '/etc/resolv.conf', 'md5sum': 'db323688118c844a76ebd6c70508b434'})
ok: [ewok] => (item={'file': '/etc/crontab', 'md5sum': 'c39252b11aad842fcb75e05c6a27eef8'})
ok: [centurion] => (item={'file': '/etc/resolv.conf', 'md5sum': 'db323688118c844a76ebd6c70508b434'})
ok: [web] => (item={'file': '/etc/lvm/lvm.conf', 'md5sum': '2d90187abd40dbcb6fc6de41640fd022'})
changed: [venture] => (item={'file': '/etc/lvm/lvm.conf', 'md5sum': '2d90187abd40dbcb6fc6de41640fd022'})
ok: [web] => (item={'file': '/etc/resolv.conf', 'md5sum': 'db323688118c844a76ebd6c70508b434'})
changed: [ewok] => (item={'file': '/etc/lvm/lvm.conf', 'md5sum': '2d90187abd40dbcb6fc6de41640fd022'})
ok: [ewok] => (item={'file': '/etc/resolv.conf', 'md5sum': 'db323688118c844a76ebd6c70508b434'})
ok: [venture] => (item={'file': '/etc/resolv.conf', 'md5sum': 'db323688118c844a76ebd6c70508b434'})
ok: [c3po] => (item={'file': '/etc/crontab', 'md5sum': 'c39252b11aad842fcb75e05c6a27eef8'})
changed: [c3po] => (item={'file': '/etc/lvm/lvm.conf', 'md5sum': '2d90187abd40dbcb6fc6de41640fd022'})
ok: [c3po] => (item={'file': '/etc/resolv.conf', 'md5sum': 'db323688118c844a76ebd6c70508b434'})
PLAY RECAP ********************************************************************************************************************************
c3po : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
centurion : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ewok : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
venture : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
wookie : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
In this example, we are setting variables for the md5sums
of three files that we want to check for consistency. Our task then uses the stat
module to calculate the MD5 checksum of each file and compare it to the defined variable. Once again, we are using the changed_when
function to make the comparison and render a "changed" state when there isn't a match.
Example 3
My final example is a way to check the contents of a file for a specific item. One way to do this is by using the shell
module and using grep
.
---
- name: grep
hosts: all
gather_facts: no
tasks:
- name: grep for nameserver
shell: grep 'nameserver 192.168.0.1' /etc/resolv.conf
register: result
changed_when: result.rc != 0
failed_when: False
The result:
[kpirkle@defiant config-verify]$ ansible-playbook grep.yml
PLAY [grep] *******************************************************************************************************************************
TASK [grep for nameserver] ****************************************************************************************************************
ok: [wookie]
ok: [web]
ok: [centurion]
ok: [ewok]
changed: [venture]
ok: [c3po]
PLAY RECAP ********************************************************************************************************************************
c3po : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
centurion : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ewok : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
venture : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
wookie : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Here again, we are using the combination of changed_when
and failed_when
to manipulate the output to be useful. By registering the output of grep
to the result
variable, we can then check the return code of the command and find out if the desired nameserver
string is present. More complex commands can be used if needed.
Wrap up
Hopefully, others find these tips useful. You can use Ansible as not only a tool to configure servers to the desired state but also as an investigative tool to verify the configuration of servers or possibly as an ad-hoc monitoring tool.
[ Need more on Ansible? Take a free technical overview course from Red Hat. Ansible Essentials: Simplicity in Automation Technical Overview. ]
Kent Pirkle
Kent is a Linux Systems Engineer with over 20 years of experience with Linux and UNIX systems. His current focus is on Ansible, automation, and infrastructure-as-code. He is a member of the Red Hat Accelerators and is a Red Hat Certified Engineer. More about me