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!