Ad Hoc Ansible Commands

Building off our lab, we need a playbook that give instructions for getting managed nodes to their desired states. Playbooks are scripts written in YAML. There are some things you need to know when working with playbooks:

  • Ad Hoc Commands
  • Modules
  • Module Documentation
  • Ad Hoc commands from bash scripts

Ad Hoc Commands

Ad hoc commands are ansible tasks you can run against managed hosts without the need of a playbook or script. These are used for bringing nodes to their desired states, verifying playbook results, and verifying nodes meet any needed criteria/pre-requisites. These must be ran as the ansible user (whatever your remote_user directive is set to under [defaults] in ansible.cfg)

Run the user module with the argument name=lisa on all hosts to make sure the user “lisa” exists. If the user doesn’t exist, it will be created on the remote system: ansible all -m user -a "name=lisa"

{command} {host} -m {module} -a {"argument1 argument2 argument3"}

In our lab:

[ansible@control base]$ ansible all -m user -a "name=lisa"
web1 | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: ssh: Could not resolve hostname web1: Name or service not known",
    "unreachable": true
}
web2 | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: ssh: Could not resolve hostname web2: Name or service not known",
    "unreachable": true
}
ansible1 | CHANGED => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": true,
    "comment": "",
    "create_home": true,
    "group": 1001,
    "home": "/home/lisa",
    "name": "lisa",
    "shell": "/bin/bash",
    "state": "present",
    "system": false,
    "uid": 1001
}
ansible2 | CHANGED => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": true,
    "comment": "",
    "create_home": true,
    "group": 1001,
    "home": "/home/lisa",
    "name": "lisa",
    "shell": "/bin/bash",
    "state": "present",
    "system": false,
    "uid": 1001
}

This Ad Hoc command created user “Lisa” on ansible1 and ansible2. If we run the command again, we get “SUCCESS” on the first line instead of “CHANGED”. Which means the hosts already meet the requirements:

[ansible@control base]$ ansible all -m user -a "name=lisa"
web2 | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: ssh: Could not resolve hostname web2: Name or service not known",
    "unreachable": true
}
web1 | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: ssh: Could not resolve hostname web1: Name or service not known",
    "unreachable": true
}
ansible2 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "append": false,
    "changed": false,
    "comment": "",
    "group": 1001,
    "home": "/home/lisa",
    "move_home": false,
    "name": "lisa",
    "shell": "/bin/bash",
    "state": "present",
    "uid": 1001
}
ansible1 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "append": false,
    "changed": false,
    "comment": "",
    "group": 1001,
    "home": "/home/lisa",
    "move_home": false,
    "name": "lisa",
    "shell": "/bin/bash",
    "state": "present",
    "uid": 1001
}

indempotent Regardless of current condition, the host is brought to the desired state. Even if you run the command multiple times.

Run the command id lisa on all managed hosts:

[ansible@control base]$ ansible all -m command -a "id lisa"
web1 | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: ssh: Could not resolve hostname web1: Name or service not known",
    "unreachable": true
}
web2 | UNREACHABLE! => {
    "changed": false, module should you use to run the rpm -qa | grep httpd command?


    "msg": "Failed to connect to the host via ssh: ssh: Could not resolve hostname web2: Name or service not known",
    "unreachable": true
}
ansible1 | CHANGED | rc=0 >>
uid=1001(lisa) gid=1001(lisa) groups=1001(lisa)
ansible2 | CHANGED | rc=0 >>
uid=1001(lisa) gid=1001(lisa) groups=1001(lisa)

Here, the command module is used to run a command on the specified hosts. And the output is displayed on screen. TO note, this does not show up in our ansible user’s command history on the host:

[ansible@ansible1 ~]$ history
    1  history

Remove the userLlisa from all managed hosts:

[ansible@control base]$ ansible all -m user -a "name=lisa state=absent"
web2 | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: ssh: Could not resolve hostname web2: Name or service not known",
    "unreachable": true
}
web1 | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: ssh: Could not resolve hostname web1: Name or service not known",
    "unreachable": true
}
ansible1 | CHANGED => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": true,
    "force": false,
    "name": "lisa",
    "remove": false,
    "state": "absent"
}
ansible2 | CHANGED => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": true,
    "force": false,
    "name": "lisa",
    "remove": false,
    "state": "absent"
}
[ansible@control base]$ ansible all -m command -a "id lisa"
web1 | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: ssh: Could not resolve hostname web1: Name or service not known",
    "unreachable": true
}
web2 | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: ssh: Could not resolve hostname web2: Name or service not known",
    "unreachable": true
}
ansible1 | FAILED | rc=1 >>
id: ‘lisa’: no such usernon-zero return code
ansible2 | FAILED | rc=1 >>
id: ‘lisa’: no such usernon-zero return code

You can also use the -u option to specify the user that Ansible will use to run the command. Remember, with no modules specified, ansible uses the command module: ansible all -a "free -m" -u david

Modules

There are more than 3000 Ansible modules available for a variety of different tasks and servers. The more modules you know, the better you will be at Ansible. They are essentially plug ins written in Python that can be used in playbooks or Ad Hoc commands. Make sure to use modules that are the most specific to the task you are trying to accomplish.

Important modules

Arbitrary Modules

Limit your use of these, it’s hard to track what has been changed using these modules. Use the more specific indempotent module for the task instead, if you can.

command Runs arbitrary commands (not using the shell). Shell stuff such as pipes and redirects will not work with this module. This is the default module if no modules are specified. You can set different default module in ansible.crf by using “module_name = module”. A python script is generated on the manage host and executed. ansible all -m command -a "rpm -qa | grep httpd" (the pipe gets ignored)

Check status of httpd: ansible all -m command -a "systemctl status httpd"

shell Same as above but with the shell. So pipes and redirects will work. A python script is generated on the manage host and executed. ansible all -m shell -a "rpm -qa | grep httpd" (the pipe is not ignored)

raw Runs arbitrary command on top of SSH without using Python. Good for managed hosts that don’t have Python. Or install Python during setup: ansible -u root -i inventory ansible3 --ask-pass -m raw -a "yum install python3

Indempotent modules

These are easier to track and guarantee indempotency.

copy Copy files or lines of text to files `ansible all -m copy -a ‘content=“hello world” dest=/etc/motd’

yum Manage packages on RHEL hosts. Can use the package module to install packages on any Linux distro. Use the yum module if you need specific yum features. And package module if you need to manage software on different distros.

Install latest version of nmap: `ansible all -m yum -a “name=nmap state=latest”

List httpd details: ansible all -m yum -a "list=httpd"

service Manage state of systemd and system-V services. Make sure to use enabled=yes and state=started to make sure services are enabled at startup. ansible -m service -a "name=httpd state=started enabled=yes"

ping Checks if managed hosts are in a manageable state. ansible all -m ping

Viewing available modules with ansible-doc

As noted before, there are over 3,000 modules that come with Ansible. These are installed on your system when you install Ansible. View all the modules available like so: ansible-doc -l

Filter to get more specific results:

[ansible@control ~]$ ansible-doc -l | grep package
ansible.builtin.apt                    Manages apt-packages                
ansible.builtin.debconf                Configure a .deb package            
ansible.builtin.dnf                    Manages packages with the `dnf' pack...
ansible.builtin.dpkg_selections        Dpkg package selection selections   
ansible.builtin.package                Generic OS package manager          
ansible.builtin.package_facts          Package information as facts        
ansible.builtin.yum                    Manages packages with the `yum' pack...

Finding details on a specific module: ansible-doc ping

Output shows the module name, maintainter information, options available, related modules, module author, examples, and return values.

Each module is a Python script on your system that you can view if you want to see what is going on under the hood: > ANSIBLE.BUILTIN.PING (/usr/lib/python3.9/site-packages/ansible/modules/ping.py)

Make sure you read the modules description for details!

A trivial test module, this module always returns `pong' on successful contact. It does not make sense in playbooks,
        but it is useful from `/usr/bin/ansible' to verify the ability to login and that a usable Python is configured. This
        is NOT ICMP ping, this is just a trivial test module that requires Python on the remote-node. For Windows targets, use
        the [ansible.windows.win_ping] module instead. For Network targets, use the [ansible.netcommon.net_ping] module
        instead.

Note that mandatory option are listed as =option instead of -option.

OPTIONS (= is mandatory):

- data
        Data to return for the `ping' return value.
        If this parameter is set to `crash', the module will cause an exception.
        default: pong
        type: str

And don’t forget to check the “SEE ALSO” section to see if there could be a module that better suits your needs:

SEE ALSO:
      * Module ansible.netcommon.net_ping
      * Module ansible.windows.win_ping

Here are some examples from the raw module doc:

EXAMPLES:

- name: Bootstrap a host without python2 installed
  ansible.builtin.raw: dnf install -y python2 python2-dnf libselinux-python

- name: Run a command that uses non-posix shell-isms (in this example /bin/sh doesn't handle redirection and wildcards together but bash does)
  ansible.builtin.raw: cat < /tmp/*txt
  args:
    executable: /bin/bash

- name: Safely use templated variables. Always use quote filter to avoid injection issues.
  ansible.builtin.raw: "{{ package_mgr|quote }} {{ pkg_flags|quote }} install {{ python|quote }}"

- name: List user accounts on a Windows system
  ansible.builtin.raw: Get-WmiObject -Class Win32_UserAccount

The examples show the playbook code for common use cases for running the module. Use the -s flag to show the playbook snippet only:

[ansible@control ~]$ ansible-doc -s service
- name: Manage services
  service:
      arguments:             # Additional arguments provided on the command line. While using remote hosts with systemd this setting will be ignored.
      enabled:               # Whether the service should start on boot. *At least one of state and enabled are required.*
      name:                  # (required) Name of the service.
      pattern:               # If the service does not respond to the status command, name a substring to look for as would be found in the output of the `ps'
                             # command as a stand-in for a status result. If the string is found, the service will be assumed to
                             # be started. While using remote hosts with systemd this setting will be ignored.
      runlevel:              # For OpenRC init scripts (e.g. Gentoo) only. The runlevel that this service belongs to. While using remote hosts with systemd
                             # this setting will be ignored.
      sleep:                 # If the service is being `restarted' then sleep this many seconds between the stop and start command. This helps to work around
                             # badly-behaving init scripts that exit immediately after signaling a process to stop. Not all
                             # service managers support sleep, i.e when using systemd this setting will be ignored.
      state:                 # `started'/`stopped' are idempotent actions that will not run commands unless necessary. `restarted' will always bounce the
                             # service. `reloaded' will always reload. *At least one of state and enabled are required.* Note
                             # that reloaded will start the service if it is not already started, even if your chosen init
                             # system wouldn't normally.
      use:                   # The service module actually uses system specific modules, normally through auto detection, this setting can force a specific
                             # module. Normally it uses the value of the 'ansible_service_mgr' fact and falls back to the old
                             # 'service' module when none matching is found.

The official Ansible documentation will also be available during the RHCE exam: https://docs.ansible.com/

The docs will also show you how to install additional module collections. To install the posix collection: [ansible@control base]$ ansible-galaxy collection install ansible.posix

Ad hoc commands in Scripts

Follow normal bash scripting guidelines to run ansible commands in a script:

[ansible@control base]$ vim httpd-ansible.sh

Let’s set up a script that installs and starts/enables httpd, creates a user called “anna”, and copies the ansible control node’s /etc/hosts file to /tmp/ on the managed nodes:

#!/bin/bash

ansible all -m yum -a "name=httpd state=latest"
ansible all -m service -a "name=httpd state=started enabled=yes"
ansible all -m user -a "name=anna"
ansible all -m copy -a "src=/etc/hosts dest=/tmp/hosts"
[ansible@control base]$ chmod +x httpd-ansible.sh
[ansible@control base]$ ./httpd-ansible.sh 
web2 | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: ssh: Could not resolve hostname web2: Name or service not known",
    "unreachable": true
}
web1 | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: ssh: Could not resolve hostname web1: Name or service not known",
    "unreachable": true
}
ansible1 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "msg": "Nothing to do",
    "rc": 0,
    "results": []
}
ansible2 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "msg": "Nothing to do",
    "rc": 0,
    "results": []
}
... <-- Results truncated

And from the ansible1 node we can verify:

[ansible@ansible1 ~]$ cat /etc/passwd | grep anna
anna:x:1001:1001::/home/anna:/bin/bash
[ansible@ansible1 ~]$ cat /tmp/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.124.201 ansible1
192.168.124.202 ansible2

View a file from a managed node: ansible ansible1 -a "cat /somfile.txt"