devilkin
devilkin

Reputation: 57

Ansible line-in-file re-adding entries

What I'm trying to solve: I have several servers (in this example 3, 1 nfs server, 2 clients - they all resolve back to localhost for this minimal example), and the clients need to access shares on the server, which are created using the playbook.

The IP addresses of the clients need to be added to the respective entries in /etc/exports as they go - the list is not predefined at any given time. (In my actual playbook I use ansible facts, for this example I've added them as a variable)

In Ansible lineinfile regexp to manage /etc/exports Vladimir was so kind as to help with an initial thing, which works, but doesn't seem to be idempotent. The entries / ip addresses are added correctly the first time round, but the second run the IP addresses get re-added to the entries in /etc/exports, causing nfs to bomb out at that time (double entries)

Correct /etc/exports:

bar 192.168.34.47(rw,sync,no_root_squash,no_subtree_check) 192.168.34.46(rw,sync,no_root_squash,no_subtree_check) 
foo 192.168.34.47(rw,sync,no_root_squash,no_subtree_check) 192.168.34.46(rw,sync,no_root_squash,no_subtree_check)

What I'm getting:

bar 192.168.34.47(rw,sync,no_root_squash,no_subtree_check) 192.168.34.46(rw,sync,no_root_squash,no_subtree_check) 192.168.34.47(rw,sync,no_root_squash,no_subtree_check) 192.168.34.46(rw,sync,no_root_squash,no_subtree_check)
foo 192.168.34.47(rw,sync,no_root_squash,no_subtree_check) 192.168.34.46(rw,sync,no_root_squash,no_subtree_check) 192.168.34.47(rw,sync,no_root_squash,no_subtree_check) 192.168.34.46(rw,sync,no_root_squash,no_subtree_check)

I've been butting my head around it but I can't come up with anything that works. I've distilled it down to the following playbook:

$ ansible-playbook main.yml

Content of the main.yml:

- hosts: localhost
  become: yes
  vars:
    client_ip: 192.168.34.46
    nfs_server: localhost
    shares:
      - bar
      - foo
  tasks:
  - name: Mountpoint management
    ansible.builtin.include_tasks: task.yml
    loop: "{{ shares | default([]) }}"
    loop_control:
      loop_var: volume
    args:
      apply:
        delegate_to: "{{ nfs_server }}"
        
- hosts: localhost
  become: yes
  vars:
    client_ip: 192.168.34.47
    nfs_server: localhost
    shares:
      - bar
      - foo

  tasks:
  - name: Mountpoint management
    ansible.builtin.include_tasks: task.yml
    loop: "{{ shares | default([]) }}"
    loop_control:
      loop_var: volume
    args:
      apply:
        delegate_to: "{{ nfs_server }}"

Second file task.yml which is needed to do a loop:

---
---
- name: Ensure /etc/exports exists
  ansible.builtin.file:
    path: /etc/exports
    owner: root
    group: root
    mode: '0644'
    state: touch
  changed_when: False

- name: Add host {{ client_ip }} to {{ volume }}
  ansible.builtin.lineinfile:
    path: "/etc/exports"
    regex: '^{{ volume }}(\s+)({{ ip_regex }})*({{ mount_opts_regex }})*(\s*)(.*)$'
    line: '{{ volume }}\g<1>{{ ip }}{{ mount_opts }} \g<5>'
    backrefs: true
  vars:
    ip: "{{ client_ip }}"
    ip_regex: '{{ client_ip | regex_escape() }}'
    mount_opts: '(rw,sync,no_root_squash,no_subtree_check)'
    mount_opts_regex: '\(.*?\)'


- name: Read /etc/exports
  command: "cat {{ item }}"
  register: result
  check_mode: no
  loop:
    - /etc/exports
  changed_when: False

- ansible.builtin.set_fact:
    content: "{{ dict(_files|zip(_lines)) }}"
  vars:
    _lines: "{{ result.results|map(attribute='stdout_lines')|list }}"
    _files: "{{ result.results|map(attribute='item')|list }}"

- name: Add new line to /etc/exports
  ansible.builtin.lineinfile:
    path: "/etc/exports"
    line: '{{ volume }} {{ client_ip }}{{ mount_opts }}'
  vars:
    mount_opts: '(rw,sync,no_root_squash,no_subtree_check)'
  loop: "{{ content }}"
  when: content[item] | select('search', volume)|length == 0

Upvotes: 0

Views: 126

Answers (1)

devilkin
devilkin

Reputation: 57

Well, thinking it through I finally went with constructing two lists, mapping those into a dict and checking if the values are already present. If they are, don't do anything, if not, add it.

main.yaml:

- hosts: localhost
  become: yes
  vars:
    client_ip: 192.168.34.46
    nfs_server: localhost
    shares:
      - bar
      - foo
  tasks:
  - name: Mountpoint management
    ansible.builtin.include_tasks: task.yml
    loop: "{{ shares | default([]) }}"
    loop_control:
      loop_var: volume
    args:
      apply:
        delegate_to: "{{ nfs_server }}"
        
- hosts: localhost
  become: yes
  vars:
    client_ip: 192.168.34.47
    nfs_server: localhost
    shares:
      - bar
      - foo

  tasks:
  - name: Mountpoint management
    ansible.builtin.include_tasks: task.yml
    loop: "{{ shares | default([]) }}"
    loop_control:
      loop_var: volume
    args:
      apply:
        delegate_to: "{{ nfs_server }}"

task.yaml:

---
- name: Ensure /etc/exports exists
  ansible.builtin.file:
    path: /etc/exports
    owner: root
    group: root
    mode: '0644'
    state: touch
  changed_when: false

- name: Read /etc/exports
  command: "cat /etc/exports"
  register: result
  check_mode: no
  changed_when: false

- ansible.builtin.set_fact:
    share_names: "{{ share_names | default([]) + [item.split(' ')[0]] }}"
    share_values: "{{ share_values | default([]) + [item.split(' ')[1:]] }}"
  loop:
    "{{ result.stdout_lines }}"

- ansible.builtin.set_fact:
    share_content: "{{ dict(share_names | zip(share_values)) }}"

- name: Add host {{ client_ip }} to {{ volume }}
  ansible.builtin.lineinfile:
    path: "/etc/exports"
    regex: '^{{ volume }}(\s+)({{ ip_regex }})*({{ mount_opts_regex }})*(\s*)(.*)$'
    line: '{{ volume }}\g<1>{{ client_ip }}{{ mount_opts }} \g<5>'
    backrefs: true
  vars:
    ip_regex: '{{ client_ip | regex_escape() }}'
    mount_opts: '(rw,sync,no_root_squash,no_subtree_check)'
    mount_opts_regex: '\(.*?\)'
    str: '{{ client_ip }}{{ mount_opts }}'
  when: volume in share_content and str not in share_content[volume]

- name: Add new line to /etc/exports
  ansible.builtin.lineinfile:
    path: "/etc/exports"
    line: '{{ volume }} {{ client_ip }}{{ mount_opts }}'
  vars:
    mount_opts: '(rw,sync,no_root_squash,no_subtree_check)'
  loop: "{{ share_content }}"
  when: volume not in share_content

Upvotes: 0

Related Questions