Ansible
a system administration automation framework

This is an introductory demonstration of basic mechanisms of ansible, a tool for automated software deployment and configuration management of systems.

Setup

We will assemble a network of 3 computers. One of them will run ansible. It will operate as a kind of "push server" to orchestrate management of the other two. Preliminary requirements:

- the computers must be in a network such that each can reach/ping the others
- the managed computers must have the ssh server, sshd, installed, running, and reachable
- the managing computer must have ansible installed

Additional requirements for this demonstration:

- each computer should have a "student" and "root" account
- you need to know their passwords

Make sure these requirements are met, with guidance from the instructor as necessary. Here we will call the main computer that's running ansible the "managing" and the others the "managed" computers.

If performing this exercise using class provided virtual machines, create three clones (VirtualBox's "linked" as opposed to "full" clones to save time and disk space). Set their memory requirements low since you will be running three instances, 768M is a good choice. 

You will log in to both of the managed computers as root and operate in command mode. You would do the same on the managing computer except that it is desirable there to be able to copy-and-paste, which requires graphical mode (and appropriate configuration in a VM's   "Devices" menu, "Shared Clipboard" sub-option). Log in to the managing computer as student and invoke the GUI (startx). Then start a terminal window and become root (sudo su -).

Log in at the managing computer. As root create an ssh key pair if the root user does not have one already:

ssh-keygen -t rsa (respond to all the prompts by pressing enter)

Note the directory .ssh within your home directory /root, and the 2 files in it id_rsa, id_rsa.pub. The latter is your public key.

If necessary, install ansible:

dnf  install  ansible

Ansible, operated from the managing computer, is used to trigger software to run in the managed computers in order to do things there. (It uses ssh for that.)

In order to shepherd these computers ansible needs to know its flock. That is, the IP addresses of each managed computer must be listed somewhere so that ansible knows for what computers it is responsible, its "inventory." It also needs to know, when it triggers software in each managed computer, under which of that computer's user accounts such software should run.

The managing computer's inventory list is in /etc/ansible/hosts. Enroll the target machines and users, and give the both of them together a group name, e.g.

[hosts]
IP of 1st managed computer
   ansible_user=student
IP of 2nd managed computer ansible_user=student

In the real world the "hosts" label would be something more meaningful like [webservers], if all the managed computers listed under that label play the webserver role. The webserver moniker then gives you a single handle by which to address that subset of your machines.

For convenience let's store in variables our IP and user account identifiers:

IP1=IP of 1st managed computer
IP2=IP of 2nd managed computer
USER1=student
USER2=student
export IP1 IP2 USER1 USER2

Ideally we want to avoid having to interactively type in passwords so let's arrange for password-less ssh key-based authentication instead:

ssh-copy-id  -i  ~/.ssh/id_rsa.pub  $USER1@$IP1
ssh-copy-id  -i ~/.ssh/id_rsa.pub   $USER2@$IP2

You will be asked for the respective users' passwords but then your public key will be inserted in their "authorized_keys" files obviating the further need for you to supply passwords while operating ansible.

As a further convenience it would be good that the computers have different hostnames, so that their shell prompts visibly tell you which computer you're on. Let's name them "managing" "managed1" and "managed2."  The commands for setting hostnames on the respective machines:

hostnamectl  set-hostname  managing

hostnamectl  set-hostname  managed1

hostnamectl  set-hostname  managed2

After setting the hostnames log out then log back in, for the new hostnames to take effect.

ansible commands ("modules")

Now let's test ansible. It has a large number of modules. They are components to do particular jobs. One of them is the "ping" module. It's distinct from the normal "ping" diagnostic network utility, though similar in function. It makes sure ansible is able to talk through to the machines it wants to manage:

ansible  -m ping  $IP1
ansible  -m ping  $IP2

Activities that ansible will cause to run on the managed computers will be performed as "student" accounts there. That's because of the entry you made above in /etc/ansible/hosts. But you will almost certainly also want to perform a lot of activities that require root privileges. After all, system configuration by nature tends to be a global, administrative job. There are various ways to provide for that. We will have ansible utilize sudo. It has a configuration file of its own called /etc/sudoers. Please add to /etc/sudoers on each managed computer the following line: 

student  ALL=(root)  NOPASSWD: ALL

That says that user "student" can run any command he wants as "root" instead, provided he runs the command through (i.e., as an argument to) sudo. And, it says, sudo won't demand any password in the process. When you want ansible to run something as root, it will invoke sudo to accomplish it. You don't have to. What you have to do is tell ansible to take care of it. That's what ansible's  " -b " option is for.  -b stands for "become," meaning that you want to become root.

So how do you "run any command"? Ansible has a couple of modules for that, called "command" and "shell". You give to either, after specifying it to ansible with the -m option, the command you want run as an argument, by using the -a option. If you use the "command" option to run a command, it just directly runs the command itself. If you use the "shell" option to run a command, it runs the shell instead and has the shell run the command. That means any shell features you sometimes use on your command line won't work if executed through ansible's "command" module. For example, redirection, backgrounding, or running a shell script.

Tell ansible to make "whoami" run in the first managed computer, with and without the -b option:

ansible -m command -a whoami $IP1
ansible -b -m command -a whoami $IP1

It tells you, when you use the -b option, that the software it ran, whoami, was run as "root". It will run as root any other software you want executed as root.

Here's something that can be done as root but not as non-root. That's starting and stopping services. Let's try it with the httpd (i.e., apache web server) service:

ansible -m shell -a 'systemctl stop httpd' $IP1           (allow a little time)
ansible -b -m shell -a 'systemctl stop httpd' $IP1
ansible -b -m shell -a 'systemctl start httpd' $IP1
ansible -b -m shell -a 'systemctl status httpd' $IP1

It fails running as student at first, but works when you add the -b option to make it run as root.

Going back to the difference between the command module and the shell module, try:

ansible -m command -a "echo hello > hello.txt" $IP1
ansible -m command -a "ls -l hello.txt" $IP1

versus:

ansible -m shell -a "echo hello > hello.txt" $IP1
ansible -m command -a "ls -l hello.txt" $IP1

With the command module no hello.txt file was created; with the shell module it was. That's because the command module executed echo, while the shell module executed the shell (giving it echo as the command for it to run. File redirection is a feature of the shell. No shell, no redirection. Note what it is that echo echo'd when you used the command module. Now that you've littered the remote machine with hello.txt, clean it up. Run this twice:

ansible -m command -a "rm hello.txt" $IP1

The first time it removes the file, the second it (consequently) reports no such file to remove.

Another ansible module is dnf, used for installing software using (not surprisingly) dnf on managed machines (there is also an apt module for debian-style machines). Try to install vim on the first managed computer.

ansible -b -m dnf -a "name=vim-enhanced  state=present" $IP1

 It might or might not have been there already. Once you're done, it is. Verify that, by running vim on the managed computer on which ansible just put it. Then have ansible remove it:

ansible -b -m dnf -a "name=vim-enhanced  state=absent" $IP1

and, again on the managed computer, verify it's gone by trying to run it and receiving a "no such file" error message. 

While this operation was applied to a single machine, machines can be grouped so that common operations across the whole group can be performed by a single ansible invocation on the managing computer. Your two machines are grouped, under the name "webservers," because of your earlier entries into the /etc/ansible/hosts inventory file.

ansible -b -m dnf -a "name=vim-enhanced  state=absent" webservers
ansible -b -m dnf -a "name=vim-enhanced  state=present" webservers   


So far you've seen four ansible modules: ping, command, shell, and dnf. Ansible ships with a large number of them. They are listed at 
https://docs.ansible.com/ansible/2.9/modules/list_of_all_modules.html

It looks like there are between 1000 and 2000 of them. Others can be written by third parties. The ones we've seen, with links to their documentation, are:

ping - try to connect to host, verify a usable python and return pong on success
https://docs.ansible.com/ansible/2.9/modules/ping_module.html

command - executes a command on a remote node
https://docs.ansible.com/ansible/2.9/modules/command_module.html

shell - execute commands in nodes.
https://docs.ansible.com/ansible/2.9/modules/shell_module.html

dnf - manages packages with the dnf package manager
https://docs.ansible.com/ansible/2.9/modules/dnf_module.html

 

The ansible command's -m option tells it which module to run. Each one (similar to regular commands) has its own set of arguments you can supply. When the ansible command runs a module, the -a option tells which of the module's arguments to give it. For example, the dnf module for installing/removing software packages need to know "which package?" ( dnf module's "name=  ") and for that package "which option(s)?" to perform ( dnf module's "state= "). "state= " can be set to "present" or "absent" and ansible's dnf module will install or remove the named package accordingly. Regular linux commands' options differ from one command to another. It's the same with modules. Thus, each module needs to be documented to tell what options it has and how to use them.

Modules of further interest here are are the copy, file, and setup modules.

copy - copies files to remote locations
https://docs.ansible.com/ansible/2.9/modules/copy_module.html

file - sets attributes of files
https://docs.ansible.com/ansible/2.9/modules/file_module.html

setup - gathers facts about remote hosts
https://docs.ansible.com/ansible/2.9/modules/setup_module.html


Here is the copy module. Suppose you want managed targets to use a new nameserver. They need to replace the /etc/resolv.conf file:

echo "nameserver=8.8.8.8" > new-resolv.conf
ansible -b -m copy -a "src=./new-resolv.conf dest=/etc/resolv.conf owner=root group=root mode=0644 backup=yes" $IP1

Was it effective?

ansible -b -m shell -a "ls -l /etc/resolv*" $IP1

(Why does this require the shell module? Why would the command module not do?) Run the same file-replace again:

ansible -b -m copy -a "src=./new-resolv.conf dest=/etc/resolv.conf owner=root group=root mode=0644 backup=yes" $IP1

Note that it didn't replace the file, determining that the candidate replacement was identical to the already-in-place target file. It saved effort by leaving things alone. The savings, over a large fileset and large set of target managed machines, might be a reason to use ansible copy instead of ansible shell with a direct cp command.

The file module is somewhat similar to the copy module but does all its work within the remote machine. Perhaps your machine is on New York time, based on the /etc/localtime file symlinking to the New York zoneinfo file, but you'd prefer to switch it to Los Angeles:

ansible -b -m file -a "src=/usr/share/zoneinfo/America/Los_Angeles dest=/etc/localtime owner=root group=root state=link backup=yes" $IP1

(If your managed1 machine is already on Los Angeles time you could put it instead on New York time by replacing "Los_Angeles" with "New_York" in the above command.)

If you want to know about the remote machine at the other end the setup module will dig out and return to you a very large amount of information:

ansible -b -m setup $IP1

You might filter it:

ansible -b -m setup $IP1 -a "filter=*family*"

This can reveal whether the other machine is Red Hat- or debian-based. That information could be valuable in a larger script ("playbook") that must adaptively run dnf or apt-get if it wants to install something.

Playbooks

1) Let's run a playbook. Playbooks are formatted using yaml-- yet another markup language. Here's some:

---

- hosts: webservers
  become: yes
  tasks:
   - name: this installs a package
     dnf: name=httpd state=latest

   - name: this restarts the apache service
     service: name=httpd enabled=yes state=restarted

Put it in a file named ~/filename.yaml on your managing computer (don't omit the leading triple-hyphen at the top). It invokes two modules, the ansible dnf module which we saw before and an ansible module named service that operates (starts/stops) services. It's going to install and start apache on both our managed computers because it targets the webservers group from its inventory and both computers are in it. Run it:

ansible-playbook  ~/filename.yaml

2) Here's a twist. We will have the first task, which installs the service, tell the second task, which starts it, when the install is complete. The start will then become conditional on the install. Time will not be wasted trying to start a service that didn't successfully get installed. (Take note of the "notify" below, differentiating this playbook from the previous.):

---

- hosts: webservers
  become: yes
  tasks:
   - name: this installs a package
     dnf: name=httpd state=latest
     notify: enable apache

   - name: enable apache
     service: name=httpd enabled=yes state=restarted

The notification hinges upon task name, which we have simplified to "enable apache". Put this into ~/filename.yaml and run it:

ansible-playbook  ~/ filename.yaml

3) Ansible supports variables:

---

- hosts: webservers
  become: yes
  vars:
   package_name: httpd
  tasks:
   - name: this installs a package
     dnf: name={{ package_name }} state=latest
     notify: enable apache

  handlers:
   - name: enable apache
     service: name={{ package_name }} enabled=yes state=started

Here we have put the name of the apache service into a variable and whenever we want to refer to the service pull its name out of the variable. We are anticipating the fact that on debian based linux systems "apache2" rather than "httpd" is the name used. Ansible might have to administer a mix of platforms. Put the above into ~/filename.yaml and run it:

ansible-playbook  ~/ filename.yaml

4) If we have the RedHat/debian mix, this single playbook can accommodate the differences in naming (httpd vs. apache2) and installation programs (dnf vs. apt) depending on which managed machine it is dealing with:

---

- hosts: webservers
  become: yes
  tasks:
   - name: install apache this way
     apt: name=apache2 update_cache=yes state=latest
     notify: start apache2
     when: ansible_os_family == "Debian"

   - name: install apache that way
     dnf: name=httpd state=latest
     notify: start httpd
     when: ansible_os_family == "RedHat"

  handlers:
   - name: start apache2
     service: name=apache2 enabled=yes state=started

   - name: start httpd
     service: name=httpd enabled=yes state=started

Note the conditionality is in the "when: " clause and that the determination what kind of system is targeted is automatic. Much as you did above when you ran the setup module, ansible does so when turning to a target so it knows in detail what it's dealing with on the other end. Put the above into ~/filename.yaml and run it:

ansible-playbook  ~/ filename.yaml

Note when it declares it is skipping, or skips, tasks. These are the tasks to be run on machines of the family other than that of the particular target; they are the ones that it should skip.

Ansible has features beyond these basics. However, this exercise gives an idea of its intent, operation, and scope as a system administration tool.

 

To do:

You have just hired web designer Pierre Gordon. He will be responsible for managing your webservers so you want to create an account for him on each of them. On your ansible managing machine write a script that will set him up. Put it in your home directory, /root. Name it usermgmt.sh. Here are its contents:

  useradd  pierre
  echo  'c$l@bLinuX'  |  passwd  --stdin    pierre

Now you need to get this script copied over to the webservers, where you need to give it execute permission,  then execute it. Do that with 3 ansible commands 
1) using ansible's copy module to copy the script file over, 
2) its command module to run the command that will change the files' permissions, and 
3) its shell module to run your script on the two servers.

In running these 3 ansible commands on the managing computer please suppress all output (  &> /dev/null  ), because there is too much screen messaging that will scroll your commands off the screen. I want them to remain on the screen for me to see. Once you are done, a user account pierre will be present on each managed machine, for you to ssh into. Please do that, ssh'ing into managed2 as pierre. When done take a shot of your screen. It will look just like this, except for the ellipses where I have cut out some of my syntax (show me your entire syntax):

 

[root@managing ~]# 
[root@managing ~]# ansible -b -m copy -a ... webservers &> /dev/null
[root@managing ~]# ansible -b -m command -a 'chmod ...' webservers >& /dev/null
[root@managing ~]# ansible -b -m shell -a '....sh' webservers >& /dev/null
[root@managing ~]# ssh pierre@$IP2 
pierre@10.0.0.156's password: 
Last login: Tue Aug 3 00:39:43 2021 from 10.0.0.167

to launch a graphical desktop from character mode terminal: startx

to set up a basic firewall (you have none initially): systemctl start firewalld.service

to request an IP address (enable wired networking): dhclient -v eno1

[pierre@managed2 ~]$


Name your screenshot file ansible.jpg (.png) and upload it to your assignments directory.