--- # 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"