Indent json output
The default output format when using -v[vvv] is json. However, it’s one 1 line without indentation, like that:
TASK [Set fact] ********************************************************************************************************
Tuesday 01 October 2024 15:53:10 +0200 (0:00:00.053) 0:00:00.053 *******
ok: [localhost] => {"ansible_facts": {"my_fact": "hello world"}, "changed": false}
I’ve copied this tons of time in a formatter to understand a module output.
Until… I found you can just set this in ansible.cfg:
[defaults]
callback_format_pretty = true
Result of a set_fact:
TASK [Set fact] ********************************************************************************************************
Tuesday 01 October 2024 15:53:39 +0200 (0:00:00.042) 0:00:00.042 *******
ok: [localhost] => {
"ansible_facts": {
"my_fact": "hello world"
},
"changed": false
}
That’s automatically more readable! Especially if the output json is big.
Don’t use quotes unless needed
First read this page: https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html#gotchas
# Bad
storageaccount: "artifacts"
buy_key: "abc 123"
admin_name: "contoso\\admin_svc"
chef_user: {{ ansible_user }} # doesn't work (dictionary vs ansible var)
drive_letter: D: # doesn't work (colons)
weird_msg: [IMPORTANT MESSAGE] # doesn't work (array)
app_services:
- "service A"
- "service B"
# Good
storageaccount: artifacts
buy_key: abc with spaces
admin_name: contoso\admin_svc
chef_user: "{{ ansible_user }}"
drive_letter: "D:"
weird_msg: "[IMPORTANT MESSAGE]"
app_services:
- service A
- service B
More: https://blogs.perl.org/users/tinita/2018/03/strings-in-yaml---to-quote-or-not-to-quote.html
Use linters
You can use:
- yamllint
- ansible-lint
- XLAB Steampunk Spotter
Harness the power of default()
time_zone: "{{ time_zones_available[customLocation | default(location) | default('UTC')] }}"
Work on more machines at a time
This is related to the default number of forks
Update the default value (currently 5) like so:
In ansible.cfg:
[defaults]
forks = 10
Make sure you have enough resources (CPU/RAM/Network) to do so.
Have a nice summary of time spent by task
In ansible.cfg:
[defaults]
callbacks_enabled = profile_tasks
Result:
Thursday 26 September 2024 14:21:05 +0000 (0:17:41.644) 0:22:41.235 ****
===============================================================================
Install all updates and reboot as many times as needed --------------- 1061.64s
Install Cortex XDR ----------------------------------------------------- 76.19s
Install Qualys --------------------------------------------------------- 53.11s
Remove setup ----------------------------------------------------------- 31.11s
Collect required facts for later --------------------------------------- 26.38s
Disable disk defrag service -------------------------------------------- 23.85s
Remove setup ----------------------------------------------------------- 22.31s
Download Qualys from network share mount with a domain identity --------- 5.54s
Download Cortex XDR from network share mount with a domain identity ----- 4.34s
Get disks facts --------------------------------------------------------- 4.25s
Enable firewall for Domain, Public and Private profiles ----------------- 3.75s
Get disks volumes ------------------------------------------------------- 3.48s
Get disks facts --------------------------------------------------------- 3.45s
Firewall rule to allow ICMP v4 on all type codes ------------------------ 3.06s
Disable indexing service ------------------------------------------------ 3.01s
Ensure NetBIOS is disabled system wide ---------------------------------- 2.96s
Check edge business is installed ---------------------------------------- 2.80s
Ensure Qualys service is runnning --------------------------------------- 2.66s
Change power plan to high performance ----------------------------------- 2.64s
Remove CD/DVD drive letter ---------------------------------------------- 2.44
Improve speed [basic]
Use pipelining in ansible.cfg:
[defaults]
pipelining = true
Do not gather facts in your plays (unless needed):
---
- name: List vms with SQL Server
hosts: windows
gather_facts: false
tasks:
By default, i never gather facts unless specific play.
You can also only gather specific facts with ansible.builtin.setup
Improve windows speed
Use psrp rather than winrm:
---
ansible_connection: psrp
ansible_port: 5985
Official documentation about this plugin: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/psrp_connection.html
For hosts requiring kerberos, add them to a ‘kerberos’ group and declare kerberos.yml in your group_vars:
---
ansible_connection: winrm
ansible_winrm_transport: kerberos
ansible_winrm_message_encryption: auto
ansible_winrm_kerberos_delegation: true
Simple things to troubleshoot
Show vars: ansible-inventory -i inventories/dev/ --list --limit vm1
Show facts:
- name: Show facts
hosts: all
gather_facts: true
tasks:
- name: Show facts
ansible.builtin.debug:
var: ansible_facts
Test connectivity:
---
- name: Test connectivity
hosts: all
gather_facts: false
tasks:
- name: Check windows connectivity
ansible.windows.win_ping:
when: inventory_hostname in groups['windows']
- name: Check linux connectivity
ansible.builtin.ping:
when: inventory_hostname in groups['linux']
Use tag ‘always’
https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_tags.html#always-and-never
For instance, if you gather facts manually and other tasks depent on it, but the user decide to launch the playbook with –tags, it will likely skip this mandatory step and fail.
To avoid this, you can use tag ‘always’:
- name: Collect required facts for later
ansible.builtin.setup:
gather_subset:
- '!all'
- min
filter:
- ansible_os_installation_type
- ansible_domain
tags: always
Simplify ansible vault credentials
In case you are using ansible vault to store some secrets, I advise to set vault_password_file
in your ansible.cfg:
[defaults]
vault_password_file = ~/.vault_pass
This will prevent any prompt and help you for ansible-vault
command.
Azure dynamic inventory plugin tricks
Here is a sample:
---
plugin: azure.azcollection.azure_rm
auth_source: auto
include_vm_resource_groups:
- "*"
subscription_id: 2c60a3c5-9d3f-40f1-973b-3ca81d281ae4
plain_host_names: true # prevents generated suffix
default_host_filters:
- powerstate != 'running' # ignore stopped machine (connection will fail!)
- provisioning_state != 'succeeded' # ignore machine being provisioned or failed
hostvar_expressions:
# kerberos needs FQDN ; zscaler (remote) needs dns
ansible_host: (private_ipv4_addresses | first) if os_profile.system == 'linux' else computer_name | default(name, true)
conditional_groups:
windows: os_profile.system == 'windows'
linux: os_profile.system == 'linux'
kerberos: os_profile.system == 'windows' and tags.Role is defined and tags.Role not in ['XAB', 'CBR']
keyed_groups:
# places each host in a group named 'tag_(tag name)_(tag value)' for each tag on a VM.
- prefix: tag
key: tags
- prefix: geo
key: location
Explanations:
default_host_filters:
Ignore stopped vms or vms being built, as we cannot connect to it.
Up to the operator to know which vms remains to configure if some were stopped during playbook execution.
keyed_groups:
Will automatically create groups based on each tag and also on the location of the vm
hostvar_expressions to set ansible_host:
The default azurerm inventory plugin logic is to set ansible_host with the public or private ipv4 provided by azure. See:
https://github.com/ansible-collections/azure/blob/dev/plugins/inventory/azure_rm.py#L357 (look for ‘ansible_host’, line might change with time)
However, that isn’t great in some scenarios where fqdn is required:
- kerberos authentication (ansible_connection: winrm ; ansible_winrm_transport: kerberos)
- zscaler (ex: remote work)
Thus, this hostvar expression allows us to set ansible_host with infos provided by azure and use conditions.
Craft a list of objects
- name: Store result for final output
ansible.builtin.set_fact:
versions: |
[
{% for n in res.results %}
{
"subenv": "{{ n.item.key }}",
"version": "{{ (n.content | b64decode | split('_'))[1] }}"
},
{% endfor %}
]
Generate a custom report for reachable machines
- name: Merge infos # noqa: run-once[task]
ansible.builtin.set_fact:
servers: |
[
{% for host in ansible_play_hosts_all %}
{% set host_vars = hostvars[host] %}
{% if host_vars.inventory_hostname not in ansible_play_hosts %} # unreachable
{
"name": "{{ host_vars.inventory_hostname }}",
"ips": {{ host_vars.ansible_facts.ip_addresses }},
"system": "{{ host_vars.os_profile.system }}",
"os": "vm unreachable",
"uptime_days": -1,
"size":
{
"name": "{{ host_vars.virtual_machine_size | default('') }}",
"vCPUs": 0,
"RAM": 0,
},
},
{% else %}
{
"name": "{{ host_vars.inventory_hostname }}",
"ips": {{ host_vars.ansible_facts.ip_addresses }},
"system": "{{ host_vars.ansible_facts.os_family }}",
"os": "{{ host_vars.ansible_facts.os_name }}",
"uptime_days": {{ (host_vars.ansible_facts.uptime_seconds / 86400) | round(2) }},
"size":
{
"name": "{{ host_vars.virtual_machine_size | default('') }}",
"vCPUs": {{ host_vars.ansible_processor_vcpus }},
"RAM": {{ (host_vars.ansible_memtotal_mb / 1024) | round | int }},
},
},
{% endif %}
{% endfor %}
]
delegate_to: localhost
run_once: true
- name: Generate report.html # noqa: run-once[task]
ansible.builtin.template:
src: templates/report.html.j2
dest: "{{ out_folder }}/report.html"
mode: "0644"
delegate_to: localhost
run_once: true
Compute a list with set_fact and loop
Advanced sample:
- name: Get disks facts
community.windows.win_disk_facts:
filter:
- physical_disk
no_log: true
- name: Compute RAW disks
ansible.builtin.set_fact:
raw_disks: "{{ ansible_disks | community.general.json_query('[?partition_style==`RAW`].number') }}"
# Give the ability to customize drive letters, sample to skip letter F:
# raw_disks: [7, 23, 2]
# custom_data_disks:
# E: Data
# G: Backup
# L: Log
- name: Compute custom list of disk infos
ansible.builtin.set_fact:
disks_infos: '{{ disks_infos | default([]) + [{"disk_number": raw_disks[idx], "letter": item.key, "label": item.value}] }}'
loop: "{{ query('dict', custom_data_disks) }}"
loop_control:
index_var: idx
when:
- custom_data_disks is defined
- raw_disks | length > 0
Compute a list with lookup query and jinja statements
See: https://jinja.palletsprojects.com/en/3.1.x/templates/#list-of-control-structures
- name: Determine logstash servers
ansible.builtin.set_fact:
logstash_servers: >-
{% set result = [] -%}
{% for s in query('inventory_hostnames', 'tag_tech_logstash') -%}
{% set dummy = result.append(hostvars[s].name) -%}
{% endfor -%}
{{ result }}