How to perform administration operations with Ansible modules

In previous tutorials we introduced Ansible and we discussed Ansible loops. This time we learn the basic usage of some modules we can use inside playbooks to perform some of the most common system administration operations.

In this tutorial you will learn:

  • How to add/modify/remove a user account with the “user” module
  • How to manage partitions with the “parted” module
  • How to execute a command with the “shell” or “command” modules
  • How to copy files or write file content using the “copy” module
  • How to manage file lines using the “lineinfile” module
How to perform administartion operations with ansible modules
How to perform administartion operations with ansible modules

Software requirements and conventions used

Software Requirements and Linux Command Line Conventions
Category Requirements, Conventions or Software Version Used
System Distribution-independent
Software Ansible
Other None
Conventions # – requires given linux-commands to be executed with root privileges either directly as a root user or by use of sudo command
$ – requires given linux-commands to be executed as a regular non-privileged user

Managing user accounts with the “user” module

When we use Ansible for provisioning and we want to manage user accounts in our playbooks, we can use the ansible.builtin.user module, which, as its full name suggests, is part of the core Ansible modules. Let’s see some examples of its usage.

Creating and modifying a user account

Suppose we want to create a task where we declare the “foo” user should exist on the target host(s) and it should be part of the wheel group, to be able to use sudo. Here is the task we would write in our playbook:

- name: Create user foo
  ansible.builtin.user:
    name: foo
    groups: wheel
    password: $6$qMDw5pdZsXt4slFl$V4RzUfqHMgSOtqpdwEeDSCZ31tfBYfiCrEfDHWyjUUEdCy7xnWpnbK54ZxpvO88n1k6EsaE0axpZQgDqkljsp0

Let’s examine what we did above. The ansible.builtin.user module parameters we used are: name, groups and password. With the first one we declared the name of the user who should be created, with the second, we passed the additional group(s) the user should be member of. Finally, with the password parameter, we specified the password of the user in crypted form. It is important to say that putting passwords directly in files is never a good practice, even if they are encrypted.




Another thing to notice is that if, for example, the task is run on a system where the “foo” user already exists and it is member of other additional groups, he will be removed from them, so that at the end of the task he will only be member of the “wheel” one. This is for the declarative nature of Ansible. In tasks we declare states, not actions, and Ansible does the necessary steps in order to achieve those states on the target machines. If we want the user to preserve its additional groups membership, we have to use another parameter: append, and use yes as its value. Here is how we would change our task:

- name: Create user foo
  ansible.builtin.user:
    name: foo
    groups: wheel
    password: $6$qMDw5pdZsXt4slFl$V4RzUfqHMgSOtqpdwEeDSCZ31tfBYfiCrEfDHWyjUUEdCy7xnWpnbK54ZxpvO88n1k6EsaE0axpZQgDqkljsp0
    append: yes

To modify the state of an existing user account, all we have to do is to change the value of the related parameters. Ansible will take care of performing the actions needed to achieve the declared states.

Removing a user account

Removing a user with the ansible.builtin.user module is simple. All we have to do is to declare that the user account should not exist on the target system(s). To do that, we use the state directive, and pass the value absent to it:

- name: Remove the foo user
  ansible.builtin.user:
    name: foo
    state: absent

The above task will make sure the user account does not exist on the target system, but will not remove directories associated with it. If this is what we want to achieve, we have to add the remove directive and pass the yes boolean value to it:

- name: Remove the foo user
  ansible.builtin.user:
    name: foo
    state: absent
    remove: yes

Managing partitions with the “parted” module

Another very common operation is the creation and manipulation of block device partitions. Using Ansible, we can perform such operations via the community.general.parted module. Let’s see some examples. Suppose we want to create a partition on the /dev/sda disk. Here is what we would write:

- name: Partition /dev/sda
  community.general.parted:
    device: /dev/sda
    number: 1
    state: present

The first parameter we used in the example is device. This is mandatory and we use it to specify on which disk the task should be performed. With the number directive we specify which partition should be modified or created. Finally, with the state directive we declare what its state should be. In this case we used “present” as value, so the partition will be created if it doesn’t already exist.

Specifying partition dimensions

As you may have noticed, there are two things missing in the example: we didn’t specify where the partition should start and where it should end. To specify the partition offset, we must add the part_start and part_end parameters. If we don’t, just like in the example above, the partition will start at the beginning of the disk (the default value for part_start is “0%”) and will take all the available space on the disk (default value for part_end is 100%). Suppose we want to make the partition start at 1MiB from the beginning of the disk and take all the available space; here is how we would change our task:

- name: Create a partition /dev/sda
  community.general.parted:
    device: /dev/sda
    number: 1
    state: present
    part_start: 1MiB

The value provided to the part_start parameter can be either in percentage form, or a number followed by one of the units supported by the parted program, (MiB, GiB, etc…) If the provided value is in negative form, it will be considered as the distance from the end of the disk.

What if we want to resize a partition? As we said before, Ansible works in a declarative way, so all we have to do is to specify the new size of the partition via the part_end directive. Additionally we want to add the resize parameter, and set it to yes. Supposing we want to resize the partition we created in the previous example to 50GiB we would write:

- name: Resize the first partition of /dev/sda to 50GiB
  community.general.parted:
    device: /dev/sda
    number: 1
    state: present
    part_end: 50GiB
    resize: yes

Removing a partition

Finally, to remove an existing partition, all we have to do is to use the state parameter and set it to “absent”. To remove the partition we created in the previous examples, we would write:

- name: Remove the first partition of /dev/sda
  community.general.parted:
    device: /dev/sda
    number: 1
    state: absent

Executing commands with the command or shell modules

As we said before, in the vast majority of cases, in Ansible tasks, we specify a certain state we want to obtain rather the specific commands needed to achieve that. Sometimes, however, we may want to perform some commands explicitly. In those cases we can use the ansible.builtin.command or ansible.builtin.shell modules.




These modules let us achieve the same goal, but work differently. The commands we execute via the shell module will be interpreted by a shell, so variable expansions and redirections will work just as they would when we launch them manually (sometimes this could cause security issues). When we use the command module the shell will not be involved, so it is the recommended method to use, except in those cases when we specifically need shell features.

Suppose we want to write a task to automate the re-build of the system initramfs. Here is what we could write, supposing the system is Fedora, where the action is achieved via the dracut command:

- name: Regenerate initramfs
  ansible.builtin.command:
    cmd: dracut --regenerate-all --force

In the example above, we passed the command as a string. This is what is called “free form”. Commands can also be passed as a list, similarly to what we do when we use the Python subprocess module. We could rewrite the above as follows using the argv parameter:

- name: Regenerate initramfs
  ansible.builtin.command:
    argv:
      - dracut
      - --regenerate-all
      - --force

As we said, the same task can be performed by using the shell module. This let us use all features available in the shell itself, such as redirections. Suppose, for example, we want to perform the same action but redirect both the standard error and standard output of the command to the /var/log/log.txt file. Here is what we could write:

- name: Regenerate initramfs and redirect
  ansible.builtin.shell:
    cmd: dracut --regenerate-all --force --verbose &> /var/log/log.txt

Copying files

When we need to write Ansible tasks to copy files we can use the ansible.builtin.copy module. The main directives of this module are: src and dest. As you can imagine, with the former we specify the path of the file which should be copied, and with the latter, the absolute path where it should be copied on the target systems. If we specify a directory path as source the directory itself with all its content will be copied, unless the path ends with a slash (/). In that case, only the directory content will copied. Suppose we want to copy the /foo.conf file to the destination hosts as /etc/foo.conf. We would write:

- name: Copy /foo.conf to /etc/foo.conf
  ansible.builtin.copy:
    src: /foo.conf
    dest: /etc/foo.conf

We can specify what owner and permissions the copied file should have on the remote system. This is achieved by using the owner, group and mode directives. Suppose we want to assign the copied file to the “bar” user and group, with 600 as permission mode:

- name: Copy /foo.conf to /etc/foo.conf with specific permissions and owner
  ansible.builtin.copy:
    src: /foo.conf
    dest: /etc/foo.conf
    owner: bar
    group: bar
    mode: 0600

One important thing to notice in the example above, is how we specified the permission mode. To make sure it is parsed as an octal number by the Ansible yaml parser, we added a leading 0 to the mode. Alternatively its possible to pass the mode as a string between quotes or use the symbolic notation (u=rw).

Specifying file content directly

One interesting thing that is possible to do with the copy module is to actually specify the content of the destination file directly instead of copying an existing file from source. To achieve such result we have to use the content directive. Just as an example suppose we want the remote /etc/foo.conf file to have the “Hello World” content (the file will be created if it doesn’t exist), we would write:

- name: Specify /etc/foo.conf file content
  ansible.builtin.copy:
    dest: /etc/foo.conf
    content: "Hello World\n"

Managing file lines using the “lineinfile” module

To manipulate file lines we can use the ansible.builtin.lineinfile module. Let’s see some examples of its usage. Imagine the /etc/foo.conf file contains the following lines:

one
two
three
four

Now, suppose we want to remove the line beginning with the “four” word. We would write:

- name: Ensure the lines starting with the word "four" don't exist in /etc/foo.conf
  ansible.builtin.lineinfile:
    path: /etc/foo.conf
    regexp: ^four
    state: absent

With the path parameter we specified the path of the remote file the action should take place. The regexp parameter, instead, is used to pass the regular expression which should match the pattern in the line(s) we want to operate on. In this case we passed a regular expression which will match all lines starting with the word “four”; they will be all removed, since we passed “absent” as the value of the state parameter.




Suppose we want to replace the line starting with “four” with a different content, instead, perhaps with: “deleted by task”. To achieve the result we use the line parameter:

- name: Substitute  "four" with "deleted by task" in /etc/foo.conf
  ansible.builtin.lineinfile:
    path: /etc/foo.conf
    regexp: ^four
    line: "deleted by task"

What if the file contained more that one line with a match? In those cases, when the value of the state parameter is “present” (the default), the replacement will take place only on the last matched line.

Conclusions

In this article we saw how to perform some common system administration tasks such as managing user accounts and partitions, executing commands, copying files and modifying their lines with Ansible using the appropriate modules. This was not meant to be an exhaustive guide, since we explored only the very basic functionalities of the modules we mentioned. For a complete overview of them you can consult the official module docs.



Comments and Discussions
Linux Forum