Within an Ansible playbook I wanted to retrieve the root partitions size and use this as a variable for a later task. What seemed to be an easy task, turned out to be a bigger head-scratcher than I thought. But let's start at the beginning.
When gathering facts with Ansible, the "ansible_mounts" array contains the mounted file systems of the target machine:
ck@ansible:/pub/ansible$ ansible -m setup target
[...]
"ansible_mounts": [
{
"block_available": 1211001,
"block_size": 4096,
"block_total": 2554693,
"block_used": 1343692,
"device": "/dev/vgdata/target",
"fstype": "ext4",
"inode_available": 551110,
"inode_total": 655360,
"inode_used": 104250,
"mount": "/",
"options": "rw,relatime",
"size_available": 4960260096,
"size_total": 10464022528,
"uuid": "N/A"
}
],
[...]
The "size_total" represents the total size of the file system, which can also be seen and verified with df on the target machine:
root@target:~# df -B1 /
Filesystem Type 1B-blocks Used Available Use% Mounted on
/dev/vgdata/target ext4 10464022528 4964831232 4945543168 51% /
As "ansible_mounts" is an array which may contain multiple file systems, I needed to make sure to only select the root partition, mounted on /, of the target machine. This can be done by using the json_query filter:
- name: FACT - Retrieve root disk size
set_fact:
root_disk_size: "{{ ansible_mounts|json_query('[?mount == `/`].size_total') }}"
This task sets a fact (variable) named "root_disk_size". The value is retrieved from the Ansible facts "ansible_mounts". The json_query function searches for an entry where the "mount" key has a value of "/". From this entry, retrieve the "size_total" value.
However the first run failed with the following error:
TASK [FACT - Retrieve root disk size] *******************************************************
fatal: [target]: FAILED! => {"msg": "You need to install \"jmespath\" prior to running json_query filter"}
The Ansible server, on which the playbook runs, requires the jmespath Python module:
ck@ansible:/pub/ansible$ sudo apt-get install python3-jmespath
After this package was installed, the task could be executed.
Now with the disk size (in Bytes) at hand, I wanted to calculate the disk size in GB. The basic formula is:
root_disk_size / 1024^3
Spoiler alert: ^3 doesn't work in Ansible, need to use division by 1024 three times
I thought I could simply create a new fact/variable with the calculation:
- name: FACT - Retrieve root disk size
set_fact:
root_disk_size: "{{ ansible_mounts|json_query('[?mount == `/`].size_total') }}"
- name: FACT - Calculate disk size in GB
set_fact:
root_disk_size_gb: "{{ root_disk_size / 1024 / 1024 / 1024 }}"
But this task ran into an error:
fatal: [target]: FAILED! => {"msg": "An unhandled exception occurred while templating '{{ root_disk_size / 1024 / 1024 / 1024 }}'. Error was a <class 'ansible.errors.AnsibleError'>, original message: Unexpected templating type error occurred on ({{ root_disk_size / 1024 / 1024 / 1024 }}): unsupported operand type(s) for /: 'list' and 'int'"}
A debug task before the actual calculation helps to understand why:
- name: DEBUG
debug:
msg: "root disk size is: {{ root_disk_size }}"
vars:
root_disk_size: "{{ ansible_mounts|json_query('[?mount == `/`].size_total') }}"
The output shows the value of the variable "root_disk_size":
TASK [DEBUG] ***************************************************************************
ok: [target] => {
"msg": "root disk size is: [10464022528]"
}
I expected a number of bytes showing up in the output, yet the number is shown up inside square brackets []. This is because the root_disk_size variable is a "list" array, containing potentially further values (if the json_query filter would be different).
As the json_query filter is set up to only match one particular partition (the "/" mount), I am fairly sure there is only one result. Hence the json_query can be adjusted to only show a single (the first) result, by appending [0]:
- name: DEBUG
debug:
msg: "{{ root_disk_size }}"
vars:
root_disk_size: "{{ ansible_mounts|json_query('[?mount == `/`].size_total|[0]') }}"
The output now shows the value without the square brackets:
TASK [DEBUG] **************************************************************************
ok: [target] => {
"msg": "root disk size is: 10464022528"
}
With this I thought the problem is solved any my GB calculation would work. But nope. I ran into yet another error:
TASK [DEBUG] **************************************************************************
ok: [target] => {
"msg": "10464022528"
}
TASK [FACT - Calculate disk size in GB] ***********************************************
fatal: [target]: FAILED! => {"msg": "Unexpected templating type error occurred on ({{ root_disk_size / 1024 * 3 }}): unsupported operand type(s) for /: 'AnsibleUnsafeText' and 'int'"}
I kind of expected this one. By default, all (templating) variables in Ansible are a "string" (unless otherwise defined). Hence how do you want to run a mathematical operation on a string?
The solution is to use the root_disk_size variable and set it as integer (int) before we want to do a calculation:
- name: DEBUG
debug:
msg: "{{ root_disk_size }}"
vars:
root_disk_size: "{{ ansible_mounts|json_query('[?mount == `/`].size_total|[0]') }}"
# Set facts
- name: FACT - Retrieve root disk size
set_fact:
root_disk_size: "{{ ansible_mounts|json_query('[?mount == `/`].size_total|[0]') }}"
- name: FACT - Calculate disk size in GB
#vars:
# root_disk_size: "{{ ansible_mounts|json_query('[?mount == `/`].size_total|[0]') }}"
set_fact:
root_disk_size_gb: '{{ root_disk_size|int / 1024 / 1024 / 1024 }}'
- name: FACT Debug root_disk_size
debug:
msg: "{{ root_disk_size }}"
ignore_errors: True
- name: FACT Debug root_disk_size_gb
debug:
msg: "{{ root_disk_size_gb }}"
ignore_errors: True
The output of the playbook run now shows:
TASK [DEBUG] *****************************************************************************
ok: [target] => {
"msg": "10464022528"
}
TASK [FACT - Retrieve root disk size] ****************************************************
ok: [target]
TASK [FACT - Calculate disk size in GB] **************************************************
ok: [target]
TASK [FACT Debug root_disk_size] *********************************************************
ok: [target] => {
"msg": "10464022528"
}
TASK [FACT Debug root_disk_size_gb] ******************************************************
ok: [target] => {
"msg": "9.745380401611328"
}
Finally the calculation has worked and we get the disk size in GB, stored inside the "root_disk_size_gb" variable. However the GB output is fairly long...
By using the integrated Jinja2 round() function, this should be do-able:
- name: FACT - Calculate disk size in GB
#vars:
# root_disk_size: "{{ ansible_mounts|json_query('[?mount == `/`].size_total|[0]') }}"
set_fact:
root_disk_size_gb: '{{ root_disk_size|int / 1024 / 1024 / 1024|round }}'
- name: FACT Debug root_disk_size_gb
debug:
msg: "{{ root_disk_size_gb }}"
ignore_errors: True
But this did not have any effect. The value of "root_disk_size_gb" still showed a large number with lots of decimals:
TASK [FACT - Calculate disk size in GB] *************************************************
ok: [target]
TASK [FACT Debug root_disk_size_gb] *****************************************************
ok: [target] => {
"msg": "9.745380401611328"
}
The mathematical operation needs to be placed into parantheses before using round(), as explained in this StackOverflow answer.
And with this last piece of fixing, the following final Ansible playbook works:
- name: DEBUG
debug:
msg: "{{ root_disk_size }}"
vars:
root_disk_size: "{{ ansible_mounts|json_query('[?mount == `/`].size_total|[0]') }}"
# Set facts
- name: FACT - Retrieve root disk size
set_fact:
root_disk_size: "{{ ansible_mounts|json_query('[?mount == `/`].size_total|[0]') }}"
- name: FACT - Calculate disk size in GB
#vars:
# root_disk_size: "{{ ansible_mounts|json_query('[?mount == `/`].size_total|[0]') }}"
set_fact:
root_disk_size_gb: '{{ (root_disk_size|int / 1024 / 1024 / 1024)|round }}'
- name: FACT Debug root_disk_size
debug:
msg: "{{ root_disk_size }}"
ignore_errors: True
- name: FACT Debug root_disk_size_gb
debug:
msg: "{{ root_disk_size_gb }}"
ignore_errors: True
Playbook output:
TASK [DEBUG] ***************************************************************************
ok: [target] => {
"msg": "10464022528"
}
TASK [FACT - Retrieve root disk size] **************************************************
ok: [target]
TASK [FACT - Calculate disk size in GB] ************************************************
ok: [target]
TASK [FACT Debug root_disk_size] *******************************************************
ok: [target] => {
"msg": "10464022528"
}
TASK [FACT Debug root_disk_size_gb] ****************************************************
ok: [target] => {
"msg": "10.0"
}
Hurray, this looks good!
At the end this information is retrieved to automatically add the target machine into Netbox, using the Netbox collection for Ansible. If I get to it, maybe I'll post this in another article.
No comments yet.
AWS Android Ansible Apache Apple Atlassian BSD Backup Bash Bluecoat CMS Chef Cloud Coding Consul Containers CouchDB DB DNS Database Databases Docker ELK Elasticsearch Filebeat FreeBSD Galera Git GlusterFS Grafana Graphics HAProxy HTML Hacks Hardware Icinga Influx Internet Java KVM Kibana Kodi Kubernetes LVM LXC Linux Logstash Mac Macintosh Mail MariaDB Minio MongoDB Monitoring Multimedia MySQL NFS Nagios Network Nginx OSSEC OTRS Office PGSQL PHP Perl Personal PostgreSQL Postgres PowerDNS Proxmox Proxy Python Rancher Rant Redis Roundcube SSL Samba Seafile Security Shell SmartOS Solaris Surveillance Systemd TLS Tomcat Ubuntu Unix VMWare VMware Varnish Virtualization Windows Wireless Wordpress Wyse ZFS Zoneminder