devilkin
devilkin

Reputation: 57

Ansible lineinfile regexp to manage /etc/exports

I've been hitting a wall trying to get /etc/exports managed via Ansible.

I've got a role that installs a piece of software on a VM, and I want to then add an entry ot /etc/exports on the NFS server, for that specific VM, so it's able to access the NFS shares needed.

Lineinfile sounds like the way to go, but sofar I can't figure out how to properly write this.

I want this to:

The latest installment of my 'add to /etc/exports' that thought should work, but doesn't, is:

- name: Add hosts to mountpoint line
  ansible.builtin.lineinfile:
    path: /etc/exports
    line: '\1 {{ host_ip }}(root_squash,no_subtree_check)'
    regex: '^((?!{{ volume_mountpoint }}.*{{ host_ip }}\(root_squash,no_subtree_check\).*).*)$'
    backrefs: yes

but i'm still getting all kinds of weird side effects. I've used backreferences etc before, but somehow this one keeps tripping me up.

Anyone who sees what's gong wrong?

Typical /etc/exports entry:

/srv/files    172.16.0.14(rw,no_root_squash,no_subtree_check)

Upvotes: 1

Views: 482

Answers (1)

Vladimir Botka
Vladimir Botka

Reputation: 68179

It's not possible in one step to modify a line using backreferences or add the line if missing. To modify the existing mount point the back-references are needed. For example, given the files for testing

shell> cat etc/export1
/srv/files    172.16.0.14(rw,no_root_squash,no_subtree_check)

shell> cat etc/export2
/srv/files    172.16.0.15(rw,no_root_squash,no_subtree_check)

shell> cat etc/export3
/srv/download    172.16.0.14(rw,no_root_squash,no_subtree_check)

the task

  tasks:
    - lineinfile:
        path: "etc/{{ item }}"
        regex: '^{{ mount }}(\s+)({{ ipr }})*({{ optionsr }})*(\s*)(.*)$'
        line: '{{ mount }}\g<1>{{ ip }}{{ options }} \g<5>'
        backrefs: true
      vars:
        mount: /srv/files
        ipr: '172\.16\.0\.14'
        ip: '172.16.0.14'
        optionsr: '\(.*?\)'
        options: '(root_squash,no_subtree_check)'
      loop:
        - export1
        - export2
        - export3

gives

--- before: etc/export1 (content)
+++ after: etc/export1 (content)
@@ -1 +1 @@
-/srv/files    172.16.0.14(rw,no_root_squash,no_subtree_check)
+/srv/files    172.16.0.14(root_squash,no_subtree_check) 

changed: [localhost] => (item=export1)
--- before: etc/export2 (content)
+++ after: etc/export2 (content)
@@ -1 +1 @@
-/srv/files    172.16.0.15(rw,no_root_squash,no_subtree_check)
+/srv/files    172.16.0.14(root_squash,no_subtree_check) 172.16.0.15(rw,no_root_squash,no_subtree_check)

changed: [localhost] => (item=export2)
ok: [localhost] => (item=export3)

The first two files are all right. The problem is the third file. The line hasn't been added to the file. Quoting from backrefs

"... if the regexp does not match anywhere in the file, the file will be left unchanged."

The explanation is simple. There are no groups if the regex doesn't match. If there are no groups the line can't be created.

On the other hand, quoting from regexp

"... If the regular expression is not matched, the line will be added to the file ..."

As a result, it's not possible to ask lineinfile to add a line if the regexp does not match and, at the same time, to do nothing if the regexp is matched. If the regexp is matched you need back-references. If you use back-references you can't add a missing line.

To solve this problem read the content of the files and create a dictionary

    - command: "cat etc/{{ item }}"
      register: result
      loop: [export1, export2, export3]
    - set_fact:
        content: "{{ dict(_files|zip(_lines)) }}"
      vars:
        _lines: "{{ result.results|map(attribute='stdout_lines')|list }}"
        _files: "{{ result.results|map(attribute='item')|list }}"

gives

  content:
    export1:
    - /srv/files    172.16.0.14(rw,no_root_squash,no_subtree_check)
    export2:
    - /srv/files    172.16.0.15(rw,no_root_squash,no_subtree_check)
    export3:
    - /srv/download    172.16.0.14(rw,no_root_squash,no_subtree_check)

Now add the line only if missing, i.e. do not replace the line if the mount point is already there

   - lineinfile:
        path: "etc/{{ item }}"
        line: '{{ mount }} {{ ip }}{{ options }}'
      vars:
        mount: /srv/files
        ip: '172.16.0.14'
        options: '(root_squash,no_subtree_check)'
      loop: "{{ content|list }}"
      when: content[item]|select('search', mount)|length == 0

gives

skipping: [localhost] => (item=export1) 
skipping: [localhost] => (item=export2) 
--- before: etc/export3 (content)
+++ after: etc/export3 (content)
@@ -1 +1,2 @@
 /srv/download    172.16.0.14(rw,no_root_squash,no_subtree_check)
+/srv/files 172.16.0.14(root_squash,no_subtree_check)

Upvotes: 1

Related Questions