Use Ansible to ensure a file exists, ignoring any extra lines - ansible

I'm trying to update the sssd.conf file on about 200 servers with a standardized configuration file, however, there is one possible exception to the standard. Most servers will have a config that looks like this:
[domain/domainname.local]
id_provider = ad
access_provider = simple
simple_allow_groups = unixsystemsadmins, datacenteradmins, sysengineeringadmins, webgroup
default_shell = /bin/bash
fallback_homedir = /export/home/%u
debug_level = 0
ldap_id_mapping = false
case_sensitive = false
cache_credentials = true
dyndns_update = true
dyndns_refresh_interval = 43200
dyndns_update_ptr = true
dyndns_ttl = 3600
ad_use_ldaps = True
[sssd]
services = nss, pam
config_file_version = 2
domains = domainname.local
[nss]
[pam]
However, on some servers, there's an additional line after simple_allow_groups called simple_allow_users, and each server that has this line has it configured for specific users to be allowed to connect without being a member of an LDAP group.
My objective is to replace the sssd.conf file on all servers, but not to remove this simple_allow_users line, if it exists. I looked into lineinfile and blockinfile, but neither of these seems to really handle this exception. I'm thinking I'm going to have to check the file for the existance of the line, store it to a variable, push the new file, and then add the line back, using the variable afterwards, but I'm not entirely sure if this is the best way to handle it. Any suggestions on the best way to accomplish what I'm looking to do?
Thanks!

I would do the following
See if the simple_allow_users exists in the current sssd.conf file
Change your model configuration to add the current value of the line simple_allow_users is exists
overwrite the sssd.conf file with the new content
You can use jinja2 conditional to achieve step 2 https://jinja2docs.readthedocs.io/
I beleive the above tasks will solve what you need, just remember to test on a simngle host and backup the original file just for good measure ;-)
- shell:
grep 'simple_allow_users' {{ sssd_conf_path }}
vars:
sssd_conf_path: /etc/sssd.conf
register: grep_result
- set_fact:
configuration_template: |
[domain/domainname.local]
id_provider = ad
access_provider = simple
simple_allow_groups = unixsystemsadmins, datacenteradmins, sysengineeringadmins, webgroup
{% if 'simple_allow_users' in grep_result.stdout %}
{{ grep_result.stdout.rstrip() }}
{% endif %}
default_shell = /bin/bash
..... Rest of your config file
- copy:
content: "{{ configuration_template }}"
dest: "{{ sssd_conf_path }}"
vars:
sssd_conf_path: /etc/sssd.conf

I used Zeitounator's tip, along with this question Only check whether a line present in a file (ansible)
This is what I came up with:
*as it turns out, the simple_allow_groups are being changed after the systems are deployed (thanks for telling the admins about that, you guys... /snark for the people messing with my config files)
---
- name: Get Remote SSSD Config
become: true
slurp:
src: /etc/sssd/sssd.conf
register: slurpsssd
- name: Set simple_allow_users if exists
set_fact:
simpleallowusers: "{{ linetomatch }}"
loop: "{{ file_lines }}"
loop_control:
loop_var: linetomatch
vars:
- decode_content: "{{ slurpsssd['content'] | b64decode }}"
- file_lines: "{{ decode_content.split('\n') }}"
when: '"simple_allow_users" in linetomatch'
- name: Set simple_allow_groups
set_fact:
simpleallowgroups: "{{ linetomatch }}"
loop: "{{ file_lines }}"
loop_control:
loop_var: linetomatch
vars:
- decode_content: "{{ slurpsssd['content'] | b64decode }}"
- file_lines: "{{ decode_content.split('\n') }}"
when: '"simple_allow_groups" in linetomatch'
- name: Install SSSD Config
copy:
src: etc/sssd/sssd.conf
dest: /etc/sssd/sssd.conf
owner: root
group: root
mode: 0600
backup: yes
become: true
- name: Add simple_allow_users back to file if it existed
lineinfile:
path: /etc/sssd/sssd.conf
line: "{{ simpleallowusers }}"
insertafter: "^simple_allow_groups"
when: simpleallowusers is defined
become: true
- name: Replace simple allow groups with existing values
lineinfile:
path: /etc/sssd/sssd.conf
line: "{{ simpleallowgroups }}"
regexp: "^simple_allow_groups"
backrefs: true
when: simpleallowgroups is defined
become: true

Related

Ansible trim string

I'm trying write role to install MySql 8, and get problem with this:
- name: Extract root password from logs into {{ mysql_root_old_password }} variable
ansible.builtin.slurp:
src: "{{ mysql_logfile_path }}"
register: mysql_root_old_password
#when: "'mysql' in ansible_facts.packages"
- name: Extract root password from logs into {{ mysql_root_old_password }} variable
set_fact:
mysql_root_old_password: "{{ mysql_root_old_password.content | b64decode | regex_findall('generated for root#localhost: (.*)$', 'multiline=true') }}"
#when: "'mysqld' in ansible_facts.packages"
- name: Get Server template
ansible.builtin.template:
src: "{{ item.name }}.j2"
dest: "{{ item.path }}"
loop:
- { name: "my.cnf", path: "/root/.my.cnf" }
notify:
- Restart mysqld
on the .my.cnf I get password with quotes and brackets:
[client]
user=root
password=['th6k(gZeJSt4']
How to trim that?
What I try:
- name: trim password
set_fact:
mysql_root_old_password2: "{{ mysql_root_old_password | regex_findall('[a-zA-Z0-9,()!##$%^&*]{12}')}}"
Thanks.
The result of regex_findall is a list because there might be more matches. Take the last item
- set_fact:
mysql_root_old_password: "{{ mysql_root_old_password.content|
b64decode|
regex_findall('generated for root#localhost: (.*)$', 'multiline=true')|
last }}"
From your description
on the .my.cnf I get password with quotes and brackets ... How to trim that
I understand that you like to read a INI file like my.cnf.ini
[client]
user=root
password=['A1234567890B']
where the value of the key password looks like a list with one element in YAML and the structure doesn't change, but you are interested in the value without leading and trailing square brackets and single quotes only.
To do so there are several possibilities.
Via Ansible Lookup plugins
---
- hosts: localhost
become: false
gather_facts: false
tasks:
- name: Extract root password from INI file
debug:
msg: "{{ lookup('ini', 'password section=client file=my.cnf.ini') }}"
register: result
- name: Show result with type
debug:
msg:
- "{{ result.msg }}"
- "result.msg is of type {{ result.msg | type_debug }}"
- "Show password only {{ result.msg[0] }}" # the first list element
Such approach will work on the Control Node.
Like all templating, lookups execute and are evaluated on the Ansible control machine.
Further Q&A
How to read a line from a file into an Ansible variable
What is the difference between .ini and .conf?
Further Documentation
ini lookup – read data from an INI file
Via Ansible shell module, sed and cut.
---
- hosts: localhost
become: false
gather_facts: false
tasks:
- name: Extract root password from INI file
shell:
cmd: echo $(sed -n 's/^password=//p' my.cnf.ini | cut -d "'" -f 2)
register: result
- name: Show result
debug:
msg: "{{ result.stdout }}"
Please take note regarding "I get password with quotes and brackets ... ['<pass>'] ... How to trim that?" that from perspective of the SQL service, .cnf file, [' and '] are actually part of the password!
- name: Server template
template:
src: "my.cnf.ini"
dest: "/root/.my.cnf"
If that is correct they will be necessary for proper function of the service.

Data from vault with with_items groups

I have a task that generates my configuration from jinja2 to conf.
- name: check password
set_fact:
my_secrets: "{{ lookup('hashi_vault', 'secret=kv/{{ stage }}.d/{{ app }}/{{ item }}/secrets token={{ token }} url={{ url }} validate_certs={{ validate_certs }}')}}"
with_items: "{{ groups['ns'] }}"
- name: copy config powerdns_auth pdns.local.gmysql.conf
template:
src: ../../../update/ns/templates/etc/powerdns/pdns.d/pdns.local.gmysql.conf.j2
dest: ../../../config/{{ stage }}/{{ item }}/etc/powerdns/pdns.d/pdns.local.gmysql.conf
mode: '0644'
with_items: "{{ groups['ns'] }}"
in pdns.local.gmysql.conf.j2
gmysql-password={{ my_secrets.user_password_mysql }}
I have a problem because it saves me the from vault password from the last host to a file.
Is it possible to set the fact depending on the host?
Don't loop over groups, use the "natural" play loop on hosts and delegate the needed tasks to localhost.
Note: I kept your relative paths in the template tasks but it looks ugly and will break one day or an other.
Note2: "moustaches don't stack" => I fixed your code (there are other ways to fix it...) where it was incorrect when fetching from hashicorp vault.
- name: Create config files per hosts
hosts: ns
gather_facts: false
vars:
# All your needed vars that I will not define here for this example
tasks:
- name: check password
vars:
secret: "kv/{{ stage }}.d/{{ app }}/{{ inventory_hostname }}/secrets"
hashi_string: "secret={{ secret }} token={{ token }} url={{ url }} validate_certs={{ validate_certs }}"
set_fact:
my_secrets: "{{ lookup('hashi_vault', hashi_string) }}"
- name: copy config powerdns_auth pdns.local.gmysql.conf
template:
src: ../../../update/ns/templates/etc/powerdns/pdns.d/pdns.local.gmysql.conf.j2
dest: ../../../config/{{ stage }}/{{ inventory_hostname }}/etc/powerdns/pdns.d/pdns.local.gmysql.conf
mode: '0644'
delegate_to: localhost

Run a task first time we execute the script but not after that

Background:
We are providing an Ansible utility for the admins to add or remove comments in motd file. We want to restrict any direct edits to motd file. Since there can be previous comments we want to retain them. This means that we parse the file only once and capture existing comments. After which the admins have to use the tool to add/delete comments. Any comments directly added to the file will be discarded.
Requirement:
I have this block which needs to run only once. Not once per execution but once only for many executions. In other words, it should run the first time we execute the script but not after that.
Approach:
To accomplish this, I defined a flag variable and initialized it to 0 like this common_motd_qsc_flag: 0 in defaults/mail.yml. Once I executed a particular task I am trying to update the variable to 1 like this common_motd_qsc_flag: 1. Within the task, I am making sure that the task is executed only when the flag variable is 0 in using the when condition.
Problem:
Every time the script executes it is still running the task that shouldn't be run. I understand why this is happening. It is because during the start of the script it is reading common_motd_qsc_flag: 0 in defaults/main.yml.
Question:
Is there a way to update common_motd_qsc_flag: 1 in defaults/main.yml without using lineinfile module? Any alternative approaches are also appreciated if this an ugly way to handle this requirement.
tasks/main.yml:
- name: Parse all existing comments from /etc/motd
shell: tail --lines=+10 "{{ common_motd_qsc_motd_file }}"
register: existing_comments
when:
- motd_file.stat.exists == True
- common_motd_qsc_flag == 0 # defaults
- name: Update flag variable
set_fact:
common_motd_qsc_flag: 1
when: common_motd_qsc_flag == 0
- name: Add existing comments to the array
set_fact:
common_motd_qsc_comments_array: "{{ common_motd_qsc_comments_array | union([t_existing_entry]) }}"
loop: "{{ existing_comments.stdout_lines }}"
when:
- not t_existing_entry is search('Note:')
- not t_existing_entry is search('APPTYPE:')
- not t_existing_entry is search('Comments:')
- t_existing_entry not in common_motd_qsc_comments_array
vars:
t_existing_entry: "{{ item | trim }}"
defaults/main.yml:
common_motd_qsc_flag: 0
I was able to fix this using local facts as per your advice. Thanks much for the pointer. Here is the working code:
- name: Parse all existing comments from /etc/motd
shell: tail --lines=+10 "{{ common_motd_qsc_motd_file }}"
register: existing_comments
when:
- t_common_motd_qsc_check_qsc_file.stat.exists == True
- ansible_local['snps'] is defined
- ansible_local['snps']['cache'] is defined
- ansible_local['snps']['cache']['common_motd_qsc_flag'] is not defined
changed_when: false
- name: Add existing comments to the array
set_fact:
common_motd_qsc_comments_array: "{{ common_motd_qsc_comments_array | union([t_existing_entry]) }}"
loop: "{{ existing_comments.stdout_lines }}"
when:
- ansible_local['snps'] is defined
- ansible_local['snps']['cache'] is defined
- ansible_local['snps']['cache']['common_motd_qsc_flag'] is not defined
- not t_existing_entry is search('Note:')
- not t_existing_entry is search('APPTYPE:')
- not t_existing_entry is search('Comments:')
- t_existing_entry not in common_motd_qsc_comments_array
vars:
t_existing_entry: "{{ item | trim }}"
- name: Set common_motd_qsc_flag to facts file
ini_file:
dest: "/etc/ansible/facts.d/snps.fact"
section: 'cache' # [header]
option: 'common_motd_qsc_flag' # key
value: "1" # value
- name: Add a new comment if it does not exist
set_fact:
common_motd_qsc_comments_array: "{{ common_motd_qsc_comments_array | union([t_new_entry]) }}"
loop: "{{ common_motd_qsc_add_comment }}"
when:
- t_new_entry not in common_motd_qsc_comments_array
- t_new_entry|length > 0
vars:
t_new_entry: "{{ item | trim }}"
- name: Delete an existing comment
set_fact:
common_motd_qsc_comments_array: "{{ common_motd_qsc_comments_array | difference([t_new_entry]) }}"
loop: "{{ common_motd_qsc_delete_comment }}"
when:
- t_new_entry in common_motd_qsc_comments_array
- t_new_entry|length > 0
vars:
t_new_entry: "{{ item | trim }}"
- name: Save comments to snps.fact file
ini_file:
dest: "/etc/ansible/facts.d/snps.fact"
section: 'motd' # [header]
option: 'common_motd_qsc_comment_array' # key
value: "{{ common_motd_qsc_comments_array }}" # value

Ansible, iterate over multiple registers and make extended use of build in methods

To start off I have all my variables defined in YAML
app_dir: "/mnt/{{ item.name }}"
app_dir_ext: "/mnt/{{ item.0.name }}"
workstreams:
- name: tigers
service_workstream: tigers-svc
app_sub_dir:
- inbound
- inbound/incoming
- inbound/error
- inbound/error/processed
- name: lions
service_workstream: lions-svc
app_sub_dir:
- inbound
- inbound/incoming
- inbound/error
- inbound/error/processed
You may note app_dir: "/mnt/{{ item.name }}" and app_dir_ext: "/mnt/{{ item.0.name }}" looking odd, so I originally had my variables set as below in YAML but decided to use the above mainly due to less lines in YAML when I have a large amount of workstreams.
workstreams:
- name: tigers
service_workstream: tigers-svc
app_dir: /mnt/tigers
...
I then have Ansible code to check if the directories exists, if not create them and apply permissions (!note, have taken this approach due to a ssh timeout on operation when using the file: module on a number of very big NFS mounted shares).
- name: Check workstreams app_dir
stat:
path: "{{ app_dir }}"
register: app_dir_status
with_items:
- "{{ workstreams }}"
- name: Check workstreams app_sub_dir
stat:
path: "{{ app_dir_ext }}/{{ item.1 }}/"
register: app_sub_dir_status
with_subelements:
- "{{ workstreams }}"
- app_sub_dir
- name: create workstreams app_dir
file:
path: "/mnt/{{ item.0.name }}"
state: directory
owner: "ftp"
group: "ftp"
mode: '0770'
recurse: yes
with_nested:
- '{{ workstreams }}'
- app_dir_status.results
when:
- '{{ item.1.stat.exists }} == false'
This is a little hacky but works, however I have a 2nd, 3rd, 4th path to check..etc
My question here is how to I update/refactor the above code to use <register_name>.stat.exists == false from both app_dir_status and app_sub_dir_status to control my task ?
You don't need to make nested loop! There's all required data inside app_sub_dir_status – just strip unnecessary items.
Here's simplified example:
---
- hosts: localhost
gather_facts: no
vars:
my_list:
- name: zzz1
sub:
- aaa
- ccc
- name: zzz2
sub:
- aaa
- bbb
tasks:
- stat:
path: /tmp/{{ item.0.name }}/{{ item.1 }}
register: stat_res
with_subelements:
- "{{ my_list }}"
- sub
- debug:
msg: "path to create '{{ item }}'"
with_items: "{{ stat_res.results | rejectattr('stat.exists') | map(attribute='invocation.module_args.path') | list }}"
You can iterate over stat_res.results | rejectattr('stat.exists') | list as well, but will have to construct path again as /tmp/{{ item.item.0.name }}/{{ item.item.1 }} – note double item, because first item is an element of stat_res.results which contain another item as element of your original loop for stat task.
P.S. Also I see no reason for your first task, as subdir task can detect all missing directories.

Ansible recursive checks in playbooks

We need to go through this structure
Zone spec
https://gist.github.com/git001/9230f041aaa34d22ec82eb17d444550c
I was able to run the following snipplet but now I'm stucked at the error checking.
playbook
--
- hosts: all
gather_facts: no
vars_files:
- "../doc/application-zone-spec.yml"
roles:
- { role: ingress_add, customers: "{{ application_zone_spec }}" }
role
- name: check if router exists
shell: "oc get dc -n default {{ customers.zone_name }}-{{ item.type }}"
with_items: "{{ customers.ingress }}"
ignore_errors: True
register: check_router
- name: Print ingress hostnames
debug: var=check_router
- name: create new router
shell: "echo 'I will create a router'"
with_items: "{{ customers.ingress }}"
when: check_router.rc == 1
Output of a ansible run
https://gist.github.com/git001/dab97d7d12a53edfcf2a69647ad543b7
The problem is that I need to go through the ingress items and I need to map the error of the differnt types from the "check_router" register.
It would be nice to make something like.
Pseudo code.
Iterate through the "customers.ingress"
check in "check_router" if the rc is ! 0
execute command.
We use.
ansible-playbook --version
ansible-playbook 2.1.0.0
config file = /etc/ansible/ansible.cfg
configured module search path = Default w/o overrides
You can replace the second loop with:
- name: create new router
shell: "echo 'I will create a router with type {{ item.item }}'"
with_items: "{{ check_router.results }}"
when: item.rc == 1
This will iterate over every step of check_route loop and you can access original items via item.item.

Resources