Running ad hoc commands

For our first exercise, we are going to run some ad hoc commands to help you get a feel for how Ansible works. Ansible ad hoc commands enable you to perform tasks on remote nodes without having to write a playbook. They are very useful when you simply need to do one or two things quickly and often, to many remote nodes.

Work with your Inventory

To use the ansible command for host management, you need to provide an inventory file which defines a list of hosts to be managed from the control node. In this lab the inventory is provided by your instructor. The inventory is an ini formatted file listing your hosts, sorted in groups, additionally providing some variables. Have a look for yourself, in your VS Code server terminal run:

[ec2-user@autoctl1 ~]$ cat lab_inventory/hosts

[all:vars]
ansible_user=student<GUID>
ansible_ssh_pass=<password>
ansible_port=22

[web]
node1 ansible_host=<IP address>
node2 ansible_host=<IP address>
node3 ansible_host=<IP address>

[control]
ansible ansible_host=<IP address>

The environment for this lab uses SSH with password authentication to login to the managed nodes. For the sake of keeping things simple the password is put into the inventory file in clear text. In real world scenarios you would either use SSH key authentication or supply the password in a secure way, e.g. by using Ansible Vault.

Note that each student has an individual lab environment so we left out the actual IPs and other data. As with the other cases, replace <N> with your actual student number.

Ansible is already configured to use the inventory specific to your environment, you’ll learn how in a minute. For now let’s execute some simple commands to work with the inventory.

To reference inventory hosts, you supply a host pattern to the ansible command. Ansible has a --list-hosts option which can be useful for clarifying which managed hosts are referenced by the host pattern in an ansible command.

The most basic host pattern is the name for a single managed host listed in the inventory file. This specifies that the host will be the only one in the inventory file that will be acted upon by the ansible command. Run:

[ec2-user@autoctl1 ~]$ ansible node1 --list-hosts
  hosts (1):
    node1

An inventory file can contain a lot more information, it can organize your hosts in groups or define variables. In our example, the current inventory has the groups web and control. Run Ansible with these host patterns and observe the output:

[ec2-user@autoctl1 ~]$ ansible web --list-hosts
[ec2-user@autoctl1 ~]$ ansible web,ansible --list-hosts
[ec2-user@autoctl1 ~]$ ansible 'node*' --list-hosts
[ec2-user@autoctl1 ~]$ ansible all --list-hosts

As you see it is OK to put systems in more than one group. For instance, a server could be both a web server and a database server. Note that in Ansible the groups are not necessarily hierarchical.

The inventory can contain more data. E.g. if you have hosts that run on non-standard SSH ports you can put the port number after the hostname with a colon. Or you could define names specific to Ansible and have them point to the “real” IP or hostname.

The Ansible Configuration Files

The behavior of Ansible can be customized by modifying settings in Ansible’s ini-style configuration file. Ansible will select its configuration file from one of several possible locations on the control node, please refer to the documentation.

The recommended practice is to create an ansible.cfg file in the directory from which you run Ansible commands. This directory would also contain any files used by your Ansible project, such as the inventory and playbooks. Another recommended practice is to create a file .ansible.cfg in your home directory.

In the lab environment provided to you an .ansible.cfg file has already been created and filled with the necessary details in the home directory of your student<GUID> user on the control node:

[ec2-user@autoctl1 ~]$ ls -la .ansible.cfg
-rw-r--r--. 1 student<GUID> student<GUID> 231 14. Mai 17:17 .ansible.cfg

Review the content of the file:

[ec2-user@autoctl1 ~]$ cat .ansible.cfg
[defaults]
stdout_callback = yaml
connection = smart
timeout = 60
deprecation_warnings = False
host_key_checking = False
retry_files_enabled = False
inventory = /home/student{{student}}/lab_inventory/hosts

There are multiple configuration flags provided. Most of them are not of interest here, but make sure to note the last line: there the location of the inventory is provided. That is the way Ansible knew in the previous commands what inventory to use to lookup the nodes.

Ping a host

Let’s start with something really basic - pinging a host. To do that we use the Ansible ping module. Basically, the ping module connects to the managed host, executes a small script there and collects the results. This ensures that the managed host is reachable and that Ansible is able to execute commands properly on it.

Think of a module as a tool which is designed to accomplish a specific task.

Ansible needs to know that it should use the ping module: The -m option defines which Ansible module to use. Options can be passed to the specified module using the -a option. In addition to the module Ansible needs to know what hosts it should run the task on, here you supply the group web.

[ec2-user@autoctl1 ~]$ ansible web -m ping
node2 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false,
    "ping": "pong"
}
[...]

As you see each node in the web group reports the successful execution and the actual result - here “pong”.

Listing Modules and Getting Help

Ansible comes with a lot of modules by default. To list all modules run:

[ec2-user@autoctl1 ~]$ ansible-doc -l

In ansible-doc leave by pressing the button q. Use the up/down arrows to scroll through the content.

To find a module try e.g.:

[ec2-user@autoctl1 ~]$ ansible-doc -l | grep -i user

Get help for a specific module including usage examples:

[ec2-user@autoctl1 ~]$ ansible-doc user

Mandatory options are marked by a “=” in ansible-doc, optional ones by a “-".

Use the command module

Now let’s see how we can run a good ol’ fashioned Linux command and format the output using the command module. It simply executes the specified command on a managed host (note this time not a group but a hostname is used as host pattern):

[ec2-user@autoctl1 ~]$ ansible node1 -m command -a "id"
node1 | CHANGED | rc=0 >>
uid=1001(student<GUID>) gid=1001(student{{ student }) groups=1001(student<GUID>) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

In this case the module is called command and the option passed with -a is the actual command to run. Try to run this ad hoc command on all managed hosts using the all host pattern.

Another example: Have a quick look at the kernel versions your hosts are running:

[ec2-user@autoctl1 ~]$ ansible all -m command -a 'uname -r'

Sometimes it’s desirable to have the output for a host on one line:

[ec2-user@autoctl1 ~]$ ansible all -m command -a 'uname -r' -o

Like many Linux commands, ansible allows long-form options as well as short-form. For example ansible web --module-name ping is the same as running ansible web -m ping. We are going to be using the short-form options throughout this workshop.

The copy module and permissions

Using the copy module, execute an ad hoc command on node1 to change the contents of the /etc/motd file. The content is handed to the module through an option in this case.

Run the following, but expect an error:

[ec2-user@autoctl1 ~]$ ansible node1 -m copy -a 'content="Managed by Ansible\n" dest=/etc/motd'

As mentioned this produces an error:

    node1 | FAILED! => {
        "changed": false,
        "checksum": "a314620457effe3a1db7e02eacd2b3fe8a8badca",
        "failed": true,
        "msg": "Destination /etc not writable"
    }

The output of the ad hoc command is screaming FAILED in red at you. Why? Because user student<GUID> is not allowed to write the motd file.

Now this is a case for privilege escalation and the reason sudo has to be setup properly. We need to instruct Ansible to use sudo to run the command as root by using the parameter -b (think “become”).

Ansible will connect to the machines using your current user name (student<GUID> in this case), just like SSH would. To override the remote user name, you could use the -u parameter.

For us it’s okay to connect as student<GUID> because sudo is set up. Change the command to use the -b parameter and run again:

[ec2-user@autoctl1 ~]$ ansible node1 -m copy -a 'content="Managed by Ansible\n" dest=/etc/motd' -b

This time the command is a success:

node1 | CHANGED => {
    "changed": true,
    "checksum": "4458b979ede3c332f8f2128385df4ba305e58c27",
    "dest": "/etc/motd",
    "gid": 0,
    "group": "root",
    "md5sum": "65a4290ee5559756ad04e558b0e0c4e3",
    "mode": "0644",
    "owner": "root",
    "secontext": "system_u:object_r:etc_t:s0",
    "size": 19,
    "src": "/home/student<GUID>/.ansible/tmp/ansible-tmp-1557857641.21-120920996103312/source",
    "state": "file",
    "uid": 0

Use Ansible with the generic command module to check the content of the motd file:

[ec2-user@autoctl1 ~]$ ansible node1 -m command -a 'cat /etc/motd'
node1 | CHANGED | rc=0 >>
Managed by Ansible

Run the ansible node1 -m copy …​ command from above again. Note:

  • The different output color (proper terminal config provided).

  • The change from "changed": true, to "changed": false,.

  • The first line says SUCCESS instead of CHANGED.

This makes it a lot easier to spot changes and what Ansible actually did.

Challenge Lab: Modules

  • Using ansible-doc

    • Find a module that uses yum to manage software packages.

    • Look up the help examples for the module to learn how to install a package in the latest version.

  • Run an Ansible ad hoc command to install the package “vim” in the latest version on all available nodes.

Use the copy ad hoc command from above as a template and change the module and options.

Click here for Solution

[ec2-user@autoctl1 ~]$ ansible-doc -l | grep -i yum
[ec2-user@autoctl1 ~]$ ansible-doc yum
[ec2-user@autoctl1 ~]$ ansible all -m yum -a 'name=vim state=latest' -b