ex294 exam notes
Ansible CLI
ad hoc ansible commands
ansible host-pattern -m module [-a 'module arguments'] [-i inventory]
ansible -i inventory localhost -m setup
# default module is command -a argument localhost is the host
ansible -a /bin/date localhost
# ansible_facts
ansible -m setup localhost
# -k prompt for password
ansible -m ping -i inventory all -u 4esnok -k
checks
ansible-playbook -i inventory site.yml --syntax-check
ansible-playbook -i inventory site.yml --check
doc
ansible-doc -l
ansible-doc -s ansible.builtin.copy
ansible-navigator doc -t lookup -m lookup -l -m stdout
inventory
ansible-navigator inventory -m stdout --list
ansible-inventory --list
ansible-inventory --graph all
ansible --list all
ansible --list 'webservers:rhel' # intersection
ansible --list 'webservers:&rhel' # conjunction
ansible --list 'webservers:!rhel' # negation
vault
ansible-vault create secret.yml
ansible-vault create --vault-password-file=vault-pass secret.yml
ansible-vault view secret1.yml
ansible-vault edit secret.yml
ansible-vault encrypt secret1.yml secret2.yml
ansible-vault decrypt secret1.yml --output=secret1-decrypted.yml
ansible-vault rekey secret.yml
# prompt for the vault password
ansible-navigator run -m stdout --playbook-artifact-enable false create_users.yml --vault-id @prompt
ansible-navigator run -m stdout create_users.yml --vault-password-file=vault-pass
galaxy
ansible-galaxy collection install fedora.linux_system_roles -p ./collections
ansible-galaxy search 'redis' --platforms EL
ansible-galaxy info geerlingguy.redis
ansible-galaxy list
ansible-galaxy remove nginx
ansible-navigator
# ee is execution environment, playbook runs on the host
ansible-navigator run --ee false -m stdout playbook.yml
config path
- /etc/ansible/ansible.cfg # system wide
- ~/.ansible.cfg # home directory
- ./ansible.cfg # current directory
ansible.cfg example
[defaults]
inventory = ./inventory
remote_user = root
host_key_checking = False
retry_files_enabled = False
roles_path = ./roles
library = ./library
module_utils = ./module_utils
callback_whitelist = profile_tasks
[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False
ansible-config
show configurations that have changed from the default
ansible-config dump --only-changed
generate ansible.cfg from a template
ansible-config init
Syntax
Conditionals
---
- name: Simple Boolean Task Demo
hosts: all
vars:
run_my_task: true
tasks:
- name: httpd package is installed
ansible.builtin.dnf:
name: httpd
when: run_my_task | bool
---
- name: Test Variable is Defined Demo
hosts: all
vars:
my_service: httpd
tasks:
- name: "{{ my_service }} package is installed"
ansible.builtin.dnf:
name: "{{ my_service }}"
when: my_service is defined
greater-thn character (>) used to split the long conditionals over multiple lines
when: >
( ansible_facts['distribution'] == "RedHat" and
ansible_facts['distribution_major_version'] == "9" )
or
( ansible_facts['distribution'] == "Fedora" and
ansible_facts['distribution_major_version'] == "34" )
these two conditions are equal
when: ansible_facts['distribution_version'] == "9.0" and ansible_facts['kernel'] == "5.14.0-70.13.1.el9_0.x86_64"
when:
- ansible_facts['distribution_version'] == "9.0"
- ansible_facts['kernel'] == "5.14.0-70.13.1.el9_0.x86_64"
When examples
when: ansible_facts['distribution'] == "RedHat"
when: "'RedHat' in ansible_facts['distribution']"
when: my_var is defined
When help
ansible-doc -t keyword when
Loops
- name: Postfix and Dovecot are running
ansible.builtin.service:
name: "{{ item }}"
state: started
loop:
- postfix
- dovecot
- name: Users exist and are in the correct groups
user:
name: "{{ item['name'] }}"
state: present
groups: "{{ item['groups'] }}"
loop:
- name: jane
groups: wheel
- name: joe
groups: root
vars:
mail_services:
- postfix
- dovecot
tasks:
- name: Postfix and Dovecot are running
ansible.builtin.service:
name: "{{ item }}"
state: started
loop: "{{ mail_services }}"
The register keyword can also capture the output of a task that loops. The following snippet shows the structure of the register variable from a task that loops:
---
- name: Loop Register Test
gather_facts: no
hosts: localhost
tasks:
- name: Looping Echo Task
ansible.builtin.shell: "echo This is my item: {{ item }}"
loop:
- one
- two
register: echo_results
- name: Show echo_results variable
ansible.builtin.debug:
var: echo_results
playbook output
{"echo_results": {
"changed": true,
"msg": "All items completed",
"results": [
{
"ansible_facts": {
"discovered_interpreter_python": "/usr/local/bin/python3.11"
},
"ansible_loop_var": "item",
"changed": true,
"cmd": "echo This is my item: one",
"delta": "0:00:00.014493",
"end": "2023-03-22 22:14:19.949881",
"failed": false,
"invocation": {}}]}}
Earlier-style Ansible loops
| Loop keyword | Description |
|---|---|
with_items |
Behaves the same as the loop keyword for simple lists, such as a list of strings or a list of dictionaries. Unlike loop, if lists of lists are provided to with_items, they are flattened into a single-level list. The item loop variable holds the list item used during each iteration. |
with_file |
Requires a list of control node file names. The item loop variable holds the content of a corresponding file from the file list during each iteration. |
with_sequence |
Requires parameters to generate a list of values based on a numeric sequence. The item loop variable holds the value of one of the generated items in the generated sequence during each iteration. |
Loop help
Loops are facilitated by lookup plugin
ansible-doc -t lookup -l
ansible-doc -t keyword loop
Handlers and Failure
tasks:
- name: copy demo.example.conf configuration template
ansible.builtin.template:
src: /var/lib/templates/demo.example.conf.template
dest: /etc/httpd/conf.d/demo.example.conf
notify:
- restart apache
handlers:
- name: restart apache
ansible.builtin.service:
name: httpd
state: restarted
ignore_errors
- name: Install {{ web_package }} package
ansible.builtin.dnf:
name: "{{ web_package }}"
state: present
ignore_errors: yes
change_when
change the status of the task
- name: Check local time
ansible.builtin.command: date
register: command_result
changed_when: false
failed_when - define what “failure” means in the task
- name: Fail task when the command error output prints FAILED
ansible.builtin.command: /usr/bin/example-command -x -y -z
register: command_result
failed_when: "'FAILED' in command_result.stderr"
check_mode
ansible-navigator run --check
tasks:
- name: task always runs even in check mode
ansible.builtin.shell: uname -a
check_mode: no
Block
block - a way to group tasks together and execute them as a single unit
tasks:
- name: Attempt to set up a webserver
block:
- name: Install {{ web_package }} package
ansible.builtin.dnf:
name: "{{ web_package }}"
state: present
rescue:
- name: Install {{ db_package }} package
ansible.builtin.dnf:
name: "{{ db_package }}"
state: present
always:
- name: Start {{ db_service }} service
ansible.builtin.service:
name: "{{ db_service }}"
state: started
Filters
Filter help
ansible-doc -t filter -l
Filter default and password_hash
password: "{{ my_password | default('redhat') | password_hash('sha512') }}"
Filter dict2items
vars:
my_users:
fred:
groups:
- flintstones
- wheel
password: yabadabadoo
wilma:
groups: flintstones
password: yabadabadoo
barney:
groups: rubbles
password: flimflom
tasks:
- name: Ensure users are in their appropriate groups
loop: "{{ my_users | dict2items }}"
ansible.builtin.user:
name: "{{ item['key'] }}"
state: present
groups: "{{ item['value']['groups'] }}"
password: "{{ item['value']['password'] | password_hash('sha512') }}"
update_password: on_create
generate_ssh_key: yes
Filter product and list
vars:
beatles:
- John
- Paul
- George
- Ringo
category_db:
- lyric
- concerts
- instruments
tasks:
- name: Ensure Beatles access to their databases
loop: "{{ beatles | product(category_db) | list }}"
ansible.mysql.mysql_user:
name: "{{ item[0] }}"
priv: "{{ item[1] }}.*:ALL"
append_privs: yes
password: "{{ 'db_pass' | password_hash('sha512') }}"
The list filter is used here to convert the result of the product filter into a list, which is then used to loop over.
Filter lookup('dict', ...)
# ansible-doc -t lookup dict
vars:
config_files:
vim:
file: vimrc
dest: ~/.vimrc
yamllint:
file: yamllint
dest: ~/.config/yamllint/config
tasks:
- name: Ensure config files are copied
loop: "{{ lookup('dict', config_files) }}"
ansible.builtin.copy:
src: "{{ item['value']['file'] }}"
dest: "{{ item['value']['dest'] }}"
Include, Import and Roles
Include
Includes are considered dynamic operation. Ansible will process the instruction as it gets to the appropriate line inside of your playbook.
include_vars
include_tasks
---
- name: Install web server
hosts: webservers
tasks:
- include_tasks: webserver_tasks.yml
include_role
One key difference between include_role and import_role is how they handle task-level keywords, conditionals, and loops:
-
ansible.builtin.import_role applies the task's conditionals and loops to each of the tasks being imported.
-
ansible.builtin.include_role applies the task's conditionals and loops to the statement that determines whether the role is included or not.
Import
Imports are considered static operation. Ansible will pre-processed at the time the playbook is parsed. (can't use loop, variables are limited)
import_playbook
example of a master playbook
- name: Prepare the web server
import_playbook: web.yml
- name: Prepare the database server
import_playbook: db.yml
import_tasks
[admin@node ~]$ cat webserver_tasks.yml
- name: Installs the httpd package
ansible.builtin.dnf:
name: httpd
state: latest
- name: Starts the httpd service
ansible.builtin.service:
name: httpd
state: started
---
- name: Install web server
hosts: webservers
tasks:
- import_tasks: webserver_tasks.yml
...output omitted...
tasks:
- name: Import task file and set variables
import_tasks: task.yml
vars:
package: httpd
service: httpd
import_role
ansible treats the role as a static import
With the ansible.builtin.import_role module, Ansible treats the role as a static import and parses it during initial playbook processing. In the preceding example, when the playbook is parsed: * If roles/role2/tasks/main.yml exists, Ansible adds the tasks in that file to the play. * If roles/role2/handlers/main.yml exists, Ansible adds the handlers in that file to the play. * If roles/role2/defaults/main.yml exists, Ansible adds the default variables in that file to the play. * If roles/role2/vars/main.yml exists, Ansible adds the variables in that file to the play (possibly overriding values from role default variables due to precedence). > Because ansible.builtin.import_role is processed when the playbook is parsed, the role's handlers, default variables, and role variables are all exposed to all the tasks and roles in the play, and can be accessed by tasks and roles that precede it in the play (even though the role has not run yet).- name: Run a role as a task
hosts: remote.example.com
tasks:
- name: A normal task
ansible.builtin.debug:
msg: 'first task'
- name: A task to import role2 here
ansible.builtin.import_role:
name: role2
vars:
var1: val1
var2: val2
Roles
Role section in a Play
---
- name: A play that only has roles
hosts: remote.example.com
roles:
- role: role1
- role: role2
Install a role from Ansible Galaxy
# roles/requirments.yml
- src: https://git.example.com/someuser/someuser.myrole
scm: git
version: "1.5.0"
- src: https://www.example.com/role-archive/someuser.otherrole.tar
name: someuser.otherrole
# from Ansible Galaxy, using the latest version
- src: geerlingguy.redis
# from Ansible Galaxy, overriding the name and using a specific version
- src: geerlingguy.redis
version: "1.5.0"
name: redis_prod
# from any Git based repository, using HTTPS
- src: https://github.com/geerlingguy/ansible-role-nginx.git
scm: git
version: master
name: nginx
# from a role tar ball, given a URL;
# supports 'http', 'https', or 'file' protocols
- src: file:///opt/local/roles/myrole.tar
name: myrole
ansible-galaxy role install -r roles/requirements.yml -p roles
Variables and Facts
Variables
Variables priority from top to bottom
* Group variables defined in the inventory * Group variables defined in files in a group_vars subdirectory in the same directory as the inventory or the playbook * Host variables defined in the inventory * Host variables defined in files in a host_vars subdirectory in the same directory as the inventory or the playbook * Host facts, discovered at runtime * Play variables in the playbook (vars and vars_files) * Task variables * Extra variables defined on the command lineInventory variables
You can define variables for hosts and host groups by creating two directories, group_vars and host_vars, in the same working directory as the inventory file or playbook.
project
├── ansible.cfg
├── group_vars
│ ├── datacenters
│ ├── datacenters1
│ └── datacenters2
├── host_vars
│ ├── demo1.example.com
│ ├── demo2.example.com
│ ├── demo3.example.com
│ └── demo4.example.com
├── inventory
└── playbook.yml
Command line variables
set a variable on the command line
ansible-navigator run main.yml -e "package=apache"
Playbook variables
define a variable in a playbook or vars_files
user1_first_name: Bob
user1_last_name: Jones
user1_home_dir: /users/bjones
user2_first_name: Anne
user2_last_name: Cook
user2_home_dir: /users/acook
users:
bjones:
first_name: Bob
last_name: Jones
home_dir: /users/bjones
acook:
first_name: Anne
last_name: Cook
home_dir: /users/acook
Var files
- hosts: all
vars_files:
- vars/users.yml
Work with dictionaries
# Ansible way
# Returns 'Bob'
users.bjones.first_name
# Returns '/users/acook'
users.acook.home_dir
# Python way
# Returns 'Bob'
users['bjones']['first_name']
# Returns '/users/acook'
users['acook']['home_dir']
Magic Variables
Hostvars
A dictionary with all the hosts in inventory and variables assigned to them
hostvars['demo2.example.com']['ansible_facts']['interfaces']
group_names
A list of groups the current host is part of
groups
A dictionary with all the groups in inventory and each group has the list of hosts that belong to it
inventory_hostname
The inventory name for the ‘current’ host being iterated over in the play
Facts
# Display all facts
- name: Fact dump
hosts: all
tasks:
- name: Print all facts
ansible.builtin.debug:
var: ansible_facts
Ansible Facts Injected as Variables Before Ansible 2.5, facts were always injected as individual variables prefixed with the string ansible_ instead of being part of the ansible_facts variable. For example, the ansible_facts['distribution'] fact was called ansible_distribution.
| ansible_facts.* name | ansible_* name |
|---|---|
ansible_facts['hostname'] |
ansible_hostname |
ansible_facts['fqdn'] |
ansible_fqdn |
ansible_facts['default_ipv4']['address'] |
ansible_default_ipv4['address'] |
Custom Facts
By default, the ansible.builtin.setup module loads custom facts from files and scripts in the etc/ansible/facts.d. File shoudl end with ".fact". The ansible.builtin.setup module stores custom facts in the ansible_facts['ansible_local'] variable.
# file name should end with .fact
[packages]
web_package = httpd
db_package = mariadb-server
[users]
user1 = joe
user2 = jane
Packages Facts
- name: Insure package facts gathered
ansible.builtin.package_facts:
- name: print package facts
ansible.builtin.debug:
var: ansible_facts.packages
Modifying and Copying Files to Hosts
ansible.builtin
| Module name | Description |
|---|---|
| blockinfile | Insert/update/remove a text block surrounded by marker lines in a file |
| copy | Copy a file from the local or remote machine to a location on the managed hosts |
| fetch | Works like the copy module, but in reverse |
| file | Set attributes such as permissions, ownership, SELinux contexts, and time stamps of regular files, symlinks, hard links, and directories. This module can also create or remove regular files, symlinks, hard links, and directories. A number of other file-related modules support the same options to set attributes as the file module, including the copy module. |
| lineinfile | Ensure that a particular line is in a file, or replace an existing line using a back-reference regular expression. This module is primarily useful when you want to change a single line in a file. |
| stat | Retrieve status information for a file, similar to the Linux stat command. |
ansible.posix
| Module name | Description |
|---|---|
| patch | Apply a patch using GNU patch. |
| synchronize | A wrapper around the rsync command to simplify common tasks. The synchronize module is not intended to provide access to the full power of the rsync command, but does make the most common invocations easier to implement. You might still need to call the rsync command directly via the run command module depending on your use case. |
Troubleshooting Ansible
ansible.builtin.uri
The ansible.builtin.uri module provides a way to verify that a RESTful API is returning the required content.
tasks:
- ansible.builtin.uri:
url: http://api.myapp.example.com
return_content: yes
register: apiresponse
- ansible.builtin.fail:
msg: 'version was not provided'
when: "'version' not in apiresponse.content"
ansible.builtin.stat
You can use it to register a variable and then test to determine if the file exists or to get other information about the file
tasks:
- name: Check if /var/run/app.lock exists
ansible.builtin.stat:
path: /var/run/app.lock
register: lock
- name: Fail if the application is running
ansible.builtin.fail:
when: lock['stat']['exists']
ansible.builtin.assert
tasks:
- name: Check if /var/run/app.lock exists
ansible.builtin.stat:
path: /var/run/app.lock
register: lock
- name: Fail if the application is running
ansible.builtin.assert:
that:
- not lock['stat']['exists']
YAML
Special characters
| Character | Description |
|---|---|
| ( | ) vertical ber | to denote a new line characters |
| ( > ) greater-than | new line characters to be converted to spaces |
include_newlines: |
Example Company
123 Main Street
Atlanta, GA 30303
fold_newlines: >
This is an example
of a long string,
that will become
a single sentence once folded.
YAML Dictionaries
name: svcrole
svcservice: httpd
svcport: 80
{name: svcrole, svcservice: httpd, svcport: 80}
YAML Lists
hosts:
- servera
- serverb
- serverc
hosts: [servera, serverb, serverc]
Jinja2
for statement
{# for statement - this is comment #}
{% for user in users %}
{{ user }}
{% endfor %}
{% for myhost in groups['myhosts'] %}
{{ myhost }}
{% endfor %}
Conditional statements
{% if finished %}
{{ result }}
{% endif %}
/etc/hosts file
- name: /etc/hosts is up to date
hosts: all
gather_facts: yes
tasks:
- name: Deploy /etc/hosts
ansible.builtin.template:
src: templates/hosts.j2
dest: /etc/hosts
{{ ansible_managed }}
{% for host in groups['all'] %}
{{ hostvars[host]['ansible_facts']['default_ipv4']['address'] }} {{ hostvars[host]['ansible_facts']['fqdn'] }} {{ hostvars[host]['ansible_facts']['hostname'] }}
{% endfor %}
jinja2 filters
{{ 'hello world' | capitalize }}
{{ output | to_json }}
{{ output | to_yaml }}