0

I have some files (file1), in some servers (group: myservers), which should look like this:

search www.mysebsite.com
nameserver 1.2.3.4
nameserver 1.2.3.5

This is an example of what this file should look like: The first line is mandatory ("search www.mysebsite.com"). The second and the third lines are mandatory as well, but the ips can change (although they should all be like this: ...).

I've being researching to implement some tasks using Ansible to check if the files are properly configured. I don't want to change any file, only check and output if the files are not ok or not. I know I can use ansible.builtin.lineinfile to check it, but I still haven't managed to find out how to achieve this. Can you help please?

fr0zt
  • 733
  • 4
  • 12
  • 30
  • Since Ansible is mostly used as Configuration Management Tool there is no need to check if a file is properly configured. Just declare the Desired State and make sure that the file is in that state. Still under some circumstances one may need to [to search for a string in a remote file](https://stackoverflow.com/questions/75250240/). So does it answer your question? – U880D Feb 14 '23 at 18:18
  • 1
    I read this question as testing patterns, not a configuration. It's expressed clearly `"first line is mandatory, but the ips can change ... to check if the files are properly configured"`. See the audit framework. It is a valid problem, I think . Don't you want to revoke the close requests? – Vladimir Botka Feb 14 '23 at 23:30

2 Answers2

2

For example, given the inventory

shell> cat hosts
[myservers]
test_11
test_13

Create a dictionary of what you want to audit

  audit:
    files:
      /etc/resolv.conf:
        patterns:
          - '^search example.com$'
          - '^nameserver \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'
      /etc/rc.conf:
        patterns:
          - '^sshd_enable="YES"$'
          - '^syslogd_flags="-ss"$'

Note: The order of the patterns is mandatory. The tests will succeed if the first two lines of the files match the sequence of patterns. For example,

shell> cat /etc/rc.conf
sshd_enable="YES"
syslogd_flags="-ss"
shell> cat /etc/resolv.conf
search example.com
nameserver 10.1.0.1

Declare the directory at the controller where the files will be stored

  my_dest: /tmp/ansible/myservers

fetch the files

    - fetch:
        src: "{{ item.key }}"
        dest: "{{ my_dest }}"
      loop: "{{ audit.files|dict2items }}"

Take a look at the fetched files

shell> tree /tmp/ansible/myservers
/tmp/ansible/myservers
├── test_11
│   └── etc
│       ├── rc.conf
│       └── resolv.conf
└── test_13
    └── etc
        ├── rc.conf
        └── resolv.conf

4 directories, 4 files

Audit the files. Create the dictionary host_files_results in the loop

    - set_fact:
        host_files_results: "{{ host_files_results|default({})|
                                combine(host_file_dict|from_yaml) }}"
      loop: "{{ audit.files|dict2items }}"
      loop_control:
        label: "{{ item.key }}"
      vars:
        host_file_path: "{{ my_dest }}/{{ inventory_hostname }}/{{ item.key }}"
        host_file_lines: "{{ lookup('file', host_file_path).splitlines() }}"
        host_file_result: |
          [{% for pattern in item.value.patterns %}
          {{ host_file_lines[loop.index0] is regex pattern }},
          {% endfor %}]
        host_file_dict: "{ {{ item.key }}: {{ host_file_result|from_yaml is all }} }"

gives

ok: [test_11] => 
  host_files_results:
    /etc/rc.conf: true
    /etc/resolv.conf: true
ok: [test_13] => 
  host_files_results:
    /etc/rc.conf: true
    /etc/resolv.conf: true

Declare the dictionary audit_files that aggregates host_files_results

  audit_files: "{{ dict(ansible_play_hosts|
                        zip(ansible_play_hosts|
                            map('extract', hostvars, 'host_files_results'))) }}"

gives

  audit_files:
    test_11:
      /etc/rc.conf: true
      /etc/resolv.conf: true
    test_13:
      /etc/rc.conf: true
      /etc/resolv.conf: true

Evaluate the audit results

    - block:
        - debug:
            var: audit_files
        - assert:
            that: "{{ audit_files|json_query('*.*')|flatten is all }}"
            fail_msg: "[ERR] Audit of files failed. [TODO: list failed]"
            success_msg: "[OK]  Audit of files passed."
      run_once: true

gives

 msg: '[OK]  Audit of files passed.'

Example of a complete playbook for testing

- hosts: myservers

  vars:

    my_dest: /tmp/ansible/myservers

    audit:
      files:
        /etc/resolv.conf:
          patterns:
            - '^search example.com$'
            - '^nameserver \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'
        /etc/rc.conf:
          patterns:
            - '^sshd_enable="YES"$'
            - '^syslogd_flags="-ss"$'

    audit_files: "{{ dict(ansible_play_hosts|
                          zip(ansible_play_hosts|
                              map('extract', hostvars, 'host_files_results'))) }}"

  tasks:

    - fetch:
        src: "{{ item.key }}"
        dest: "{{ my_dest }}"
      loop: "{{ audit.files|dict2items }}"
      loop_control:
        label: "{{ item.key }}"

    - set_fact:
        host_files_results: "{{ host_files_results|default({})|
                                combine(host_file_dict|from_yaml) }}"
      loop: "{{ audit.files|dict2items }}"
      loop_control:
        label: "{{ item.key }}"
      vars:
        host_file_path: "{{ my_dest }}/{{ inventory_hostname }}/{{ item.key }}"
        host_file_lines: "{{ lookup('file', host_file_path).splitlines() }}"
        host_file_result: |
          [{% for pattern in item.value.patterns %}
          {{ host_file_lines[loop.index0] is regex pattern }},
          {% endfor %}]
        host_file_dict: "{ {{ item.key }}: {{ host_file_result|from_yaml is all }} }"

    - debug:
        var: host_files_results

    - block:
        - debug:
            var: audit_files
        - assert:
            that: "{{ audit_files|json_query('*.*')|flatten is all }}"
            fail_msg: "[ERR] Audit of files failed. [TODO: list failed]"
            success_msg: "[OK]  Audit of files passed."
      run_once: true
Vladimir Botka
  • 58,131
  • 4
  • 32
  • 63
  • An interesting approach for auditing only certain or explicit configuration properties instead of full configuration files. – U880D Feb 15 '23 at 08:44
  • 1
    FWIW. See for example [Ubuntu 20 CIS](https://github.com/ansible-lockdown/UBUNTU20-CIS) it's a mix of configuring and testing. – Vladimir Botka Feb 16 '23 at 00:10
0

... implement some tasks using Ansible to check if the files are properly configured. I don't want to change any file, only check and output if the files are not ok or not.

Since Ansible is mostly used as Configuration Management Tool there is no need to check (before) if a file is properly configured. Just declare the Desired State and make sure that the file is in that state. As this is approach is working with Validating: check_mode too, if interested in a Configuration Check or an Audit it could be implemented simply as follow:

resolv.conf as is it should be

# Generated by NetworkManager
search example.com
nameserver 192.0.2.1

hosts.ini

[test]
test.example.com NS_IP=192.0.2.1

resolv.conf.j2 template

# Generated by NetworkManager
search {{ DOMAIN }}
nameserver {{ NS_IP }}

A minimal example playbook for Configuration Check in order to audit the config

---
- hosts: test
  become: false
  gather_facts: false

  vars:

    # Ansible v2.9 and later
    DOMAIN: "{{ inventory_hostname.split('.', 1) | last }}"

  tasks:

  - name: Check configuration (file)
    template:
      src: resolv.conf.j2
      dest: resolv.conf
    check_mode: true # will never change existing config
    register: result

  - name: Config change
    debug:
      msg: "{{ result.changed }}"

will result for no changes into an output of

TASK [Check configuration (file)] ******
ok: [test.example.com]

TASK [Config change] *******************
ok: [test.example.com] =>
  msg: false

or for changes into

TASK [Check configuration (file)] ******
changed: [test.example.com]

TASK [Config change] *******************
ok: [test.example.com] =>
  msg: true

and depending on what's in the config file.

If one is interested in an other message text and need to invert the output therefore, just use msg: "{{ not result.changed }}" as it will report an false if true and true if false.

Further Reading

Using Ansible inventory, variables in inventory, the template module (to) Template a file out to a target host and Enforcing check_mode on tasks makes it extremely simply to prevent Configuration Drift.

And as a reference for getting the search domain, Ansible: How to get hostname without domain name?.

U880D
  • 8,601
  • 6
  • 24
  • 40
  • I tried this approach. Both files resolv.conf.j2 and resolv.conf are different. But I am getting msg: true. Shouldn't I get false if the files are not alike? – fr0zt Feb 15 '23 at 10:06
  • If `resolv.conf.j2` and `resolv.conf` are different you'll get an `changed: true` which is correct so far as it means "Are they different?: true". If you like to get a message "Are the files are the same?" you would need to invert the boolean value. – U880D Feb 15 '23 at 10:15
  • Yes, I'd like some info if the file doesn't contain the mentioned strings : The file is not ok. Or if the files are alike: "The file is ok". Can you help? – fr0zt Feb 15 '23 at 10:17
  • "_if the file doesn't contain the mentioned strings : The file is not ok._", that is what the example is doing already. However, if you like to invert the message output just use `msg: "{{ not result.changed }}"` as it will report an false if true and true if false. – U880D Feb 15 '23 at 10:24
  • ok, you are correct. ANyway I could use regex or a wild card to the ips? To make it like this format: *.*.*.* – fr0zt Feb 15 '23 at 10:47
  • For templates I am not sure, I would need to find this out by myself before and since it is adding significant (for me unnecessary) complexity to such a case. Maybe you can just go with the [other here provided answer](https://stackoverflow.com/a/75453482/6771046) as they is addressing regex in IP already. – U880D Feb 15 '23 at 10:56