While the Ansible Automation Platform is great at a lot of things, Reporting is generally not considered one of them. So today we will explore how to use Ansible to collect data from our hosts, and use that data to build out a simple CSV report to email to us.
The types of reports and the data required will vary greatly depending on the use case, so we will pick a simple use case and dive into it.
“I have lots of Windows Servers, and I want to create a report of all their shares and their share permissions”
The concept behind building a report for this is a bit simple. We need to connect to some hosts and pull data from those hosts, and then we will store that data in a CSV file and email it out. Doing all of this in Ansible is fairly easy, but since YAML isn’t really a programming language, depending on the data and how you want to manipulate it, it can get a bit complex.
With the way that Ansible works, each host is running in its own fork, so building reports in Ansible generally consists of creating a small report per host (or a report per host per share such as in this case) and then combining them all back together at the end.
To begin our playbook, we will start with some pre_tasks to ensure we have a directory created to store these report fragments in. We will then ensure the directory is empty, but this step isn’t completely necessary if we are running from an Execution Environment. All the pre_tasks are delegated to the localhost and are set to run once, instead of with each host.
- name: Create Report
hosts: all
pre_tasks:
- name: Ensure Reports directories exist
ansible.builtin.file:
state: directory
path: "{{ playbook_dir }}/reports/fragments/"
delegate_to: localhost
run_once: true
- name: Ensure fragments directory is empty
ansible.builtin.file:
state: absent
path: "{{ playbook_dir }}/reports/fragments/*"
delegate_to: localhost
run_once: true
Next will come the tasks section in our playbook. This section is fairly small, as we only need to do 2 things. The first task is running a Powershell command to grab the Shares of the server and just get the properties that we want. Most importantly, we tell Powershell to convert the results to JSON, so that we can easily parse the data.
The second task is looping over the shares and including in more tasks using the include_tasks module. You will notice that even though we told Powershell in the previous task to convert it to JSON, we still have to tell Ansible to convert the data to JSON too via the from_json filter. This is because in the first task, even though its returned formatted as JSON, it is still a string to Ansible, so we must change it to be an object.
tasks:
- name: Powershell | Get-SMBShare
ansible.windows.win_shell: Get-SMBShare | Select-Object -Property Name,ScopeName,Path,Description,CimSystemProperties | ConvertTo-JSON
register: shares
- name: include Permissions report
ansible.builtin.include_tasks: tasks/share_permissions.yml
loop: "{{ shares.stdout | from_json }}"
loop_control:
loop_var: share
label: "{{ share.Name }}"
Now, inside our tasks/share_permissions.yml file we will start to build out our report per share. So each share will create its own file to be added to the report. Our first task is to get the Share Permissions, which just requires a simple Powershell command that we will again convert to JSON for parsing. The 2nd task is simply creating the variable for the JSON object.
The 3rd task gets a bit complicated, because I am utilizing Jinja2 to do a bit of programming. While this might not be necessary in your report, I am doing this to format the data exactly as I want it (removing some strings and removing duplicates). This step could be done utilizing ansible modules and some loops, but I find it easier and faster do leave the fancy programming to Jinja2 and let Ansible do what it does best.
- name: Powershell | Get-SMBShareAccess
ansible.windows.win_shell: Get-SMBShareAccess -Name "{{ share.Name }}" | Select-Object -Property AccountName,AccessRight | ConvertTo-JSON
register: permission
- name: Set Permission variable
ansible.builtin.set_fact:
permission: "{{ permission.stdout | from_json }}"
- name: Combine Share Permissions
ansible.builtin.set_fact:
pcom: '{%- set r = [] -%}
{%- if permission.AccountName is defined -%}
{%- set permission = [permission] -%}
{%- endif -%}
{%- for item in permission -%}
{%- set v = item.AccountName | replace("NT AUTHORITY\\", "") | replace("BUILTIN\\", "") | replace("NT SERVICE\\", "") -%}
{%- if v not in r -%}
{{- r.append(v) -}}
{%- endif -%}
{%- endfor -%}
{{- r | sort | join(";") -}}'
We then want to do the same for any File System ACLs for the Shares. We will wrap all of this in a block and rescue as sometimes it will fail if there are no ACL permissions for the share, and we want to avoid that. So our steps here are basically the same as before. Get the ACLs, parse and format them, with an additional step in the rescue that if we fail, set the ACL variable to blank.
- block:
- name: Powershell | Get-ACL
ansible.windows.win_shell: Get-ACL "{{ share.Path }}" | Select -ExpandProperty Access | Select -ExpandProperty IdentityReference | ConvertTo-JSON
register: acl
- name: Combine ACL Permissions
ansible.builtin.set_fact:
pacl: '{%- set r = [] -%}
{%- for item in acl.stdout | from_json -%}
{%- set v = item.Value | replace("NT AUTHORITY\\", "") | replace("BUILTIN\\", "") | replace("NT SERVICE\\", "") -%}
{%- if v not in r -%}
{{- r.append(v) -}}
{%- endif -%}
{%- endfor -%}
{{- r | sort | join(";") -}}'
rescue:
- name: Set ACL Permission Blank
ansible.builtin.set_fact:
pacl: ""
Now that we have both our Share Permissions and ACL Permissions, we can get ready to insert them into the report. First though, I want to set one more variable so we can create a column in the report that tells me whether the Share is Open or Secured. We do this by searching for “Everyone” in the Share Permissions (and ideally we should do a bit more here). We then create the report using the template module, and you will notice that we name the report using the hostname and share name to ensure its unique, and this task is then delegated to the localhost.
- name: Check for Everyone Permission
ansible.builtin.set_fact:
open: "{% if pcom is search('Everyone') %}Open{% else %}Secured{% endif %}"
- name: Render the Host Report Template
ansible.builtin.template:
src: "templates/share.csv.j2"
dest: "{{ playbook_dir }}/reports/fragments/{{ inventory_hostname }}-{{ share.Name }}.csv"
delegate_to: localhost
If we take a look at our template at templates/share.csv.j2 you will see that it is just a single line with all the information we have gathered.
{{ inventory_hostname }},{{ share.Name }},{{ open }},\\{{ share.CimSystemProperties.ServerName}}\{{ share.Name }},{{ share.Path }},{{ pcom }},{{ pacl }},{{ share.Description }}
From this point we will head back to the main playbook and create some post_tasks to start building the main report. We will use the assemble module to take all the files we have created and combine them into a single file. Again this is delegated to localhost and set to run only once. We now have all the data in the report, but we need to know which columns are which, so we will use the lineinfile module to add a header at the beginning of the file (BOF).
- name: Assemble all the csv files
ansible.builtin.assemble:
src: "{{ playbook_dir }}/reports/fragments/"
dest: "{{ playbook_dir }}/reports/shares.csv"
delegate_to: localhost
run_once: true
- name: Append the header to the csv file
ansible.builtin.lineinfile:
dest: "{{ playbook_dir }}/reports/shares.csv"
insertbefore: BOF
line: "Hostname,Share_Name,Share_Value,FullSharePath,Share_Mapping,Share_Permissions,NTFS_Permissions,Description"
delegate_to: localhost
run_once: true
Now our report is created. We have a lot of options on what we can do with the report file, but for this scenario we will email it out via the mail module. Take note of the additional variables required when you do this. We could also copy it to a remote file share and rename it based upon the date, etc…
- name: Mail Report
community.general.mail:
host: "{{ smtp_server | default('127.0.0.1') }}"
port: "{{ smtp_port | default(25) }}"
subject: Windows Share Report
body: Here is the report of all Windows Shares
from: "{{ from_address }}"
to:
- "{{ to_address }}"
attach:
- "{{ playbook_dir }}/reports/shares.csv"
ignore_errors: true
delegate_to: localhost
run_once: true
The wraps up the reporting playbook. The concepts used in this reporting playbook can be carried over to virtually any report you need to build. In the end, you are just collecting data from servers (or maybe an API), creating files per server and then assembling those files together. This CSV file output will look something like this
The repo containing this example playbook is also available on Github at
https://github.com/cigamit/ansible_misc/tree/master/windows_share_report