--- - name: Query automate workspace hosts: localhost gather_facts: False vars_files: vars/main.yml 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: # ansible runner timeout, something changed from 5.11.1.2 to 5.11.6.0? #url: "{{ endpoint }}/api/auth" url: "https://127.0.0.1/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 }}" url: "https://127.0.0.1/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 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 # - name: DEBUG print all user attributes # debug: # msg: # - "{{ user }}" # # useful fields: # "email": "ucats@exmail.nottingham.ac.uk" # "name": "Toby Seed" # "userid": "toby.seed@nottingham.ac.uk" # # we see that the UON active directory schema has a different correlation between the AD short login id and the lookup of the userid and name # the above user generally would use the login id "ucats" across the UON estate to authenticate against AD # interestingly cloudforms queries several fields in the schema and will allow login as "ucats" and "toby.seed@nottingham.ac.uk" # to detect if this script is being run in self service mode, a match is performed against the requesting user and a single entry in groupmember list # we can only retrieve the expected AD short login id from the email field using UON AD # remove after testing - self service mode wasnt tested enough on dev system where cloudforms admin has no email # - name: get AD account name # set_fact: # requester_user_ad: "{{ (user.json.email).split('@')[0] }}" # when: manageiq is defined - name: get AD account name set_fact: requester_user_ad: "{{ (requester_email).split('@')[0] }}" when: manageiq is defined - name: get service uri: #url: "{{ endpoint }}/api/services/{{ service_id }}" url: "https://127.0.0.1/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 }}" url: "https://127.0.0.1/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 - hosts: localhost gather_facts: false name: Validation tasks vars: groupmemberslist: [] groupmemberslistvalidate: [] vars_files: vars/main.yml tasks: - name: Build inventory for AD server add_host: > name=adserver groups=windows ansible_host="{{ ad_host }}" - name: Fail Where Requisite Vars Not Set fail: msg: "Parameter {{item.key}} has value {{item.value}}, {{item.key}} is required to be passed from Cloudforms" when: item.value == 'placeholder' loop: "{{ lookup('dict', vars ) }}" #vars is a special variable of a list containing all variables in the playbook no_log: "{{ suppress_vars_output }}" - 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 }}" - hosts: adserver gather_facts: false name: Check AD user exists vars: # set present/absent flag for netapp modules from create/delete values in the perform parameter, included here for action to perform state: "{{ 'present' if perform == 'create' else ( 'absent' if perform == 'delete' else 'placeholder') }}" groupmemberslistvalidate: "{{ hostvars['localhost']['groupmemberslistvalidate'] }}" email_customer: [] no_aduser: [] email_recipients: [] user_no_action: [] body_service_name: "{{ hostvars['localhost']['new_service_name'] }}" body_requester_user: "{{ hostvars['localhost']['requester_user'] }}" # NOT USED body_requester_user_ad: "{{ hostvars['localhost']['requester_user_ad }}" vars_files: vars/main.yml tasks: # to avoid using group_vars we set_facts that were not accepted by add_host, add_host does not work with many windows winrm connectivity vars - 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 }}" - name: Check AD user exists win_shell: ([ADSISearcher] "(sAMAccountName={{ item }})").FindOne() register: command_result with_items: "{{ groupmemberslistvalidate }}" - name: Flag fail where AD user 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: Get Domain user email address win_shell: Get-ADUser {{ item }} -Properties mail | Select-Object -ExpandProperty mail register: email_result with_items: "{{ groupmemberslistvalidate }}" when: no_aduser | length == 0 # this will crash out where customer account has no associated email - needs logic for empty elements - name: Add customer email to list of email recipients set_fact: email_customer: "{{ email_customer }} + [ '{{ item.stdout_lines[0] }}' ]" with_items: "{{ email_result.results }}" when: no_aduser | length == 0 - name: Build list of user and accociated email address set_fact: account_email: "{{ account_email | default([]) + [dict(name=item[0], email=item[1])] }}" loop: "{{ groupmemberslistvalidate | zip (email_customer) | list }}" when: no_aduser | length == 0 - name: Add Domain user/group to Domain Group win_domain_group_membership: name: "{{ group }}" members: "{{ groupmemberslistvalidate }}" state: "{{ state }}" register: result when: no_aduser | length == 0 - name: Build list of user emails requiring notification for add operation set_fact: email_recipients: "{{ email_recipients | default([]) + [item.email] }}" with_items: "{{ account_email }}" when: ( no_aduser | length == 0 and (result.added | length > 0 and state == 'present')) and item.name in result.added - name: Build list of user emails requiring notification for delete operation set_fact: email_recipients: "{{ email_recipients | default([]) + [item.email] }}" with_items: "{{ account_email }}" when: ( no_aduser | length == 0 and (result.removed | length > 0 and state == 'absent')) and item.name in result.removed # users in this list were not added/removed from AD group as they were already present/not-present - name: Build a list of users where no action was taken set_fact: user_no_action: "{{ user_no_action | default([]) + [item.name] }}" with_items: "{{ account_email }}" when: no_aduser | length == 0 and (item.name not in result.removed and item.name not in result.added) - name: Build a list of emails for users where no action was taken set_fact: email_recipients_no_action: "{{ email_recipients_no_action | default([]) + [item.email] }}" with_items: "{{ account_email }}" when: no_aduser | length == 0 and (item.name not in result.removed and item.name not in result.added) # - debug: # msg: # - "email recipients change {{ email_recipients }}" # - "email recipients nochange {{ email_recipients_no_action }}" # - "users not actioned {{ user_no_action }}" - set_fact: template_prefix: "default" when: template_prefix is not defined handlers: - name: Build invalid user(s) email body set_fact: mail_body: "{{ lookup('template', 'templates/{{ template_prefix }}/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: "{{ hostvars['localhost']['requester_email'] }}" subject: "Result of request {{ body_service_name }} : {{ perform }} user(s) in {{ group }}" body: "{{ mail_body }}" delegate_to: localhost when: hostvars['localhost']['requester_email'] is defined and enable_requester_email == 'true' listen: - topic_noad - name: Condition fail on invalid AD user debug: msg: - "invalid AD user(s) to {{ perform }} in group {{ group }}" - --------------------------------------- - "{{ no_aduser }}" - --------------------------------------- delegate_to: localhost listen: topic_noad # provides failure exit code for cloudforms and terminates playbook - name: Hard exit fail: listen: topic_noad - hosts: localhost gather_facts: false name: Report Results vars: state: "{{ 'present' if perform == 'create' else ( 'absent' if perform == 'delete' else 'placeholder') }}" result: "{{ hostvars['adserver']['result'] }}" requester_email: "{{ hostvars['localhost']['requester_email'] }}" body_service_name_noreqid: "{{ hostvars['localhost']['service_name'] }}" # service_name matches cloudforms tile name (i.e Automated Transcription Service), we want this for the customer email subject line - welcome to body_service_name: "{{ hostvars['localhost']['new_service_name'] }}" body_requester_user: "{{ hostvars['localhost']['requester_user'] }}" body_requester_user_ad: "{{ hostvars['localhost']['requester_user_ad'] }}" groupmemberslistvalidate: "{{ hostvars['localhost']['groupmemberslistvalidate'] }}" email_recipients: [] email_recipients_no_action: [] user_no_action: [] self_service_user: false vars_files: vars/main.yml tasks: # the initial design of the script catered for user(s) being added to a group and the requester getting status emails (add / remove / invalid-user / no-change) and the user(s) recieving (add / remove) emails # a use case where the requester is requesting only himself could mean status emails and user emails being recieved # logic at the end of this script evaluates if the requester user matches a single entry in groupmember list and then disables requester/status emails # to test this behaviour on the command line ensure the following fact (requester_user_ad) is set to the same value as the single item in the groupmembers parameter # this condition is replicated with the spoof_self_service parameter - name: checking for spoof self service mode set_fact: body_requester_user_ad: "{{ groupmemberslistvalidate[0] }}" when: spoof_self_service == "true" - name: detect if the requester user is adding/removing only itself to group (only active when using a custom template_prefix) set_fact: self_service_user: true when: (groupmemberslistvalidate | length == 1 and groupmemberslistvalidate[0] == body_requester_user_ad) and template_prefix != 'default' - debug: msg: "running in self service mode" when: self_service_user - name: set list of email recipients where action has been taken upon their account set_fact: email_recipients: "{{ hostvars['adserver']['email_recipients'] }}" when: hostvars['adserver']['email_recipients'] is defined - name: set list of email recipients where no action has been taken upon their account set_fact: email_recipients_no_action: "{{ hostvars['adserver']['email_recipients_no_action'] }}" when: hostvars['adserver']['email_recipients_no_action'] is defined - name: set list of user names where no action has been taken upon their account set_fact: user_no_action: "{{ hostvars['adserver']['user_no_action'] }}" when: hostvars['adserver']['user_no_action'] is defined - name: Print email recipient information debug: msg: - "requester to recieve status email: {{ requester_email }}" - "enable requester mail: {{ enable_requester_email }}" - "enable customer mail: {{ enable_customer_email }}" - "self service mode: {{ self_service_user }}" - "user(s) that can recieve change notification: {{ email_recipients }}" - "user(s) with no action undertaken: {{ user_no_action }}" - "user(s) that can recieve nochange notification: {{ email_recipients_no_action }}" - set_fact: template_prefix: "default" when: template_prefix is not defined - name: Build nochange email status body set_fact: mail_body: "{{ lookup('template', 'templates/{{ template_prefix }}/mail-nochange.j2') }}" when: not result.changed - debug: # changed_when: false in 'build nochange email' doesnt force notification of topic, use debug as a work around to always get a changed: true status to ensure a desired handler is executed msg: "Invoke nochange handler" changed_when: true when: not result.changed notify: topic_nochange - name: Build add email status body set_fact: mail_body: "{{ lookup('template', 'templates/{{ template_prefix }}/mail-add.j2') }}" changed_when: true when: result.changed and result.added |length > 0 notify: topic_add - name: Build remove email status body set_fact: mail_body: "{{ lookup('template', 'templates/{{ template_prefix }}/mail-remove.j2') }}" changed_when: true when: result.changed and result.removed |length > 0 notify: topic_remove - name: Build add email customer body set_fact: customer_change_mail_body: "{{ lookup('template', 'templates/{{ template_prefix }}/customer-mail-add.j2') }}" changed_when: true when: (result.changed and result.added |length > 0) and template_prefix != 'default' notify: topic_add - name: Build remove email customer body set_fact: customer_change_mail_body: "{{ lookup('template', 'templates/{{ template_prefix }}/customer-mail-remove.j2') }}" changed_when: true when: (result.changed and result.removed |length > 0) and template_prefix != 'default' notify: topic_remove - name: Build nochange add email customer body set_fact: customer_nochange_add_mail_body: "{{ lookup('template', 'templates/{{ template_prefix }}/customer-mail-nochange.j2') }}" changed_when: true when: user_no_action |length >0 and template_prefix != 'default' notify: topic_nochange handlers: - 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 }} : {{ perform }} user(s) in {{ group }}" body: "{{ mail_body }}" #body: "{{ lookup('file', '/var/www/release.txt') }}" # not using files due to WSL/ANSIBLE_RUNNER virtual environments #when: hostvars['localhost']['requester_email'] is defined and enable_requester_email == 'true' when: (hostvars['localhost']['requester_email'] is defined and enable_requester_email == 'true') and not self_service_user listen: - topic_nochange - topic_add - topic_remove - name: Send customer add / remove email mail: host: "{{ smtp_relay }}" port: "{{ smtp_port }}" charset: utf-8 from: "{{ from_email }}" to: "{{ email_recipients }}" #subject: "Cloudforms - Result of {{ body_service_name }}" subject: "Welcome to the {{ body_service_name_noreqid }}" # no attachments, cloudforms ansible-runner has trouble using directory paths # we cant use file filter with binary files such as images # instead our html body should reference the logo @ https://www.nottingham.ac.uk/SiteElements/Images/Base/logo.png #attach: 'templates/{{ template_prefix }}/images/logo.png' subtype: html body: "{{ customer_change_mail_body }}" when: (enable_customer_email == 'true' and email_recipients | length >0) and template_prefix != 'default' listen: - topic_add - topic_remove - name: Send customer nochange add email mail: host: "{{ smtp_relay }}" port: "{{ smtp_port }}" charset: utf-8 from: "{{ from_email }}" to: "{{ email_recipients_no_action }}" subject: "Welcome to the {{ body_service_name_noreqid }}" subtype: html body: "{{ customer_nochange_add_mail_body }}" #when: (enable_customer_email == 'true' and email_recipients_no_action | length >0) and template_prefix != 'default' when: ((enable_customer_email == 'true' and email_recipients_no_action | length >0) and template_prefix != 'default') and state == 'present' listen: - topic_nochange # commit message "disable customer email for scenario: no change with delete action" # quick change so the customer nochange email (no longer being rendered with create/delete value of the perform variable) is only sent if a user is being added and is already in a group # there is no nochange email for a user who is being removed from a group who is already not a member # we likely dont want to fail on this condition when doing lookup on AD as other users still want to be processed (we only fail on invalid users for safety) # is a customer nochange remove email useful? probably not # Clarify with Mike # - name: Send email # debug: # msg: "{{ mail_body }}" # listen: # - topic_nochange # - topic_add # - topic_remove - name: Condition no change debug: msg: - "no changes made to group {{ group }}" - --------------------------------------- - "users that had no action taken due to already being {{ state }} in group" - --------------------------------------- - "{{ user_no_action }}" - --------------------------------------- - "members of group {{ group }}" - --------------------------------------- - "{{ result.members }}" - --------------------------------------- listen: topic_nochange - name: Condition user(s) added debug: msg: - "user(s) added to group {{ group }}" - --------------------------------------- - "{{ result.added }}" - --------------------------------------- - "users that had no action taken due to already being {{ state }} in group" - --------------------------------------- - "{{ user_no_action }}" - --------------------------------------- - "members of group {{ group }}" - --------------------------------------- - "{{ result.members }}" - --------------------------------------- listen: topic_add - name: Condition user(s) removed debug: msg: - "user(s) removed from group {{ group }}" - --------------------------------------- - "{{ result.removed }}" - --------------------------------------- - "users that had no action taken due to already being {{ state }} in group" - --------------------------------------- - "{{ user_no_action }}" - --------------------------------------- - "members of group {{ group }}" - --------------------------------------- - "{{ result.members }}" - --------------------------------------- listen: topic_remove # may need to test verbosity levels with handlers for clean log output in cloudforms, example of using verbosity level # # - name: Print Results # debug: # var: result # verbosity: 0