Skip to content

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 line

Inventory 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

YAML in a nutshell

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 }}