Recently in ceph-ansible I've been playing around with adding an entry to an existing line, in a configuration file that isn't managed by Ansible.
In general you would want to use the template module to manage a configuration file, which would mean you can completely control the contents of the file.
This isn't suitable for some configuration files, for example /etc/ssh/sshd_config
, for which you don't want to carry a template file that can get out of date, and for which you generally don't want to make too many changes, and use the package defaults as set out by your distribution.
A simple example
There are some easy options, for example the PermitEmptyPasswords
setting in /etc/ssh/sshd_config
can only have 1 value, and be specified once.
You could adjust this by doing the following:
- name: Adjust PermitEmptyPasswords setting in /etc/ssh/sshd_config
lineinfile:
dest: /etc/ssh/sshd_config
regexp: "^PermitEmptyPasswords"
line: "PermitEmptyPasswords no"
This will look for a line starting with PermitEmptyPasswords
and replace it with PermitEmtpyPasswords no
.
Note that this would add a new line if #PermitEmptyPasswords
existed as a line, since it's commented out and the line doesn't start with PermitEmptyPasswords
.
We could adjust this to include looking for lines starting with a # by doing the following:
- name: Adjust PermitEmptyPasswords setting in /etc/ssh/sshd_config
lineinfile:
dest: /etc/ssh/sshd_config
regexp: "^(#)?PermitEmptyPasswords"
line: "PermitEmptyPasswords no"
Using Regular Expressions
At this point it gets a bit more complicated unless you are familiar with Regular Expressions! Ansible uses Python Regular Expresions and you can find out more by clicking the link.
The above regular expression says the line will match if the it starts with (^
) 0 or 1 repetitions (?
) of the #
character, followed by PermitEmptyPasswords
. In other words, if it starts with a #
followed by PermitEmptyPasswords
or just PermitEmptyPasswords
without a #
.
If that regular expression is matched, replace the line with PermitEmptyPasswords no
. If the regular expression isn't matched the entry will be added to the end of the file.
If the line is exactly the same as what you are trying to add (PermitEmptyPasswords no
) then the line won't be added because the lineinfile
Ansible module will see it as no change.
How about multiple entries of the same value?
This is actually even easier! Since we don't care if other values of the setting have been specified, we can just ensure the line exists, for example:
- name: Add HostKey value to /etc/ssh/sshd_config
lineinfile:
dest: /etc/ssh/sshd_config
line: "HostKey /path/to/my/host.key"
This will add the line entry if it doesn't exist.
This has a downside, your /etc/ssh/sshd_config will have a weird ordering, and you could get options all over the show, that aren't easy to view. We can fix that though, using the insertbefore
or insertafter
setting.
- name: Add HostKey value to /etc/ssh/sshd_config in order
lineinfile:
dest: /etc/ssh/sshd_config
line: "HostKey /path/to/my/host.key"
insertafter: "^(#)?HostKey"
You'll recognize the regular expression from the last section, in this case we are using it with the insertafter
setting, which will mean our HostKey line is inserted into the file after the last entry that matches the regular expression. If the regular expression isn't matched it'll be added at the end of the file.
How about a setting that takes multiple values?
This is a bit trickier, and the crux of the article. I was playing around with it, and it occurred to me that this isn't that straight forward, especially if you are just starting out.
If you have a setting that takes multiple values in one line, for example the PRUNEPATHS
setting in /etc/updatedb.conf, which will contain a list of paths that are to be skipped when running updatedb
. When we update this line we don't want to replace the whole line, but what we want to do is ensure a specific entry is there, and update the line to include the entry if not (but not remove any existing entries!).
This is a bit trickier but can be done in 2 ways, using either the lineinfile
module as before, or the replace
module.
- name: add /var/lib/ceph to PRUNEPATHS in /etc/updatedb.conf
replace:
dest: /etc/updatedb.conf
regexp: '^(PRUNEPATHS(?!.*/var/lib/ceph).*)"$'
replace: '\1 /var/lib/ceph"'
The regular expression here is a bit more complex, but what it says is, look for a line that starts with(^
) PRUNEPATHS
, and isn't followed by (?!
) any character (.
) any number of times (*
), followed by /var/lib/ceph
, but is followed by any character (.
) any number of times (*
), followed by a "
character and the end of line ($
).
This means if /var/lib/ceph exists in the PRUNEPATHS
line we won't replace the line, but if PRUNEPATHS
exists and doesn't have /var/lib/ceph
in it, it will match and initiate the replace.
The replace line says to take the matched text (\1
) and add /var/lib/ceph"
to it, and it's that simple!
Ok great, but what are the parenthesis for?
So what are all the parenthesis (
and )
for, and is their placement important?
The parenthesis denote what will be matched - this is really important, in this example we don't want to match on the final "
otherwise we will end up with /some/path" /var/lib/ceph"
at the end of the line, which would cause problems.
So that means the line starts with (^
) and begins a match ((
) of PRUNEPATHS
that is NOT followed (?!
) by another match ((
) that consists of any character (.
) any number of times (*
), followed by /var/lib/ceph
, which is the end of the match ()
) for what can't follow PRUNEPATHS
. The initial match, which started with PRUNEPATHS
will match if any character (.
) appears any number of times (*
), which ends the text to be matched ()
), and the line ends ($
) with a "
character, which isn't included in the match.
We are using the parenthesis to manipulate what is and isn't matched by the regular expression.
If we use the lineinfile module to do that there is one extra thing we need to do, and that's enable backrefs
, as follows:
- name: add /var/lib/ceph to PRUNEPATHS in /etc/updatedb.conf using lineinfile
lineinfile:
dest: /etc/updatedb.conf
regexp: '^(PRUNEPATHS(?!.*/var/lib/ceph).*)"$'
line: '\1 /var/lib/ceph"'
backrefs: true
Conclusion
That's pretty much it! I personally like replace
, since it has backrefs
enabled by default, I think lineinfile
is more useful for multiple entries, and ensuring a specific line exists, but if you know the line exists and you want to change it, replace
works well!
Knowing your Regular Expressions is pretty much the only way to use lineinfile
and replace
to their full potential, but once you get the hang of reading them and understanding what is going on, it isn't so frightening. Hopefully that helped!
Comments