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
Software requirements and conventions used
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.