Using sudoedit in Ansible
Imagine you are at work. After years of arguing you finally get people to use systemd to start services at boot instead of simply hoping the servers never go down. You even convinced the admins to give you a sudoers rule akin to:
merlin ALL=(root) sudoedit /etc/systemd/system/merlin-*, \
/usr/bin/systemctl start merlin-*, \
/usr/bin/systemctl stop merlin-*, \
/usr/bin/systemctl restart merlin-*, \
/usr/bin/systemctl enable merlin-*, \
/usr/bin/systemctl disable merlin-*, \
/usr/bin/systemctl status merlin-*, \
/usr/bin/systemctl daemon-reload
So far so good, you can now edit the service files and even start/stop them. Good luck manually editing the service files on your fleet of hundreds of servers.
Fortunately, you thought ahead and started using Ansible years ago. But how do you get Ansible scripts (which you cannot run as root) to edit your service files? After all, you only have the rights to sudoedit instead of proper write permissions on the files.
You can solve this by overriding EDITOR:
---
- name: Create temporary service file
ansible.builtin.tempfile:
state: file
register: util_service_temp_file
- name: Template the service file
ansible.builtin.template:
src: "{{ service_template }}.j2"
dest: "{{ util_service_temp_file.path }}"
# https://www.freedesktop.org/software/systemd/man/latest/systemd-analyze.html
validate: 'systemd-analyze verify %s:{{ service_name }}'
mode: '0700'
- name: Install all service files via sudoedit stub
ansible.builtin.command:
cmd: "sudoedit /etc/systemd/system/{{ service_name }}"
environment:
EDITOR: sh -c "cat \$SOURCE > \$0"
SOURCE: "{{ util_service_temp_file.path }}"
- name: Reload systemd service files
# We have to do it via command as otherwise we will not pass the sudoers rules
ansible.builtin.command:
cmd: "sudo systemctl daemon-reload"
This works. Why though?
What does sudoedit even do?
To understand what’s going on let’s take a look at sudoedit since it’s load-bearing in this post.
When you run
EDITOR=nano sudoedit /root/file.txt
sudoedit does three things:
- Copy the requested file (as root) to a temporary location
- Open your desired
EDITOR(as the invoking user) pointing to that location ($EDITOR $1) - After the
EDITORprocess has ended, check if the temporary file has been modified and if so, copy it back (as root) to the original location, overriding the old file
Using this to our advantage
Usually, the requested editor is just this - a text editor like nano, vim, helix. It doesn’t have to be though. In my first iterations it was a simple bash file:
#!/usr/bin/env bash
# edit.sh
cat /tmp/new-service-file > $1
This fits nicely into the usual sudoedit flow. sudoedit copies the file to some temporary location and runs edit.sh with the temporary path as its only argument. edit.sh just writes our desired contents into the temporary file, which doesn’t need any root privileges at that point. After edit.sh has exited, sudoedit helpfully copies the edited file back to its original location.
Now, just package that up into Ansible, make the service file a template, replace the separate EDITOR script with a call to sh -c, and add some usual systemd logic like reloading the service files after you’re done.
Just like this we can deploy our hundreds of service files without SSHing into every single server.