589 lines
20 KiB
YAML
Executable File
589 lines
20 KiB
YAML
Executable File
---
|
|
# PLAY
|
|
# Gather cloudforms information and set service name
|
|
- name: Query automate workspace
|
|
hosts: localhost
|
|
gather_facts: False
|
|
|
|
tasks:
|
|
|
|
- set_fact:
|
|
endpoint: "{{ manageiq.api_url }}"
|
|
auth_token: "{{ manageiq.api_token }}"
|
|
request_id: "{{ (manageiq.request).split('/')[-1] }}"
|
|
service_id: "{{ (manageiq.service).split('/')[-1] }}"
|
|
user_id: "{{ (manageiq.user).split('/')[-1] }}"
|
|
when: manageiq is defined
|
|
|
|
# when users run ansible their manageiq auth token does not have sufficient rights to interact with the API, no combination of rights in a role for a non admin user are sufficient
|
|
# use the local admin credentials to get an auth token
|
|
- name: Get auth token
|
|
uri:
|
|
url: "{{ endpoint }}/api/auth"
|
|
validate_certs: no
|
|
method: GET
|
|
user: "{{ api_user }}"
|
|
password: "{{ api_pass }}"
|
|
status_code: 200
|
|
register: login
|
|
when: manageiq is defined
|
|
|
|
- set_fact:
|
|
auth_token: "{{ login.json.auth_token }}"
|
|
when: manageiq is defined
|
|
|
|
- name: Get requester user attributes
|
|
uri:
|
|
url: "{{ endpoint }}/api/users/{{ user_id }}"
|
|
validate_certs: no
|
|
method: GET
|
|
headers:
|
|
X-Auth-Token: "{{ auth_token }}"
|
|
status_code: 200
|
|
register: user
|
|
when: manageiq is defined
|
|
|
|
- set_fact:
|
|
requester_email: "{{ user.json.email }}"
|
|
when: manageiq is defined and user.json.email is not none # cloudforms admin user has no email address (unless set), this is a null field in json
|
|
|
|
- set_fact:
|
|
#requester_email: "{{ from_email }}" # from_email should be able to recieve mail via relay
|
|
requester_email: ucats@exmail.nottingham.ac.uk
|
|
when: requester_email is not defined
|
|
|
|
- set_fact:
|
|
requester_user: "{{ user.json.name }}"
|
|
when: manageiq is defined
|
|
|
|
# when run from the command line set a default requester user
|
|
- set_fact:
|
|
requester_user: "command line invocation"
|
|
when: manageiq is not defined
|
|
|
|
# use when we need to determine if this is a self service request - might not be used for this playbook
|
|
# - name: get AD account name
|
|
# set_fact:
|
|
# requester_user_ad: "{{ (user.json.email).split('@')[0] }}"
|
|
# when: manageiq is defined
|
|
|
|
- name: get service
|
|
uri:
|
|
url: "{{ endpoint }}/api/services/{{ service_id }}"
|
|
validate_certs: no
|
|
method: GET
|
|
headers:
|
|
X-Auth-Token: "{{ auth_token }}"
|
|
status_code: 200
|
|
register: service
|
|
when: manageiq is defined
|
|
|
|
- set_fact:
|
|
service_name: "{{ service.json.name }}"
|
|
new_service_name: "{{ service.json.name }} {{ request_id }}"
|
|
when: manageiq is defined
|
|
|
|
- set_fact:
|
|
service_name: "command line invocation"
|
|
new_service_name: "command line invocation"
|
|
when: manageiq is not defined
|
|
|
|
- name: set service name
|
|
uri:
|
|
url: "{{ endpoint }}/api/services/{{ service_id }}"
|
|
validate_certs: no
|
|
method: POST
|
|
headers:
|
|
X-Auth-Token: "{{ auth_token }}"
|
|
body_format: json
|
|
body: { "action" : "edit", "resource" : { "name" : "{{ new_service_name }}" }}
|
|
status_code: 200, 204
|
|
register: service
|
|
when: manageiq is defined
|
|
|
|
|
|
# PLAY
|
|
# Validate parameters passed to script from cloudforms
|
|
- name: Script input Validation
|
|
hosts: localhost
|
|
gather_facts: False
|
|
vars_files:
|
|
- vars/main.yml
|
|
vars:
|
|
ownerlist: []
|
|
groupmemberslist: []
|
|
groupmemberslistvalidate: []
|
|
|
|
tasks:
|
|
|
|
# add task to check + fail for any variables with name placeholder here
|
|
|
|
- name: Convert owner to list
|
|
set_fact:
|
|
ownerlist: "{{ ownerlist }} + [ '{{ item }}' ]"
|
|
with_items: "{{ owner }}"
|
|
|
|
- name: Split groupmembers parameter on , delimiter
|
|
set_fact:
|
|
groupmemberslist: "{{ groupmemberslist }} + [ '{{ item }}' ]"
|
|
with_items: "{{ groupmembers.split(',') }}"
|
|
|
|
- name: Remove empty fields from groupmembers parameter
|
|
set_fact:
|
|
groupmemberslistvalidate: "{{ groupmemberslistvalidate }} + [ '{{ item }}' ]"
|
|
when: item | length != 0
|
|
with_items: "{{ groupmemberslist }}"
|
|
|
|
|
|
# PLAY
|
|
# Add winrm host used for AD querys to inventory
|
|
- name: Build inventory for AD server
|
|
hosts: localhost
|
|
gather_facts: False
|
|
vars_files:
|
|
- vars/main.yml
|
|
|
|
tasks:
|
|
|
|
- name: Add host entry for adserver
|
|
add_host: >
|
|
name=adserver
|
|
groups=windows
|
|
ansible_host="{{ ad_host }}"
|
|
|
|
|
|
# PLAY
|
|
# Query winrm to validate AD user/group exist, build dict for share ACL and dict of all user/email from a recursively search of user/group
|
|
- name: Check AD user exists
|
|
hosts: adserver
|
|
gather_facts: false
|
|
vars_files:
|
|
- vars/main.yml
|
|
vars:
|
|
body_service_name: "{{ hostvars['localhost']['new_service_name'] }}"
|
|
requester_email: "{{ hostvars['localhost']['requester_email'] }}"
|
|
body_requester_user: "{{ hostvars['localhost']['requester_user'] }}"
|
|
ownerlist: "{{ hostvars['localhost']['ownerlist'] }}"
|
|
groupmemberslistvalidate: "{{ hostvars['localhost']['groupmemberslistvalidate'] }}"
|
|
ownermembers: "{{ ownerlist + groupmemberslistvalidate }}"
|
|
no_aduser: []
|
|
object_type: []
|
|
|
|
tasks:
|
|
|
|
- name: Add connectivity variables for adserver
|
|
set_fact:
|
|
ansible_user: "{{ ad_user }}"
|
|
ansible_password: "{{ ad_pass }}"
|
|
ansible_connection: "{{ ad_connection }}"
|
|
ansible_winrm_transport: "{{ ad_winrm_transport }}"
|
|
ansible_winrm_kinit_mode: "{{ ad_winrm_kinit_mode }}"
|
|
ansible_winrm_message_encryption: "{{ ad_winrm_message_encryption }}"
|
|
ansible_port: "{{ ad_port }}"
|
|
ansible_winrm_scheme: "{{ ad_winrm_scheme }}"
|
|
ansible_winrm_server_cert_validation: "{{ ad_winrm_server_cert_validation }}"
|
|
# ansible_winrm_operation_timeout_sec: 60
|
|
# ansible_winrm_read_timeout_sec: 60
|
|
|
|
- name: Check AD user/group exists
|
|
win_shell: ([ADSISearcher] "(sAMAccountName={{ item }})").FindOne()
|
|
register: command_result
|
|
with_items:
|
|
# - "{{ ownerlist }}"
|
|
# - "{{ groupmemberslistvalidate }}"
|
|
- "{{ ownermembers }}"
|
|
|
|
- name: Flag fail where AD user/group not exist
|
|
set_fact:
|
|
no_aduser: "{{ no_aduser + [item.item] }}"
|
|
when: item.stdout | length == 0
|
|
with_items: "{{ command_result.results }}"
|
|
changed_when: true
|
|
notify: topic_noad
|
|
|
|
- name: Check AD object is user or group
|
|
win_shell: ([ADSISearcher] "(sAMAccountName={{ item.item }})").FindOne().Properties.objectcategory
|
|
register: object_result
|
|
with_items: "{{ command_result.results }}"
|
|
when: no_aduser | length == 0
|
|
|
|
- name: Build list of AD object type
|
|
set_fact:
|
|
object_type: "{{ object_type + [(item.stdout.split(',')[0].split('CN=')[1])] }}" # faster than regex
|
|
with_items: "{{ object_result.results }}"
|
|
when: no_aduser | length == 0
|
|
|
|
# cheat, the first entry in the list ownermember (user or group) is the owner where we merge ownerlist + groupmemberslistvalidate
|
|
# for this to work the owner list must have only one entry
|
|
# to use this logic for groups of objects (i.e rwx, rw, r groups), we will need to merge a dict with fields name, role and the list object_attributes
|
|
- name: Build dict of object names, types and roles positionally from list with object name and list with object type
|
|
set_fact:
|
|
object_attributes: "{{ object_attributes | default([]) + [dict(name=item[0], type=item[1], role='owner' if object_attributes is undefined else 'member') ] }}"
|
|
loop: "{{ ownermembers|zip(object_type)|list }}"
|
|
when: no_aduser | length == 0
|
|
|
|
# - debug:
|
|
# msg: "{{ object_attributes }}"
|
|
# when: no_aduser | length == 0
|
|
|
|
# recursive loop control using include_tasks and block rescue behaviour, https://github.com/ansible/ansible/issues/46203
|
|
# write/access variables between plays using dummy host variables, https://www.unixarena.com/2019/05/passing-variable-from-one-playbook-to-another-playbook-ansible.html/
|
|
|
|
- name: Register dummy host with variable object_attributes
|
|
add_host:
|
|
name: "DUMMY_HOST"
|
|
object_attributes: "{{ object_attributes }}"
|
|
when: no_aduser | length == 0
|
|
|
|
- name: Find all group members
|
|
include_tasks: group_lookup.yml
|
|
when: no_aduser | length == 0
|
|
|
|
# - name: Inspect all users who will require email notification
|
|
# debug:
|
|
# msg: "{{ hostvars['DUMMY_HOST']['find_users'] }}"
|
|
# when: no_aduser | length == 0
|
|
|
|
- name: Import dummy host variable from group_lookup.yml
|
|
set_fact:
|
|
unique_users: "{{ hostvars['DUMMY_HOST']['find_users'] }}"
|
|
when: no_aduser | length == 0
|
|
|
|
- name: Get unique name/role/type entries, remove duplicate entries that may arise from group nesting
|
|
set_fact:
|
|
unique_users: "{{ unique_users | unique }}"
|
|
when: no_aduser | length == 0
|
|
|
|
# - debug:
|
|
# msg: "{{ unique_users }}"
|
|
|
|
- name: Get owner names into a list
|
|
set_fact:
|
|
owner_users: "{{ owner_users | default([]) + [item.name] }}"
|
|
with_items: "{{ unique_users }}"
|
|
when: no_aduser | length == 0 and item.role == 'owner'
|
|
|
|
- name: Get member names into a list
|
|
set_fact:
|
|
member_users: "{{ member_users | default([]) + [item.name] }}"
|
|
with_items: "{{ unique_users }}"
|
|
when: no_aduser | length == 0 and item.role == 'member'
|
|
|
|
- name: Get names common in both lists
|
|
set_fact:
|
|
common_users: "{{ owner_users | intersect(member_users) }}"
|
|
when: no_aduser | length == 0
|
|
|
|
# - debug:
|
|
# msg: "{{ common_users }}"
|
|
# when: no_aduser | length == 0
|
|
|
|
# KEEP THIS
|
|
# this is a dedupe task where duplicate users exist one with role owner and one with role member, the user with role member is removed
|
|
# this isnt too helpful in gpfs as the owner attribute is the unix owner of the fileset mount point not a windows ACL
|
|
# the logic is useful in a permissions scenario - example multiple entries for a user with rwx and r-- permissions, we want to ensure only the rwx role is used
|
|
# - name: Remove member entries where competing owner entry exists
|
|
# set_fact:
|
|
# email_users: "{{ email_users | default([]) + [dict(name=item.name, role=item.role, type=item.type)] }}"
|
|
# with_items: "{{ unique_users }}"
|
|
# when: no_aduser | length == 0 and (item.name not in common_users or (item.name in common_users and item.role == 'owner'))
|
|
- set_fact:
|
|
email_users: "{{ unique_users }}"
|
|
when: no_aduser | length == 0
|
|
|
|
# final deduplicated dict to send appropriate class of email to users
|
|
# - debug:
|
|
# msg: "{{ email_users }}"
|
|
# when: no_aduser | length == 0
|
|
|
|
- name: Get member email address from AD
|
|
win_shell: Get-ADUser {{ item.name }} -Properties mail | Select-Object -ExpandProperty mail
|
|
register: email_result
|
|
with_items: "{{ email_users }}"
|
|
when: no_aduser | length == 0
|
|
|
|
# - debug:
|
|
# msg: "{{ email_result }}"
|
|
# when: no_aduser | length == 0
|
|
|
|
# this will crash out where customer account has no associated email, UoN are very consistent with account creation and adding email - needs logic for empty elements
|
|
- name: Get member emails into a list
|
|
set_fact:
|
|
email_address: "{{ email_address | default([]) + [item.stdout_lines[0]] }}"
|
|
with_items: "{{ email_result.results }}"
|
|
when: no_aduser | length == 0
|
|
|
|
- name: Build dict of object name, role, type and email
|
|
set_fact:
|
|
user_dict: "{{ user_dict | default([]) + [dict(name=item[0].name, role=item[0].role, type=item[0].type, email=item[1])] }}" # adding a new positional field from the list to the dict
|
|
loop: "{{ email_users|zip(email_address)|list }}"
|
|
when: no_aduser | length == 0
|
|
|
|
# COMMENT OUT
|
|
- debug:
|
|
msg: "{{ user_dict }}"
|
|
when: no_aduser | length == 0
|
|
|
|
handlers:
|
|
|
|
- name: Build invalid user(s) email body
|
|
set_fact:
|
|
mail_body: "{{ lookup('template', 'templates/mail-invalid.j2') }}"
|
|
delegate_to: localhost
|
|
listen:
|
|
- topic_noad
|
|
|
|
- name: Send status email
|
|
mail:
|
|
host: "{{ smtp_relay }}"
|
|
port: "{{ smtp_port }}"
|
|
charset: utf-8
|
|
from: "{{ from_email }}"
|
|
to: "{{ requester_email }}"
|
|
subject: "Result of request {{ body_service_name }}"
|
|
body: "{{ mail_body }}"
|
|
delegate_to: localhost
|
|
when: requester_email is defined and enable_requester_email
|
|
listen:
|
|
- topic_noad
|
|
|
|
- name: Condition fail on invalid AD user
|
|
debug:
|
|
msg:
|
|
- "invalid AD user(s)/groups(s)"
|
|
- ---------------------------------------
|
|
- "{{ no_aduser }}"
|
|
- ---------------------------------------
|
|
delegate_to: localhost
|
|
listen:
|
|
- topic_noad
|
|
|
|
- name: Hard exit
|
|
fail:
|
|
listen:
|
|
- topic_noad
|
|
|
|
|
|
# PLAY
|
|
# Create GPFS fileset with quota, samba export and accociated ACLs
|
|
- name: GPFS create fileset and samba export with quota
|
|
hosts: localhost
|
|
gather_facts: False
|
|
vars_files:
|
|
- vars/main.yml
|
|
- vars/requests.yml
|
|
vars:
|
|
body_service_name: "{{ hostvars['localhost']['new_service_name'] }}"
|
|
requester_email: "{{ hostvars['localhost']['requester_email'] }}"
|
|
body_requester_user: "{{ hostvars['localhost']['requester_user'] }}"
|
|
|
|
tasks:
|
|
|
|
# task here to work out quota size as a parameter
|
|
|
|
- name: Check samba share exists
|
|
include_tasks: tasks/checkfilesetsambaexport.yml
|
|
|
|
- name: Check fileset exists
|
|
include_tasks: tasks/checkfileset.yml
|
|
when: hostvars['DUMMY_HOST']['storage_fail'] is not defined
|
|
|
|
- name: Create fileset
|
|
include_tasks: tasks/createfileset.yml
|
|
when: hostvars['DUMMY_HOST']['storage_fail'] is not defined
|
|
|
|
- name: Create fileset quota
|
|
include_tasks: tasks/createfilesetquota.yml
|
|
when: hostvars['DUMMY_HOST']['storage_fail'] is not defined
|
|
|
|
- name: Create samba share
|
|
include_tasks: tasks/createsambaexport.yml
|
|
when: hostvars['DUMMY_HOST']['storage_fail'] is not defined
|
|
|
|
# if the ACL apply fails the loop terminates, users following a failed user ACL will not be processed, this shouldnt happen but there is no control for this
|
|
# would be good to check share exists already like the following remove task, would help if the script needs to update ACL
|
|
- name: Create samba share ACL
|
|
include_tasks: tasks/createsambaacl.yml
|
|
vars:
|
|
ad_object: "{{ item.name }}"
|
|
with_items: "{{ hostvars['adserver']['object_attributes'] }}"
|
|
when: hostvars['DUMMY_HOST']['storage_fail'] is not defined and item.role == 'member' # remember KEEP THIS comment under hosts: adserver
|
|
|
|
- name: Remove samba share ACL
|
|
include_tasks: tasks/removesambaacl.yml
|
|
when: hostvars['DUMMY_HOST']['storage_fail'] is not defined
|
|
|
|
- name: List samba share ACL
|
|
include_tasks: tasks/listsambaacl.yml
|
|
when: hostvars['DUMMY_HOST']['storage_fail'] is not defined
|
|
|
|
- name: Report storage failure
|
|
set_fact:
|
|
storage_fail: "{{ hostvars['DUMMY_HOST']['storage_fail'] }}"
|
|
when: hostvars['DUMMY_HOST']['storage_fail'] is defined
|
|
changed_when: true
|
|
notify: topic_fail
|
|
|
|
# - name: Get members for output
|
|
# set_fact:
|
|
# notify_members: "{{ notify_members | default([]) + [dict(name=item.name, email=item.email)]}}"
|
|
# with_items: "{{ hostvars['adserver']['user_dict'] }}"
|
|
# when: hostvars['DUMMY_HOST']['acl_entry'] is defined and (item.role == 'member' and item.type == 'Person')
|
|
|
|
# - name: Report ACL entry
|
|
# set_fact:
|
|
# acl_entry: "{{ hostvars['DUMMY_HOST']['acl_entry'] }}"
|
|
# when: hostvars['DUMMY_HOST']['acl_entry'] is defined
|
|
# changed_when: true
|
|
# notify: topic_pass
|
|
|
|
handlers:
|
|
|
|
- name: Build fail on storage provision email body
|
|
set_fact:
|
|
mail_body: "{{ lookup('template', 'templates/mail-storage-fail.j2') }}"
|
|
listen:
|
|
- topic_fail
|
|
|
|
- name: Send fail email
|
|
mail:
|
|
host: "{{ smtp_relay }}"
|
|
port: "{{ smtp_port }}"
|
|
charset: utf-8
|
|
from: "{{ from_email }}"
|
|
to: "{{ requester_email }}"
|
|
subject: "Result of request {{ body_service_name }}"
|
|
body: "{{ mail_body }}"
|
|
when: requester_email is defined and enable_requester_email
|
|
listen:
|
|
- topic_fail
|
|
|
|
- name: Condition fail on provision storage
|
|
debug:
|
|
msg:
|
|
- "provisioning storage failure"
|
|
- ---------------------------------------
|
|
- "{{ storage_fail }}"
|
|
- ---------------------------------------
|
|
listen:
|
|
- topic_fail
|
|
|
|
- name: Hard exit
|
|
fail:
|
|
listen:
|
|
- topic_fail
|
|
|
|
# PLAY
|
|
# Console and email output of results
|
|
- name: Report results
|
|
hosts: localhost
|
|
gather_facts: False
|
|
vars_files:
|
|
- vars/main.yml
|
|
vars:
|
|
body_service_name: "{{ hostvars['localhost']['new_service_name'] }}"
|
|
requester_email: "{{ hostvars['localhost']['requester_email'] }}"
|
|
body_requester_user: "{{ hostvars['localhost']['requester_user'] }}"
|
|
|
|
tasks:
|
|
|
|
# - name: Report storage failure
|
|
# set_fact:
|
|
# storage_fail: "{{ hostvars['DUMMY_HOST']['storage_fail'] }}"
|
|
# when: hostvars['DUMMY_HOST']['storage_fail'] is defined
|
|
# changed_when: true
|
|
# notify: topic_fail
|
|
|
|
- name: Get members name/email for notification requester email body
|
|
set_fact:
|
|
notify_members: "{{ notify_members | default([]) + [dict(name=item.name, email=item.email)]}}"
|
|
with_items: "{{ hostvars['adserver']['user_dict'] }}"
|
|
when: hostvars['DUMMY_HOST']['acl_entry'] is defined and (item.role == 'member' and item.type == 'Person')
|
|
|
|
- name: Get member email to list for email module
|
|
set_fact:
|
|
notify_customers: "{{ notify_customers | default([]) + [item.email]}}"
|
|
with_items: "{{ hostvars['adserver']['user_dict'] }}"
|
|
when: hostvars['DUMMY_HOST']['acl_entry'] is defined and (item.role == 'member' and item.type == 'Person')
|
|
|
|
- name: Report ACL entry
|
|
set_fact:
|
|
acl_entry: "{{ hostvars['DUMMY_HOST']['acl_entry'] }}"
|
|
when: hostvars['DUMMY_HOST']['acl_entry'] is defined
|
|
changed_when: true
|
|
notify: topic_pass
|
|
|
|
handlers:
|
|
|
|
- name: Build requester storage provision email body
|
|
set_fact:
|
|
mail_body: "{{ lookup('template', 'templates/mail-storage-requester.j2') }}"
|
|
listen:
|
|
- topic_pass
|
|
|
|
- name: Send requester storage provision email body
|
|
mail:
|
|
host: "{{ smtp_relay }}"
|
|
port: "{{ smtp_port }}"
|
|
charset: utf-8
|
|
from: "{{ from_email }}"
|
|
to: "{{ requester_email }}"
|
|
subject: "Result of request {{ body_service_name }}"
|
|
body: "{{ mail_body }}"
|
|
when: requester_email is defined and enable_requester_email
|
|
listen:
|
|
- topic_pass
|
|
|
|
- name: Build member storage provision email body
|
|
set_fact:
|
|
mail_body: "{{ lookup('template', 'templates/mail-storage-customer.j2') }}"
|
|
listen:
|
|
- topic_pass
|
|
|
|
- name: Send member storage provision email body
|
|
mail:
|
|
host: "{{ smtp_relay }}"
|
|
port: "{{ smtp_port }}"
|
|
charset: utf-8
|
|
from: "{{ from_email }}"
|
|
to: "{{ notify_customers }}"
|
|
subject: "Result of request {{ body_service_name }}"
|
|
body: "{{ mail_body }}"
|
|
when: enable_customer_email
|
|
listen:
|
|
- topic_pass
|
|
|
|
- name: Report "{{ filesetName }}" sucessful creation and applied ACL
|
|
debug:
|
|
msg:
|
|
- "storage {{ filesetName }} sucessfully created"
|
|
- ------------------------------------------------------------------------------
|
|
- "access share @UNC path //{{ clusterapiIP }}/{{ filesetName }}"
|
|
- ------------------------------------------------------------------------------
|
|
- "applied ACL for {{ filesetName }}"
|
|
- ------------------------------------------------------------------------------
|
|
- "{{ acl_entry }}"
|
|
- ------------------------------------------------------------------------------
|
|
- "users with access to the share that will recieve email notification"
|
|
- ------------------------------------------------------------------------------
|
|
- "{{ notify_members }}"
|
|
listen:
|
|
- topic_pass
|
|
|
|
|
|
|
|
# TEST + BUILD
|
|
# need input placeholder validation
|
|
# need a maximum size of share as a parameter, this will enable different tiles/tag combinations for users/groups
|
|
# may change with netapp module - use a different play to report + email - is there any point? maybe with netapp + dual functionality (play for netapp, play for gpfs) - use some more handlers and the exisitng customer emails switch
|
|
|
|
# NOT UNTIL MORE REQUIREMENTS
|
|
# owner as a group - no need today - this is a unix permission, maybe best set to service_cloudforms or even local root of GPFS
|
|
# dont think i need to worry about 'you are already member of' regarding ACL's - we are creating new storage - no requirement to delete storage yet - can put in check it will help for update/add permissions
|
|
|
|
# nice jmespath check - clean
|
|
# - name: query results of output files exist on remote host
|
|
# set_fact:
|
|
# query_certs: "{{ stat_result | json_query(jmesquery) }}"
|
|
# vars:
|
|
# jmesquery: "results[?(@.stat.exists)].item" |