Reputation: 57
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
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