How to use Ansible for verifying configurations
Ansible is a handy tool for configuration management.
Sometimes, you want to verify configurations without actually changing. You can do that with Ansible assert
module.
The assert
module can evaluate Jinja2 expressions. Coupled with Ansible variable assignment, you can write Ansible playbooks to check for specific configuration parameters.
How to use Ansible assert
The assert
module accepts three main parameters.
- name: task name
ansible.builtin.assert:
that:
- "condition"
success_msg: "Success message"
fail_msg: "Fail message"
The parameter that
is a list of Jinja2 expressions.
The task prints the success_msg
if all the expressions are evaluated true or fail_msg
if any expression is false.
To build the Jinja2 expressions we can use Ansible variables.
Registering variables
An Ansible task is an invocation of an Ansible module which returns a value. The register
keyword stores this value in memory and makes it available for subsequent tasks.
- name: list files
ansible.builtin.command:
cmd: ls -k /home/ubuntu
register: cmd_ls
The list files
task invokes the module cmd
which executes the Linux command ls -k /home/ubuntu
. The task stores the return value in the variable cmd_ls
. Any task that comes below this task in the play, can refer the variable cmd_ls
.
Let’s inspect the variable using the debug
module.
- name: debug cmd_ls
ansible.builtin.debug:
var: cmd_ls
The complete playbook variable-test.yml
is available in ansible_assert GitHub repository. Clone the repo and run the playbook to check the output.
TASK [debug cmd_ls] ********************************************************************************************************************************************************************************
ok: [740-1-k8s1] => {
"cmd_ls": {
"changed": true,
"cmd": [
"ls",
"-k",
"/home/ubuntu"
],
"delta": "0:00:00.005862",
"end": "2023-06-10 15:00:20.355971",
"failed": false,
"msg": "",
"rc": 0,
"start": "2023-06-10 15:00:20.350109",
"stderr": "",
"stderr_lines": [],
"stdout": "my_file\ntest_file",
"stdout_lines": [
"my_file",
"test_file"
]
}
}
As you can see from this output, the return value of the cmd
module is a Python dictionary.
Most Ansible modules return a similar data structure. You can use the keys and valuse in this data structure for building the Jinja2 expressions for that
parameter in the assert
module.
Usecases
Let’s see some usecases for verifying configurations with Ansible assert
.
Check the existence of a file
The module stat
can retrieve status of files and directories.
This task retrieves the status of test_file
in the user’s home directory.
- name: Get status of test_file
ansible.builtin.stat:
path: /home/ubuntu/test_file
register: test_file_1
Inspect the contents of the test_file_1
variable with debug.
- name: debug test_file_1
ansible.builtin.debug:
var: test_file_1
Task output.
TASK [debug test_file_1] ***************************************************************************************************************************************************************************
ok: [740-1-k8s1] => {
"test_file_1": {
"changed": false,
"failed": false,
"stat": {
"atime": 1686121585.736978,
"attr_flags": "e",
"attributes": [
"extents"
],
"block_size": 4096,
"blocks": 8,
"charset": "us-ascii",
"checksum": "6f2170a833122bdfb8382742c2ca693a36c3ac58",
"ctime": 1685828081.3049781,
"dev": 2049,
"device_type": 0,
"executable": false,
"exists": true,
"gid": 1000,
"gr_name": "ubuntu",
"inode": 258118,
"isblk": false,
"ischr": false,
"isdir": false,
"isfifo": false,
"isgid": false,
"islnk": false,
"isreg": true,
"issock": false,
"isuid": false,
"mimetype": "text/plain",
"mode": "0664",
"mtime": 1685828081.3049781,
"nlink": 1,
"path": "/home/ubuntu/test_file",
"pw_name": "ubuntu",
"readable": true,
"rgrp": true,
"roth": true,
"rusr": true,
"size": 45,
"uid": 1000,
"version": "1457468022",
"wgrp": true,
"woth": false,
"writeable": true,
"wusr": true,
"xgrp": false,
"xoth": false,
"xusr": false
}
}
}
The stat
module returns a range of key-value pairs that represent multiple parameters about the file. Some keys are self explanatory. Refer the Stat module docs for interpretation of the others.
Here are three usefule keys and how to use them in Jinja2 expressions in assert
module.
The exists
key is a boolean representation of the existence of the file.
- name: assert test_file exists
ansible.builtin.assert:
that:
- "test_file.stat.exists"
success_msg: "OK: test_file exists"
fail_msg: "NOK: test_file does not exists"
The pw_name
key holds the username of the owner. We will check whether the value equals ubuntu
.
- name: assert test_file owner username is ubuntu
ansible.builtin.assert:
that:
- "test_file.stat.pw_name == 'ubuntu'"
success_msg: "OK: test_file owner is ubuntu"
fail_msg: "NOK: test_file owner is not ubuntu"
The gr_name
key holds the group name of owner and here’s how we check whether the owner does not belong to the root
user group.
- name: assert test_file owner group name is not root
ansible.builtin.assert:
that:
- "test_file.stat.gr_name != 'root'"
success_msg: "OK: test_file owner group is not root"
fail_msg: "NOK: test_file owner group is root"
Create the file /home/ubuntu/test_file
and run the playbook file-check.yml
in the ansible_assert to see the output.
Check the contents of a file
The Ansible module command
can run Linux commands.
Let’s run cat
command with the test_file
and inpect the contents.
- name: cat test_file
ansible.builtin.command:
cmd: cat /home/ubuntu/test_file
register: test_file
Here’s the debug output of the `test_file.
TASK [debug test_file] *****************************************************************************************************************************************************************************
ok: [740-1-k8s1] => {
"test_file": {
"changed": true,
"cmd": [
"cat",
"/home/ubuntu/test_file"
],
"delta": "0:00:00.005361",
"end": "2023-06-07 07:32:53.087623",
"failed": false,
"msg": "",
"rc": 0,
"start": "2023-06-07 07:32:53.082262",
"stderr": "",
"stderr_lines": [],
"stdout": "test_string\nnew string\nThis string is in fil",
"stdout_lines": [
"test_string",
"new string",
"This string is in fil"
]
}
}
The test_file.stdout
contains the output of the cat
command.
We can use it in Jinja2 expressions to check the contents of the file.
- name: assert line in file
ansible.builtin.assert:
that:
- "'This string is in file' in test_file.stdout"
success_msg: "OK: String is in file"
fail_msg: "NOK: String is not in file"
- name: assert line not in file
ansible.builtin.assert:
that:
- "'This string is not in file' not in test_file.stdout"
success_msg: "OK: String is not in file"
fail_msg: "NOK: String is in file
Refer file-content-check.yml
in the ansible_assert for the complete playbook.
Check the status of a service
The module ansible.builtin.service_facts
can rerieve information related to services and store them in ansible_facts.services
variable.
- name: Populate service facts
ansible.builtin.service_facts:
Let’s print the ansible_facts.services
to see what it contains.
- name: Print service facts
ansible.builtin.debug:
var: ansible_facts.services
We get a list of the services installed in the target system.
Note that a part of the output is omitted for brevity.
TASK [Print service facts] *************************************************************************************************************************************************************************
ok: [740-1-k8s1] => {
"ansible_facts.services": {
"ModemManager.service": {
"name": "ModemManager.service",
"source": "systemd",
"state": "running",
"status": "enabled"
},
"NetworkManager.service": {
"name": "NetworkManager.service",
"source": "systemd",
"state": "stopped",
"status": "not-found"
},
"accounts-daemon.service": {
"name": "accounts-daemon.service",
"source": "systemd",
"state": "running",
"status": "enabled"
},
"apparmor": {
"name": "apparmor",
"source": "sysv",
"state": "running"
},
...
Check whether apparmor
service is running.
- name: assert apparmor is running
ansible.builtin.assert:
that:
- "ansible_facts.services.apparmor.state == 'running'"
success_msg: "OK: apparmor is running"
fail_msg: "NOK: apparmor is not running"
Some service names hava a dot notation such as ssh.service
.
For accessing such keys use the Python array notations - single quoted key within square brackets.
- name: assert ssh.service is running
ansible.builtin.assert:
that:
- "ansible_facts.services['ssh.service'].state == 'running'"
success_msg: "OK: ssh.service is running"
fail_msg: "NOK: ssh.service is not running"
Run the playbook service-check.yml
in the ansible_assert to see the output.
Check the status of a kernel module
Ansible does not have a module for checking status of Linux kernel modules. But, we can use the command
module to get output of lsmod
and analyze.
- name: List loaded kernel modules
ansible.builtin.command:
cmd: "lsmod"
register: loaded_modules
Checking the ip_tables
module status.
- name: Check iptables is loaded
ansible.builtin.assert:
that:
- "'ip_tables' in loaded_modules.stdout"
success_msg: "OK: ip_tables is loaded"
fail_msg: "NOK: ip_tables is not loaded"
The command
module does not use the Linux shell to execute commands. So, it cannot interpret $
notations.
To run commands with such notations, use the shell
module.
- name: List all kernel modules
ansible.builtin.shell:
cmd: "find /lib/modules/$(uname -r) -name '*.ko*'"
register: all_modules
Run the playbook kernel-module-check.yml
in the ansible_assert to see the output.
Ignoring errors
Ansible stops execution when a task fails. That’s fine for configuring servers.
But, when we are using Ansible for verifying configurations, we need to continue even if a task evaluates to false.
So, when using Ansible for configuration verification set ignore errors
to true in the playbook.
ignore_errors: true
Ansible can do many things
Ansible can do things other than configuring servers. We just used Ansible for verifying configurations. These configuration verification capabilities in Ansible are handy for automating security compliance checking and audits.
In future posts, let’s explore more usecases of Ansible.