diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
commit | 975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch) | |
tree | 89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/community/sap/plugins | |
parent | Initial commit. (diff) | |
download | ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip |
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/community/sap/plugins')
16 files changed, 4174 insertions, 0 deletions
diff --git a/ansible_collections/community/sap/plugins/doc_fragments/__init__.py b/ansible_collections/community/sap/plugins/doc_fragments/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/sap/plugins/doc_fragments/__init__.py diff --git a/ansible_collections/community/sap/plugins/module_utils/__init__.py b/ansible_collections/community/sap/plugins/module_utils/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/sap/plugins/module_utils/__init__.py diff --git a/ansible_collections/community/sap/plugins/modules/database/saphana/hana_query.py b/ansible_collections/community/sap/plugins/modules/database/saphana/hana_query.py new file mode 100644 index 000000000..9eb43db09 --- /dev/null +++ b/ansible_collections/community/sap/plugins/modules/database/saphana/hana_query.py @@ -0,0 +1,238 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: hana_query +short_description: Execute SQL on HANA +version_added: "0.1.0" +description: This module executes SQL statements on HANA with hdbsql. +options: + sid: + description: The system ID. + type: str + required: false + bin_path: + description: The path to the hdbsql binary. + type: str + required: false + instance: + description: The instance number. + type: str + required: true + user: + description: A dedicated username. The user could be also in hdbuserstore. + type: str + default: SYSTEM + userstore: + description: If C(true), the user must be in hdbuserstore. + type: bool + default: false + password: + description: + - The password to connect to the database. + - "B(Note:) Since the passwords have to be passed as command line arguments, I(userstore=true) should + be used whenever possible, as command line arguments can be seen by other users + on the same machine." + type: str + autocommit: + description: Autocommit the statement. + type: bool + default: true + host: + description: The Host IP address. The port can be defined as well. + type: str + database: + description: Define the database on which to connect. + type: str + encrypted: + description: Use encrypted connection. + type: bool + default: false + filepath: + description: + - One or more files each containing one SQL query to run. + - Must be a string or list containing strings. + type: list + elements: path + query: + description: + - SQL query to run. + - Must be a string or list containing strings. Please note that if you supply a string, it will be split by commas (C(,)) to a list. + It is better to supply a one-element list instead to avoid mangled input. + type: list + elements: str +notes: + - Does not support C(check_mode). Always reports that the state has changed even if no changes have been made. +author: + - Rainer Leber (@rainerleber) +''' + +EXAMPLES = r''' +- name: Simple select query + community.sap.hana_query: + sid: "hdb" + instance: "01" + password: "Test123" + query: select user_name from users + +- name: RUN select query with host port + community.sap.hana_query: + sid: "hdb" + instance: "01" + password: "Test123" + host: "10.10.2.4:30001" + query: select user_name from users + +- name: Run several queries + community.sap.hana_query: + sid: "hdb" + instance: "01" + password: "Test123" + query: + - select user_name from users + - select * from SYSTEM + host: "localhost" + autocommit: False + +- name: Run several queries with path + community.sap.hana_query: + bin_path: "/usr/sap/HDB/HDB01/exe/hdbsql" + instance: "01" + password: "Test123" + query: + - select user_name from users + - select * from users + host: "localhost" + autocommit: False + +- name: Run several queries from file + community.sap.hana_query: + sid: "hdb" + instance: "01" + password: "Test123" + filepath: + - /tmp/HANA_CPU_UtilizationPerCore_2.00.020+.txt + - /tmp/HANA.txt + host: "localhost" + +- name: Run several queries from user store + community.sap.hana_query: + sid: "hdb" + instance: "01" + user: hdbstoreuser + userstore: true + query: + - select user_name from users + - select * from users + autocommit: False +''' + +RETURN = r''' +query_result: + description: List containing results of all queries executed (one sublist for every query). + returned: on success + type: list + elements: list + sample: [[{"Column": "Value1"}, {"Column": "Value2"}], [{"Column": "Value1"}, {"Column": "Value2"}]] +''' + +import csv +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import StringIO +from ansible.module_utils.common.text.converters import to_native + + +def csv_to_list(rawcsv): + reader_raw = csv.DictReader(StringIO(rawcsv)) + reader = [dict((k, v.strip()) for k, v in row.items()) for row in reader_raw] + return list(reader) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + sid=dict(type='str', required=False), + bin_path=dict(type='str', required=False), + instance=dict(type='str', required=True), + encrypted=dict(type='bool', default=False), + host=dict(type='str', required=False), + user=dict(type='str', default="SYSTEM"), + userstore=dict(type='bool', default=False), + password=dict(type='str', no_log=True), + database=dict(type='str', required=False), + query=dict(type='list', elements='str', required=False), + filepath=dict(type='list', elements='path', required=False), + autocommit=dict(type='bool', default=True), + ), + required_one_of=[('query', 'filepath'), ('sid', 'instance')], + required_if=[('userstore', False, ['password'])], + supports_check_mode=False, + ) + rc, out, err, out_raw = [0, [], "", ""] + + params = module.params + + sid = params['sid'] + bin_path = params['bin_path'] + instance = params['instance'] + user = params['user'] + userstore = params['userstore'] + password = params['password'] + autocommit = params['autocommit'] + host = params['host'] + database = params['database'] + encrypted = params['encrypted'] + + filepath = params['filepath'] + query = params['query'] + + if bin_path is None: + bin_path = "/usr/sap/{sid}/HDB{instance}/exe/hdbsql".format(sid=sid.upper(), instance=instance) + + try: + command = [module.get_bin_path(bin_path, required=True)] + except Exception as e: + module.fail_json(msg='Failed to find hdbsql at the expected path "{0}".Please check SID and instance number: "{1}"'.format(bin_path, to_native(e))) + + if encrypted is True: + command.extend(['-attemptencrypt']) + if autocommit is False: + command.extend(['-z']) + if host is not None: + command.extend(['-n', host]) + if database is not None: + command.extend(['-d', database]) + # -x Suppresses additional output, such as the number of selected rows in a result set. + if userstore: + command.extend(['-x', '-U', user]) + else: + command.extend(['-x', '-i', instance, '-u', user, '-p', password]) + + if filepath is not None: + command.extend(['-I']) + for p in filepath: + # makes a command like hdbsql -i 01 -u SYSTEM -p secret123# -I /tmp/HANA_CPU_UtilizationPerCore_2.00.020+.txt, + # iterates through files and append the output to var out. + query_command = command + [p] + (rc, out_raw, err) = module.run_command(query_command) + out.append(csv_to_list(out_raw)) + if query is not None: + for q in query: + # makes a command like hdbsql -i 01 -u SYSTEM -p secret123# "select user_name from users", + # iterates through multiple commands and append the output to var out. + query_command = command + [q] + (rc, out_raw, err) = module.run_command(query_command) + out.append(csv_to_list(out_raw)) + changed = True + + module.exit_json(changed=changed, rc=rc, query_result=out, stderr=err) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/sap/plugins/modules/files/sapcar_extract.py b/ansible_collections/community/sap/plugins/modules/files/sapcar_extract.py new file mode 100644 index 000000000..d586dd330 --- /dev/null +++ b/ansible_collections/community/sap/plugins/modules/files/sapcar_extract.py @@ -0,0 +1,220 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: sapcar_extract +short_description: Manages SAP SAPCAR archives +version_added: "0.1.0" +description: + - Provides support for unpacking C(sar)/C(car) files with the SAPCAR binary from SAP and pulling + information back into Ansible. +options: + path: + description: The path to the SAR/CAR file. + type: path + required: true + dest: + description: + - The destination where SAPCAR extracts the SAR file. Missing folders will be created. + If this parameter is not provided, it will unpack in the same folder as the SAR file. + type: path + binary_path: + description: + - The path to the SAPCAR binary, for example, C(/home/dummy/sapcar) or C(https://myserver/SAPCAR). + If this parameter is not provided, the module will look in C(PATH). + type: path + signature: + description: + - If C(true), the signature will be extracted. + default: false + type: bool + security_library: + description: + - The path to the security library, for example, C(/usr/sap/hostctrl/exe/libsapcrytp.so), for signature operations. + type: path + manifest: + description: + - The name of the manifest. + default: "SIGNATURE.SMF" + type: str + remove: + description: + - If C(true), the SAR/CAR file will be removed. B(This should be used with caution!) + default: false + type: bool +author: + - Rainer Leber (@RainerLeber) +notes: + - Always returns C(changed=true) in C(check_mode). +''' + +EXAMPLES = r""" +- name: Extract SAR file + community.sap.sapcar_extract: + path: "~/source/hana.sar" + +- name: Extract SAR file with destination + community.sap.sapcar_extract: + path: "~/source/hana.sar" + dest: "~/test/" + +- name: Extract SAR file with destination and download from webserver can be a fileshare as well + community.sap.sapcar_extract: + path: "~/source/hana.sar" + dest: "~/dest/" + binary_path: "https://myserver/SAPCAR" + +- name: Extract SAR file and delete SAR after extract + community.sap.sapcar_extract: + path: "~/source/hana.sar" + remove: true + +- name: Extract SAR file with manifest + community.sap.sapcar_extract: + path: "~/source/hana.sar" + signature: true + +- name: Extract SAR file with manifest and rename it + community.sap.sapcar_extract: + path: "~/source/hana.sar" + manifest: "MyNewSignature.SMF" + signature: true +""" + +import os +from tempfile import NamedTemporaryFile +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.urls import open_url +from ansible.module_utils.common.text.converters import to_native + + +def get_list_of_files(dir_name): + # create a list of file and directories + # names in the given directory + list_of_file = os.listdir(dir_name) + allFiles = list() + # Iterate over all the entries + for entry in list_of_file: + # Create full path + fullPath = os.path.join(dir_name, entry) + # If entry is a directory then get the list of files in this directory + if os.path.isdir(fullPath): + allFiles = allFiles + [fullPath] + allFiles = allFiles + get_list_of_files(fullPath) + else: + allFiles.append(fullPath) + return allFiles + + +def download_SAPCAR(binary_path, module): + bin_path = None + # download sapcar binary if url is provided otherwise path is returned + if binary_path is not None: + if binary_path.startswith('https://') or binary_path.startswith('http://'): + random_file = NamedTemporaryFile(delete=False) + with open_url(binary_path) as response: + with random_file as out_file: + data = response.read() + out_file.write(data) + os.chmod(out_file.name, 0o700) + bin_path = out_file.name + module.add_cleanup_file(bin_path) + else: + bin_path = binary_path + return bin_path + + +def check_if_present(command, path, dest, signature, manifest, module): + # manipulating output from SAR file for compare with already extracted files + iter_command = [command, '-tvf', path] + sar_out = module.run_command(iter_command)[1] + sar_raw = sar_out.split("\n")[1:] + if dest[-1] != "/": + dest = dest + "/" + sar_files = [dest + x.split(" ")[-1] for x in sar_raw if x] + # remove any SIGNATURE.SMF from list because it will not unpacked if signature is false + if not signature: + sar_files = [item for item in sar_files if not item.endswith('.SMF')] + # if signature is renamed manipulate files in list of sar file for compare. + if manifest != "SIGNATURE.SMF": + sar_files = [item for item in sar_files if not item.endswith('.SMF')] + sar_files = sar_files + [manifest] + # get extracted files if present + files_extracted = get_list_of_files(dest) + # compare extracted files with files in sar file + present = all(elem in files_extracted for elem in sar_files) + return present + + +def main(): + module = AnsibleModule( + argument_spec=dict( + path=dict(type='path', required=True), + dest=dict(type='path'), + binary_path=dict(type='path'), + signature=dict(type='bool', default=False), + security_library=dict(type='path'), + manifest=dict(type='str', default="SIGNATURE.SMF"), + remove=dict(type='bool', default=False), + ), + supports_check_mode=True, + ) + rc, out, err = [0, "", ""] + params = module.params + check_mode = module.check_mode + + path = params['path'] + dest = params['dest'] + signature = params['signature'] + security_library = params['security_library'] + manifest = params['manifest'] + remove = params['remove'] + + bin_path = download_SAPCAR(params['binary_path'], module) + + if dest is None: + dest_head_tail = os.path.split(path) + dest = dest_head_tail[0] + '/' + else: + if not os.path.exists(dest): + os.makedirs(dest, 0o755) + + if bin_path is not None: + command = [module.get_bin_path(bin_path, required=True)] + else: + try: + command = [module.get_bin_path('sapcar', required=True)] + except Exception as e: + module.fail_json(msg='Failed to find SAPCAR at the expected path or URL "{0}". Please check whether it is available: {1}' + .format(bin_path, to_native(e))) + + present = check_if_present(command[0], path, dest, signature, manifest, module) + + if not present: + command.extend(['-xvf', path, '-R', dest]) + if security_library: + command.extend(['-L', security_library]) + if signature: + command.extend(['-manifest', manifest]) + if not check_mode: + (rc, out, err) = module.run_command(command, check_rc=True) + changed = True + else: + changed = False + out = "already unpacked" + + if remove: + os.remove(path) + + module.exit_json(changed=changed, message=rc, stdout=out, + stderr=err, command=' '.join(command)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/sap/plugins/modules/hana_query.py b/ansible_collections/community/sap/plugins/modules/hana_query.py new file mode 100644 index 000000000..9eb43db09 --- /dev/null +++ b/ansible_collections/community/sap/plugins/modules/hana_query.py @@ -0,0 +1,238 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: hana_query +short_description: Execute SQL on HANA +version_added: "0.1.0" +description: This module executes SQL statements on HANA with hdbsql. +options: + sid: + description: The system ID. + type: str + required: false + bin_path: + description: The path to the hdbsql binary. + type: str + required: false + instance: + description: The instance number. + type: str + required: true + user: + description: A dedicated username. The user could be also in hdbuserstore. + type: str + default: SYSTEM + userstore: + description: If C(true), the user must be in hdbuserstore. + type: bool + default: false + password: + description: + - The password to connect to the database. + - "B(Note:) Since the passwords have to be passed as command line arguments, I(userstore=true) should + be used whenever possible, as command line arguments can be seen by other users + on the same machine." + type: str + autocommit: + description: Autocommit the statement. + type: bool + default: true + host: + description: The Host IP address. The port can be defined as well. + type: str + database: + description: Define the database on which to connect. + type: str + encrypted: + description: Use encrypted connection. + type: bool + default: false + filepath: + description: + - One or more files each containing one SQL query to run. + - Must be a string or list containing strings. + type: list + elements: path + query: + description: + - SQL query to run. + - Must be a string or list containing strings. Please note that if you supply a string, it will be split by commas (C(,)) to a list. + It is better to supply a one-element list instead to avoid mangled input. + type: list + elements: str +notes: + - Does not support C(check_mode). Always reports that the state has changed even if no changes have been made. +author: + - Rainer Leber (@rainerleber) +''' + +EXAMPLES = r''' +- name: Simple select query + community.sap.hana_query: + sid: "hdb" + instance: "01" + password: "Test123" + query: select user_name from users + +- name: RUN select query with host port + community.sap.hana_query: + sid: "hdb" + instance: "01" + password: "Test123" + host: "10.10.2.4:30001" + query: select user_name from users + +- name: Run several queries + community.sap.hana_query: + sid: "hdb" + instance: "01" + password: "Test123" + query: + - select user_name from users + - select * from SYSTEM + host: "localhost" + autocommit: False + +- name: Run several queries with path + community.sap.hana_query: + bin_path: "/usr/sap/HDB/HDB01/exe/hdbsql" + instance: "01" + password: "Test123" + query: + - select user_name from users + - select * from users + host: "localhost" + autocommit: False + +- name: Run several queries from file + community.sap.hana_query: + sid: "hdb" + instance: "01" + password: "Test123" + filepath: + - /tmp/HANA_CPU_UtilizationPerCore_2.00.020+.txt + - /tmp/HANA.txt + host: "localhost" + +- name: Run several queries from user store + community.sap.hana_query: + sid: "hdb" + instance: "01" + user: hdbstoreuser + userstore: true + query: + - select user_name from users + - select * from users + autocommit: False +''' + +RETURN = r''' +query_result: + description: List containing results of all queries executed (one sublist for every query). + returned: on success + type: list + elements: list + sample: [[{"Column": "Value1"}, {"Column": "Value2"}], [{"Column": "Value1"}, {"Column": "Value2"}]] +''' + +import csv +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import StringIO +from ansible.module_utils.common.text.converters import to_native + + +def csv_to_list(rawcsv): + reader_raw = csv.DictReader(StringIO(rawcsv)) + reader = [dict((k, v.strip()) for k, v in row.items()) for row in reader_raw] + return list(reader) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + sid=dict(type='str', required=False), + bin_path=dict(type='str', required=False), + instance=dict(type='str', required=True), + encrypted=dict(type='bool', default=False), + host=dict(type='str', required=False), + user=dict(type='str', default="SYSTEM"), + userstore=dict(type='bool', default=False), + password=dict(type='str', no_log=True), + database=dict(type='str', required=False), + query=dict(type='list', elements='str', required=False), + filepath=dict(type='list', elements='path', required=False), + autocommit=dict(type='bool', default=True), + ), + required_one_of=[('query', 'filepath'), ('sid', 'instance')], + required_if=[('userstore', False, ['password'])], + supports_check_mode=False, + ) + rc, out, err, out_raw = [0, [], "", ""] + + params = module.params + + sid = params['sid'] + bin_path = params['bin_path'] + instance = params['instance'] + user = params['user'] + userstore = params['userstore'] + password = params['password'] + autocommit = params['autocommit'] + host = params['host'] + database = params['database'] + encrypted = params['encrypted'] + + filepath = params['filepath'] + query = params['query'] + + if bin_path is None: + bin_path = "/usr/sap/{sid}/HDB{instance}/exe/hdbsql".format(sid=sid.upper(), instance=instance) + + try: + command = [module.get_bin_path(bin_path, required=True)] + except Exception as e: + module.fail_json(msg='Failed to find hdbsql at the expected path "{0}".Please check SID and instance number: "{1}"'.format(bin_path, to_native(e))) + + if encrypted is True: + command.extend(['-attemptencrypt']) + if autocommit is False: + command.extend(['-z']) + if host is not None: + command.extend(['-n', host]) + if database is not None: + command.extend(['-d', database]) + # -x Suppresses additional output, such as the number of selected rows in a result set. + if userstore: + command.extend(['-x', '-U', user]) + else: + command.extend(['-x', '-i', instance, '-u', user, '-p', password]) + + if filepath is not None: + command.extend(['-I']) + for p in filepath: + # makes a command like hdbsql -i 01 -u SYSTEM -p secret123# -I /tmp/HANA_CPU_UtilizationPerCore_2.00.020+.txt, + # iterates through files and append the output to var out. + query_command = command + [p] + (rc, out_raw, err) = module.run_command(query_command) + out.append(csv_to_list(out_raw)) + if query is not None: + for q in query: + # makes a command like hdbsql -i 01 -u SYSTEM -p secret123# "select user_name from users", + # iterates through multiple commands and append the output to var out. + query_command = command + [q] + (rc, out_raw, err) = module.run_command(query_command) + out.append(csv_to_list(out_raw)) + changed = True + + module.exit_json(changed=changed, rc=rc, query_result=out, stderr=err) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/sap/plugins/modules/identity/sap_company.py b/ansible_collections/community/sap/plugins/modules/identity/sap_company.py new file mode 100644 index 000000000..8d3838e5f --- /dev/null +++ b/ansible_collections/community/sap/plugins/modules/identity/sap_company.py @@ -0,0 +1,326 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> <rainer.leber@sva.de> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: sap_company + +short_description: This module will manage a company entities in a SAP S4HANA environment + +version_added: "1.0.0" + +description: + - The M(community.sap.sap_user) module depends on C(pyrfc) Python library (version 2.4.0 and upwards). + Depending on distribution you are using, you may need to install additional packages to + have these available. + - This module will use the company BAPIs C(BAPI_COMPANY_CLONE) and C(BAPI_COMPANY_DELETE) to manage company entities. + +options: + state: + description: + - The decision what to do with the company. + default: 'present' + choices: + - 'present' + - 'absent' + required: false + type: str + conn_username: + description: The required username for the SAP system. + required: true + type: str + conn_password: + description: The required password for the SAP system. + required: true + type: str + host: + description: The required host for the SAP system. Can be either an FQDN or IP Address. + required: true + type: str + sysnr: + description: + - The system number of the SAP system. + - You must quote the value to ensure retaining the leading zeros. + required: false + default: '01' + type: str + client: + description: + - The client number to connect to. + - You must quote the value to ensure retaining the leading zeros. + required: false + default: '000' + type: str + company_id: + description: The company id. + required: true + type: str + name: + description: The company name. + required: false + type: str + name_2: + description: Additional company name. + required: false + type: str + country: + description: The country code for the company. For example, C('DE'). + required: false + type: str + time_zone: + description: The timezone. + required: false + type: str + city: + description: The city where the company is located. + required: false + type: str + post_code: + description: The post code from the city. + required: false + type: str + street: + description: Street where the company is located. + required: false + type: str + street_no: + description: Street number. + required: false + type: str + e_mail: + description: General E-Mail address. + required: false + type: str + +requirements: + - pyrfc >= 2.4.0 + +author: + - Rainer Leber (@rainerleber) + +notes: + - Does not support C(check_mode). +''' + +EXAMPLES = r''' +- name: Create SAP Company + community.sap.sap_company: + conn_username: 'DDIC' + conn_password: 'HECtna2021#' + host: 100.0.201.20 + sysnr: '01' + client: '000' + state: present + company_id: "Comp_ID" + name: "Test_comp" + name_2: "LTD" + country: "DE" + time_zone: "UTC" + city: "City" + post_code: "12345" + street: "test_street" + street_no: "1" + e_mail: "test@test.de" + +# pass in a message and have changed true +- name: Delete SAP Company + community.sap.sap_company: + conn_username: 'DDIC' + conn_password: 'HECtna2021#' + host: 100.0.201.20 + sysnr: '01' + client: '000' + state: absent + company_id: "Comp_ID" + name: "Test_comp" + name_2: "LTD" + country: "DE" + time_zone: "UTC" + city: "City" + post_code: "12345" + street: "test_street" + street_no: "1" + e_mail: "test@test.de" +''' + +RETURN = r''' +# These are examples of possible return values, and in general should use other names for return values. +msg: + description: A small execution description. + type: str + returned: always + sample: 'Company address COMP_ID created' +out: + description: A complete description of the executed tasks. If this is available. + type: list + elements: dict + returned: always + sample: '{ + "RETURN": [ + { + "FIELD": "", + "ID": "01", + "LOG_MSG_NO": "000000", + "LOG_NO": "", + "MESSAGE": "Company address COMP_ID created", + "MESSAGE_V1": "COMP_ID", + "MESSAGE_V2": "", + "MESSAGE_V3": "", + "MESSAGE_V4": "", + "NUMBER": "078", + "PARAMETER": "", + "ROW": 0, + "SYSTEM": "", + "TYPE": "S" + } + ] + } + }' +''' + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +import traceback +try: + from pyrfc import Connection +except ImportError: + HAS_PYRFC_LIBRARY = False + ANOTHER_LIBRARY_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYRFC_LIBRARY = True + + +def call_rfc_method(connection, method_name, kwargs): + # PyRFC call function + return connection.call(method_name, **kwargs) + + +def build_company_params(name, name_2, country, time_zone, city, post_code, street, street_no, e_mail): + # Creates RFC parameters for creating organizations + # define dicts in batch + params = dict() + # define company name + params['NAME'] = name + params['NAME_2'] = name_2 + # define location + params['COUNTRY'] = country + params['TIME_ZONE'] = time_zone + params['CITY'] = city + params['POSTL_COD1'] = post_code + params['STREET'] = street + params['STREET_NO'] = street_no + # define communication + params['E_MAIL'] = e_mail + # return dict + return params + + +def return_analysis(raw): + change = False + failed = False + msg = raw['RETURN'][0]['MESSAGE'] + for state in raw['RETURN']: + if state['TYPE'] == "E": + if state['NUMBER'] == '081': + change = False + else: + failed = True + if state['TYPE'] == "S": + if state['NUMBER'] != '079': + change = True + else: + msg = "No changes where made." + return [{"change": change}, {"failed": failed}, {"msg": msg}] + + +def run_module(): + module = AnsibleModule( + argument_spec=dict( + state=dict(default='present', choices=['absent', 'present']), + conn_username=dict(type='str', required=True), + conn_password=dict(type='str', required=True, no_log=True), + host=dict(type='str', required=True), + sysnr=dict(type='str', default="01"), + client=dict(type='str', default="000"), + company_id=dict(type='str', required=True), + name=dict(type='str', required=False), + name_2=dict(type='str', required=False), + country=dict(type='str', required=False), + time_zone=dict(type='str', required=False), + city=dict(type='str', required=False), + post_code=dict(type='str', required=False), + street=dict(type='str', required=False), + street_no=dict(type='str', required=False), + e_mail=dict(type='str', required=False), + ), + supports_check_mode=False, + ) + result = dict(changed=False, msg='', out={}) + raw = "" + + params = module.params + + state = params['state'] + conn_username = (params['conn_username']).upper() + conn_password = params['conn_password'] + host = params['host'] + sysnr = params['sysnr'] + client = params['client'] + + company_id = (params['company_id']).upper() + name = params['name'] + name_2 = params['name_2'] + country = params['country'] + time_zone = params['time_zone'] + city = params['city'] + post_code = params['post_code'] + street = params['street'] + street_no = params['street_no'] + e_mail = params['e_mail'] + + if not HAS_PYRFC_LIBRARY: + module.fail_json( + msg=missing_required_lib('pyrfc'), + exception=ANOTHER_LIBRARY_IMPORT_ERROR) + + # basic RFC connection with pyrfc + try: + conn = Connection(user=conn_username, passwd=conn_password, ashost=host, sysnr=sysnr, client=client) + except Exception as err: + result['error'] = str(err) + result['msg'] = 'Something went wrong connecting to the SAP system.' + module.fail_json(**result) + + # build parameter dict of dict + company_params = build_company_params(name, name_2, country, time_zone, city, post_code, street, street_no, e_mail) + + if state == "absent": + raw = call_rfc_method(conn, 'BAPI_COMPANY_DELETE', {'COMPANY': company_id}) + + if state == "present": + raw = call_rfc_method(conn, 'BAPI_COMPANY_CLONE', + {'METHOD': {'USMETHOD': 'COMPANY_CLONE'}, 'COMPANY': company_id, 'COMP_DATA': company_params}) + + analysed = return_analysis(raw) + + result['out'] = raw + + result['changed'] = analysed[0]['change'] + result['msg'] = analysed[2]['msg'] + + if analysed[1]['failed']: + module.fail_json(**result) + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/sap/plugins/modules/identity/sap_user.py b/ansible_collections/community/sap/plugins/modules/identity/sap_user.py new file mode 100644 index 000000000..f83472657 --- /dev/null +++ b/ansible_collections/community/sap/plugins/modules/identity/sap_user.py @@ -0,0 +1,499 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> <rainer.leber@sva.de> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: sap_user +short_description: This module will manage a user entities in a SAP S4/HANA environment +version_added: "1.0.0" +description: + - The M(community.sap.sap_user) module depends on C(pyrfc) Python library (version 2.4.0 and upwards). + Depending on distribution you are using, you may need to install additional packages to + have these available. + - This module will use the following user BAPIs to manage user entities. + - C(BAPI_USER_GET_DETAIL) + - C(BAPI_USER_DELETE) + - C(BAPI_USER_CREATE1) + - C(BAPI_USER_CHANGE) + - C(BAPI_USER_ACTGROUPS_ASSIGN) + - C(BAPI_USER_PROFILES_ASSIGN) + - C(BAPI_USER_UNLOCK) + - C(BAPI_USER_LOCK) +options: + state: + description: + - The decision what to do with the user. + default: 'present' + choices: + - 'present' + - 'absent' + - 'lock' + - 'unlock' + required: false + type: str + force: + description: + - Must be C('True') if the password or type should be overwritten. + default: False + required: false + type: bool + conn_username: + description: The required username for the SAP system. + required: true + type: str + conn_password: + description: The required password for the SAP system. + required: true + type: str + host: + description: The required host for the SAP system. Can be either an FQDN or IP Address. + required: true + type: str + sysnr: + description: + - The system number of the SAP system. + - You must quote the value to ensure retaining the leading zeros. + default: '00' + type: str + client: + description: + - The client number to connect to. + - You must quote the value to ensure retaining the leading zeros. + default: '000' + type: str + username: + description: + - The username. + type: str + required: true + firstname: + description: + - The Firstname of the user in the SAP system. + type: str + required: false + lastname: + description: + - The lastname of the user in the SAP system. + type: str + required: false + email: + description: + - The email address of the user in the SAP system. + type: str + required: false + password: + description: + - The password for the user in the SAP system. + type: str + required: false + useralias: + description: + - The alias for the user in the SAP system. + type: str + required: false + user_type: + description: + - The type for the user in the SAP system. + - C('A') Dialog user, C('B') System User, C('C') Communication User, + C('S') Service User, C('L') Reference User. + - Must be in uppercase. + type: str + required: false + default: 'A' + choices: ['A', 'B', 'C', 'S', 'L'] + company: + description: + - The specific company the user belongs to. + - The company name must be available in the SAP system. + type: str + required: false + profiles: + description: + - Assign profiles to the user. + - Should be in uppercase, for example C('SAP_NEW') or C('SAP_ALL'). + type: list + elements: str + default: [''] + required: false + roles: + description: + - Assign roles to the user. + type: list + elements: str + default: [''] + required: false + +requirements: + - pyrfc >= 2.4.0 +author: + - Rainer Leber (@rainerleber) +notes: + - Does not support C(check_mode). +''' + +EXAMPLES = r''' +- name: Create SAP User + community.sap.sap_user: + conn_username: 'DDIC' + conn_password: 'Test123' + host: 192.168.1.150 + sysnr: '01' + client: '000' + state: present + username: ADMIN + firstname: first_admin + lastname: last_admin + email: admin@test.de + password: Test123456 + useralias: ADMIN + company: DEFAULT_COMPANY + roles: + - "SAP_ALL" + +- name: Force change SAP User + community.sap.sap_user: + conn_username: 'DDIC' + conn_password: 'Test123' + host: 192.168.1.150 + sysnr: '01' + client: '000' + state: present + force: true + username: ADMIN + firstname: first_admin + lastname: last_admin + email: admin@test.de + password: Test123456 + useralias: ADMIN + company: DEFAULT_COMPANY + roles: + - "SAP_ALL" + +- name: Delete SAP User + community.sap.sap_user: + conn_username: 'DDIC' + conn_password: 'Test123' + host: 192.168.1.150 + sysnr: '01' + client: '000' + state: absent + force: true + username: ADMIN + +- name: Unlock SAP User + community.sap.sap_user: + conn_username: 'DDIC' + conn_password: 'Test123' + host: 192.168.1.150 + sysnr: '01' + client: '000' + state: unlock + force: true + username: ADMIN +''' + +RETURN = r''' +msg: + description: A small execution description about the user action. + type: str + returned: always + sample: 'User ADMIN created' +out: + description: A detailed description about the user action. + type: list + elements: dict + returned: on success + sample: [...,{ + "RETURN": [ + { + "FIELD": "BNAME", + "ID": "01", + "LOG_MSG_NO": "000000", + "LOG_NO": "", + "MESSAGE": "User ADMIN created", + "MESSAGE_V1": "ADMIN", + "MESSAGE_V2": "", + "MESSAGE_V3": "", + "MESSAGE_V4": "", + "NUMBER": "102", + "PARAMETER": "", + "ROW": 0, + "SYSTEM": "", + "TYPE": "S" + } + ], + "SAPUSER_UUID_HIST": []}] +''' +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +import traceback +import datetime +try: + from pyrfc import Connection +except ImportError: + HAS_PYRFC_LIBRARY = False + PYRFC_LIBRARY_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYRFC_LIBRARY = True + + +def add_to_dict(target_dict, target_key, value): + # Adds the given value to a dict as the key + # check if the given key is in the given dict yet + if target_key in target_dict: + return False + target_dict[target_key] = value + return True + + +def call_rfc_method(connection, method_name, kwargs): + # PyRFC call function + return connection.call(method_name, **kwargs) + + +def build_rfc_user_params(username, firstname, lastname, email, raw_password, + useralias, user_type, raw_company, user_change, force): + """Creates RFC parameters for Creating users""" + # define dicts in batch + params = dict() + address = dict() + password = dict() + alias = dict() + logondata = dict() + company = dict() + # for change parameters + addressx = dict() + passwordx = dict() + logondatax = dict() + companyx = dict() + # define username + add_to_dict(params, 'USERNAME', username) + # define Address + add_to_dict(address, 'FIRSTNAME', firstname) + add_to_dict(address, 'LASTNAME', lastname) + add_to_dict(address, 'E_MAIL', email) + # define Password + add_to_dict(password, 'BAPIPWD', raw_password) + # define Alias + add_to_dict(alias, 'USERALIAS', useralias) + # define LogonData + add_to_dict(logondata, 'GLTGV', datetime.date.today()) + add_to_dict(logondata, 'GLTGB', '20991231') + add_to_dict(logondata, 'USTYP', user_type) + # define company + add_to_dict(company, 'COMPANY', raw_company) + params['LOGONDATA'] = logondata + params['ADDRESS'] = address + params['COMPANY'] = company + params['ALIAS'] = alias + params['PASSWORD'] = password + # add change if user exists + if user_change and force: + add_to_dict(addressx, 'FIRSTNAME', 'X') + add_to_dict(addressx, 'LASTNAME', 'X') + add_to_dict(addressx, 'E_MAIL', 'X') + # define Password + add_to_dict(passwordx, 'BAPIPWD', 'X') + # define LogonData + add_to_dict(logondatax, 'USTYP', 'X') + # define company + add_to_dict(companyx, 'COMPANY', 'X') + params['LOGONDATAX'] = logondatax + params['ADDRESSX'] = addressx + params['COMPANYX'] = companyx + params['PASSWORDX'] = passwordx + return params + + +def user_role_assignment_build_rfc_params(roles, username): + rfc_table = [] + + for role_name in roles: + table_row = {'AGR_NAME': role_name} + + add_to_dict(table_row, 'FROM_DAT', datetime.date.today()) + add_to_dict(table_row, 'TO_DAT', '20991231') + + rfc_table.append(table_row) + + return { + 'USERNAME': username, + 'ACTIVITYGROUPS': rfc_table + } + + +def user_profile_assignment_build_rfc_params(profiles, username): + rfc_table = [] + + for profile_name in profiles: + table_row = {'BAPIPROF': profile_name} + rfc_table.append(table_row) + + return { + 'USERNAME': username, + 'PROFILES': rfc_table + } + + +def check_user(user_detail): + if len(user_detail['RETURN']) > 0: + for sub in user_detail['RETURN']: + if sub['NUMBER'] == '124': + return False + return True + + +def return_analysis(raw): + change = False + failed = False + for state in raw['RETURN']: + if state['TYPE'] == "E": + if state['NUMBER'] == '224' or state['NUMBER'] == '124': + change = False + else: + failed = True + if state['TYPE'] == "S": + if state['NUMBER'] != '029': + change = True + if state['TYPE'] == "W": + if state['NUMBER'] == '049' or state['NUMBER'] == '047': + change = True + if state['NUMBER'] == '255': + change = True + return [{"change": change}, {"failed": failed}] + + +def run_module(): + module = AnsibleModule( + argument_spec=dict( + # logical values + state=dict(default='present', choices=[ + 'absent', 'present', 'lock', 'unlock']), + force=dict(type='bool', default=False), + # values for connection + conn_username=dict(type='str', required=True), + conn_password=dict(type='str', required=True, no_log=True), + host=dict(type='str', required=True), + sysnr=dict(type='str', default="00"), + client=dict(type='str', default="000"), + # values for the new or existing user + username=dict(type='str', required=True), + firstname=dict(type='str', required=False), + lastname=dict(type='str', required=False), + email=dict(type='str', required=False), + password=dict(type='str', required=False, no_log=True), + useralias=dict(type='str', required=False), + user_type=dict(default="A", + choices=['A', 'B', 'C', 'S', 'L']), + company=dict(type='str', required=False), + # values for profile must a list + # Example ["SAP_NEW", "SAP_ALL"] + profiles=dict(type='list', elements='str', default=[""]), + # values for roles must a list + roles=dict(type='list', elements='str', default=[""]), + ), + supports_check_mode=False, + required_if=[('state', 'present', ['useralias', 'company'])] + ) + result = dict(changed=False, msg='', out='') + count = 0 + raw = "" + + params = module.params + + state = params['state'] + conn_username = (params['conn_username']).upper() + conn_password = params['conn_password'] + host = params['host'] + sysnr = params['sysnr'] + client = params['client'] + + username = (params['username']).upper() + firstname = params['firstname'] + lastname = params['lastname'] + email = params['email'] + password = params['password'] + force = params['force'] + if not params['useralias'] is None: + useralias = (params['useralias']).upper() + user_type = (params['user_type']).upper() + company = params['company'] + + profiles = params['profiles'] + roles = params['roles'] + + if not HAS_PYRFC_LIBRARY: + module.fail_json( + msg=missing_required_lib('pyrfc'), + exception=PYRFC_LIBRARY_IMPORT_ERROR) + + # basic RFC connection with pyrfc + try: + conn = Connection(user=conn_username, passwd=conn_password, ashost=host, sysnr=sysnr, client=client) + except Exception as err: + result['error'] = str(err) + result['msg'] = 'Something went wrong connecting to the SAP system.' + module.fail_json(**result) + + # user details + user_detail = call_rfc_method(conn, 'BAPI_USER_GET_DETAIL', {'USERNAME': username}) + user_exists = check_user(user_detail) + + if state == "absent": + if user_exists: + raw = call_rfc_method(conn, 'BAPI_USER_DELETE', {'USERNAME': username}) + + if state == "present": + user_params = build_rfc_user_params(username, firstname, lastname, email, password, useralias, user_type, company, user_exists, force) + if not user_exists: + raw = call_rfc_method(conn, 'BAPI_USER_CREATE1', user_params) + + if user_exists: + # check for address changes when user exists + user_no_changes = all((user_detail.get('ADDRESS')).get(k) == v for k, v in (user_params.get('ADDRESS')).items()) + if not user_no_changes or force: + raw = call_rfc_method(conn, 'BAPI_USER_CHANGE', user_params) + + call_rfc_method(conn, 'BAPI_USER_ACTGROUPS_ASSIGN', user_role_assignment_build_rfc_params(roles, username)) + + call_rfc_method(conn, 'BAPI_USER_PROFILES_ASSIGN', user_profile_assignment_build_rfc_params(profiles, username)) + + if state == "unlock": + if user_exists: + raw = call_rfc_method(conn, 'BAPI_USER_UNLOCK', {'USERNAME': username}) + + if state == "lock": + if user_exists: + raw = call_rfc_method(conn, 'BAPI_USER_LOCK', {'USERNAME': username}) + + # analyse return value + if raw != '': + analysed = return_analysis(raw) + + result['out'] = raw + + result['changed'] = analysed[0]['change'] + for msgs in raw['RETURN']: + if count > 0: + result['msg'] = result['msg'] + '\n' + result['msg'] = result['msg'] + msgs['MESSAGE'] + count = count + 1 + + if analysed[1]['failed']: + module.fail_json(**result) + else: + result['msg'] = "No changes where made." + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/sap/plugins/modules/sap_company.py b/ansible_collections/community/sap/plugins/modules/sap_company.py new file mode 100644 index 000000000..8d3838e5f --- /dev/null +++ b/ansible_collections/community/sap/plugins/modules/sap_company.py @@ -0,0 +1,326 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> <rainer.leber@sva.de> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: sap_company + +short_description: This module will manage a company entities in a SAP S4HANA environment + +version_added: "1.0.0" + +description: + - The M(community.sap.sap_user) module depends on C(pyrfc) Python library (version 2.4.0 and upwards). + Depending on distribution you are using, you may need to install additional packages to + have these available. + - This module will use the company BAPIs C(BAPI_COMPANY_CLONE) and C(BAPI_COMPANY_DELETE) to manage company entities. + +options: + state: + description: + - The decision what to do with the company. + default: 'present' + choices: + - 'present' + - 'absent' + required: false + type: str + conn_username: + description: The required username for the SAP system. + required: true + type: str + conn_password: + description: The required password for the SAP system. + required: true + type: str + host: + description: The required host for the SAP system. Can be either an FQDN or IP Address. + required: true + type: str + sysnr: + description: + - The system number of the SAP system. + - You must quote the value to ensure retaining the leading zeros. + required: false + default: '01' + type: str + client: + description: + - The client number to connect to. + - You must quote the value to ensure retaining the leading zeros. + required: false + default: '000' + type: str + company_id: + description: The company id. + required: true + type: str + name: + description: The company name. + required: false + type: str + name_2: + description: Additional company name. + required: false + type: str + country: + description: The country code for the company. For example, C('DE'). + required: false + type: str + time_zone: + description: The timezone. + required: false + type: str + city: + description: The city where the company is located. + required: false + type: str + post_code: + description: The post code from the city. + required: false + type: str + street: + description: Street where the company is located. + required: false + type: str + street_no: + description: Street number. + required: false + type: str + e_mail: + description: General E-Mail address. + required: false + type: str + +requirements: + - pyrfc >= 2.4.0 + +author: + - Rainer Leber (@rainerleber) + +notes: + - Does not support C(check_mode). +''' + +EXAMPLES = r''' +- name: Create SAP Company + community.sap.sap_company: + conn_username: 'DDIC' + conn_password: 'HECtna2021#' + host: 100.0.201.20 + sysnr: '01' + client: '000' + state: present + company_id: "Comp_ID" + name: "Test_comp" + name_2: "LTD" + country: "DE" + time_zone: "UTC" + city: "City" + post_code: "12345" + street: "test_street" + street_no: "1" + e_mail: "test@test.de" + +# pass in a message and have changed true +- name: Delete SAP Company + community.sap.sap_company: + conn_username: 'DDIC' + conn_password: 'HECtna2021#' + host: 100.0.201.20 + sysnr: '01' + client: '000' + state: absent + company_id: "Comp_ID" + name: "Test_comp" + name_2: "LTD" + country: "DE" + time_zone: "UTC" + city: "City" + post_code: "12345" + street: "test_street" + street_no: "1" + e_mail: "test@test.de" +''' + +RETURN = r''' +# These are examples of possible return values, and in general should use other names for return values. +msg: + description: A small execution description. + type: str + returned: always + sample: 'Company address COMP_ID created' +out: + description: A complete description of the executed tasks. If this is available. + type: list + elements: dict + returned: always + sample: '{ + "RETURN": [ + { + "FIELD": "", + "ID": "01", + "LOG_MSG_NO": "000000", + "LOG_NO": "", + "MESSAGE": "Company address COMP_ID created", + "MESSAGE_V1": "COMP_ID", + "MESSAGE_V2": "", + "MESSAGE_V3": "", + "MESSAGE_V4": "", + "NUMBER": "078", + "PARAMETER": "", + "ROW": 0, + "SYSTEM": "", + "TYPE": "S" + } + ] + } + }' +''' + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +import traceback +try: + from pyrfc import Connection +except ImportError: + HAS_PYRFC_LIBRARY = False + ANOTHER_LIBRARY_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYRFC_LIBRARY = True + + +def call_rfc_method(connection, method_name, kwargs): + # PyRFC call function + return connection.call(method_name, **kwargs) + + +def build_company_params(name, name_2, country, time_zone, city, post_code, street, street_no, e_mail): + # Creates RFC parameters for creating organizations + # define dicts in batch + params = dict() + # define company name + params['NAME'] = name + params['NAME_2'] = name_2 + # define location + params['COUNTRY'] = country + params['TIME_ZONE'] = time_zone + params['CITY'] = city + params['POSTL_COD1'] = post_code + params['STREET'] = street + params['STREET_NO'] = street_no + # define communication + params['E_MAIL'] = e_mail + # return dict + return params + + +def return_analysis(raw): + change = False + failed = False + msg = raw['RETURN'][0]['MESSAGE'] + for state in raw['RETURN']: + if state['TYPE'] == "E": + if state['NUMBER'] == '081': + change = False + else: + failed = True + if state['TYPE'] == "S": + if state['NUMBER'] != '079': + change = True + else: + msg = "No changes where made." + return [{"change": change}, {"failed": failed}, {"msg": msg}] + + +def run_module(): + module = AnsibleModule( + argument_spec=dict( + state=dict(default='present', choices=['absent', 'present']), + conn_username=dict(type='str', required=True), + conn_password=dict(type='str', required=True, no_log=True), + host=dict(type='str', required=True), + sysnr=dict(type='str', default="01"), + client=dict(type='str', default="000"), + company_id=dict(type='str', required=True), + name=dict(type='str', required=False), + name_2=dict(type='str', required=False), + country=dict(type='str', required=False), + time_zone=dict(type='str', required=False), + city=dict(type='str', required=False), + post_code=dict(type='str', required=False), + street=dict(type='str', required=False), + street_no=dict(type='str', required=False), + e_mail=dict(type='str', required=False), + ), + supports_check_mode=False, + ) + result = dict(changed=False, msg='', out={}) + raw = "" + + params = module.params + + state = params['state'] + conn_username = (params['conn_username']).upper() + conn_password = params['conn_password'] + host = params['host'] + sysnr = params['sysnr'] + client = params['client'] + + company_id = (params['company_id']).upper() + name = params['name'] + name_2 = params['name_2'] + country = params['country'] + time_zone = params['time_zone'] + city = params['city'] + post_code = params['post_code'] + street = params['street'] + street_no = params['street_no'] + e_mail = params['e_mail'] + + if not HAS_PYRFC_LIBRARY: + module.fail_json( + msg=missing_required_lib('pyrfc'), + exception=ANOTHER_LIBRARY_IMPORT_ERROR) + + # basic RFC connection with pyrfc + try: + conn = Connection(user=conn_username, passwd=conn_password, ashost=host, sysnr=sysnr, client=client) + except Exception as err: + result['error'] = str(err) + result['msg'] = 'Something went wrong connecting to the SAP system.' + module.fail_json(**result) + + # build parameter dict of dict + company_params = build_company_params(name, name_2, country, time_zone, city, post_code, street, street_no, e_mail) + + if state == "absent": + raw = call_rfc_method(conn, 'BAPI_COMPANY_DELETE', {'COMPANY': company_id}) + + if state == "present": + raw = call_rfc_method(conn, 'BAPI_COMPANY_CLONE', + {'METHOD': {'USMETHOD': 'COMPANY_CLONE'}, 'COMPANY': company_id, 'COMP_DATA': company_params}) + + analysed = return_analysis(raw) + + result['out'] = raw + + result['changed'] = analysed[0]['change'] + result['msg'] = analysed[2]['msg'] + + if analysed[1]['failed']: + module.fail_json(**result) + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/sap/plugins/modules/sap_snote.py b/ansible_collections/community/sap/plugins/modules/sap_snote.py new file mode 100644 index 000000000..24f393927 --- /dev/null +++ b/ansible_collections/community/sap/plugins/modules/sap_snote.py @@ -0,0 +1,258 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> <rainer.leber@sva.de> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: sap_snote + +short_description: This module will upload and (de)implements C(SNOTES) in a SAP S4HANA environment. + +version_added: "1.0.0" + +description: + - The C(sap_snote) module depends on C(pyrfc) Python library (version 2.4.0 and upwards). + Depending on distribution you are using, you may need to install additional packages to + have these available. + - This module will use the Function Group C(SCWB_API). + - The C(TMS) must be configured at first. + - Integrating SNOTES cannot be done via C(DDIC)- or C(SAP*)-User. +options: + state: + description: + - The decision what to do with the SNOTE. + - Could be C('present'), C('absent') + default: 'present' + choices: + - 'present' + - 'absent' + required: false + type: str + conn_username: + description: The required username for the SAP system. + required: true + type: str + conn_password: + description: The required password for the SAP system. + required: true + type: str + host: + description: The required host for the SAP system. Can be either an FQDN or IP Address. + required: true + type: str + sysnr: + description: + - The system number of the SAP system. + - You must quote the value to ensure retaining the leading zeros. + required: false + default: '01' + type: str + client: + description: + - The client number to connect to. + - You must quote the value to ensure retaining the leading zeros. + required: false + default: '000' + type: str + snote_path: + description: + - The path to the extracted SNOTE txt file. + - The File could be extracted from SAR package. + - If C(snote_path) is not provided, the C(snote) parameter must be defined. + - The SNOTE txt file must be at a place where the SAP System is authorized for. For example C(/usr/sap/trans/files). + required: false + type: str + snote: + description: + - With the C(snote) paramter only implementation and deimplementation will work. + - Upload SNOTES to the System is only available if C(snote_path) is provided. + required: false + type: str + +requirements: + - pyrfc >= 2.4.0 + +author: + - Rainer Leber (@rainerleber) +''' + +EXAMPLES = r''' +- name: test snote module + hosts: localhost + tasks: + - name: implement SNOTE + community.sap.sap_snote: + conn_username: 'DDIC' + conn_password: 'Passwd1234' + host: 192.168.1.100 + sysnr: '01' + client: '000' + state: present + snote_path: /usr/sap/trans/tmp/0002949148.txt + +- name: test snote module without path + hosts: localhost + tasks: + - name: deimplement SNOTE + community.sap.sap_snote: + conn_username: 'DDIC' + conn_password: 'Passwd1234' + host: 192.168.1.100 + sysnr: '01' + client: '000' + state: absent + snote: 0002949148 + +''' + +RETURN = r''' +msg: + description: A small execution description. + type: str + returned: always + sample: 'SNOTE 000298026 implemented.' +out: + description: A complete description of the SNOTE implementation. If this is available. + type: list + elements: dict + returned: always + sample: '{ + "RETURN": [{"ES_MSG": { "MSGNO": "000", "MSGTY": "", "MSGTXT": "", "MSGV1": "" }, + "ET_MSG": [], + "EV_RC": 0, + "ET_MISSING_NOTES": [], + "IT_FILENAME": [{"FILENAME": "/usr/sap/trans/tmp/0002980265.txt"}], + "IT_NOTES": [{"NUMM": "0002980265", "VERSNO": "0000"}] + }]}' +''' + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from os import path as os_path +import traceback +try: + from pyrfc import Connection +except ImportError: + HAS_PYRFC_LIBRARY = False + ANOTHER_LIBRARY_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYRFC_LIBRARY = True + + +def call_rfc_method(connection, method_name, kwargs): + # PyRFC call function + return connection.call(method_name, **kwargs) + + +def check_implementation(conn, snote): + check_implemented = call_rfc_method(conn, 'SCWB_API_GET_NOTES_IMPLEMENTED', {}) + for snote_list in check_implemented['ET_NOTES_IMPL']: + if snote in snote_list['NUMM']: + return True + return False + + +def run_module(): + module = AnsibleModule( + argument_spec=dict( + state=dict(default='present', choices=['absent', 'present']), + conn_username=dict(type='str', required=True), + conn_password=dict(type='str', required=True, no_log=True), + host=dict(type='str', required=True), + sysnr=dict(type='str', default="01"), + client=dict(type='str', default="000"), + snote_path=dict(type='str', required=False), + snote=dict(type='str', required=False), + ), + required_one_of=[('snote_path', 'snote')], + supports_check_mode=False, + ) + result = dict(changed=False, msg='', out={}, error='') + raw = "" + post_check = False + + params = module.params + + state = params['state'] + conn_username = (params['conn_username']).upper() + conn_password = params['conn_password'] + host = params['host'] + sysnr = (params['sysnr']).zfill(2) + client = params['client'] + + path = params['snote_path'] + snote = params['snote'] + + if not HAS_PYRFC_LIBRARY: + module.fail_json( + msg=missing_required_lib('pyrfc'), + exception=ANOTHER_LIBRARY_IMPORT_ERROR) + + if conn_username == "DDIC" or conn_username == "SAP*": + result['msg'] = 'User C(DDIC) or C(SAP*) not allowed for this operation.' + module.fail_json(**result) + + # basic RFC connection with pyrfc + try: + conn = Connection(user=conn_username, passwd=conn_password, ashost=host, sysnr=sysnr, client=client) + except Exception as err: + result['error'] = str(err) + result['msg'] = 'Something went wrong connecting to the SAP system.' + module.fail_json(**result) + + # pre evaluation of parameters + if path is not None: + if path.endswith('.txt'): + # splits snote number from path and txt extension + snote = os_path.basename(os_path.normpath(path)).split('.')[0] + else: + result['msg'] = 'The path must include the extracted snote file and ends with txt.' + module.fail_json(**result) + + pre_check = check_implementation(conn, snote) + + if state == "absent" and pre_check: + raw = call_rfc_method(conn, 'SCWB_API_NOTES_DEIMPLEMENT', {'IT_NOTES': [snote]}) + + if state == "present" and not pre_check: + if path: + raw_upload = call_rfc_method(conn, 'SCWB_API_UPLOAD_NOTES', {'IT_FILENAME': [path], 'IT_NOTES': [snote]}) + if raw_upload['EV_RC'] != 0: + result['out'] = raw_upload + result['msg'] = raw_upload['ES_MSG']['MSGTXT'] + module.fail_json(**result) + + raw = call_rfc_method(conn, 'SCWB_API_NOTES_IMPLEMENT', {'IT_NOTES': [snote]}) + queued = call_rfc_method(conn, 'SCWB_API_CINST_QUEUE_GET', {}) + + if queued['ET_MANUAL_ACTIVITIES']: + raw = call_rfc_method(conn, 'SCWB_API_CONFIRM_MAN_ACTIVITY', {}) + + if raw: + if raw['EV_RC'] == 0: + post_check = check_implementation(conn, snote) + if post_check and state == "present": + result['changed'] = True + result['msg'] = 'SNOTE "{0}" implemented.'.format(snote) + if not post_check and state == "absent": + result['changed'] = True + result['msg'] = 'SNOTE "{0}" deimplemented.'.format(snote) + else: + result['msg'] = "Something went wrong." + module.fail_json(**result) + result['out'] = raw + else: + result['msg'] = "Nothing to do." + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/sap/plugins/modules/sap_system_facts.py b/ansible_collections/community/sap/plugins/modules/sap_system_facts.py new file mode 100644 index 000000000..b5f4eb9b6 --- /dev/null +++ b/ansible_collections/community/sap/plugins/modules/sap_system_facts.py @@ -0,0 +1,206 @@ +#!/usr/bin/python + +# Copyright: (c) 2022, Rainer Leber rainerleber@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: sap_system_facts + +short_description: Gathers SAP facts in a host + +version_added: "1.0.0" + +description: + - This facts module gathers SAP system facts about the running instance. + +author: + - Rainer Leber (@rainerleber) + +notes: + - Supports C(check_mode). +''' + +EXAMPLES = r''' +- name: Return SAP system ansible_facts + community.sap.sap_system_fact: +''' + +RETURN = r''' +# These are examples of possible return values, +# and in general should use other names for return values. +ansible_facts: + description: Facts to add to ansible_facts. + returned: always + type: list + elements: dict + contains: + sap: + description: Facts about the running SAP system. + type: list + elements: dict + returned: When SAP system fact is present + sample: [ + { + "InstanceType": "NW", + "NR": "00", + "SID": "ABC", + "TYPE": "ASCS" + }, + { + "InstanceType": "NW", + "NR": "01", + "SID": "ABC", + "TYPE": "PAS" + }, + { + "InstanceType": "HANA", + "NR": "02", + "SID": "HDB", + "TYPE": "HDB" + }, + { + "InstanceType": "NW", + "NR": "80", + "SID": "WEB", + "TYPE": "WebDisp" + } + ] +''' + +from ansible.module_utils.basic import AnsibleModule +import os +import re + + +def get_all_hana_sid(): + hana_sid = list() + if os.path.isdir("/hana/shared"): + # /hana/shared directory exists + for sid in os.listdir('/hana/shared'): + if os.path.isdir("/usr/sap/" + sid): + hana_sid = hana_sid + [sid] + if hana_sid: + return hana_sid + + +def get_all_nw_sid(): + nw_sid = list() + if os.path.isdir("/sapmnt"): + # /sapmnt directory exists + for sid in os.listdir('/sapmnt'): + if os.path.isdir("/usr/sap/" + sid): + nw_sid = nw_sid + [sid] + else: + # Check to see if /sapmnt/SID/sap_bobj exists + if os.path.isdir("/sapmnt/" + sid + "/sap_bobj"): + # is a bobj system + nw_sid = nw_sid + [sid] + if nw_sid: + return nw_sid + + +def get_hana_nr(sids, module): + hana_list = list() + for sid in sids: + for instance in os.listdir('/usr/sap/' + sid): + if 'HDB' in instance: + instance_nr = instance[-2:] + # check if instance number exists + command = [module.get_bin_path('/usr/sap/hostctrl/exe/sapcontrol', required=True)] + command.extend(['-nr', instance_nr, '-function', 'GetProcessList']) + check_instance = module.run_command(command, check_rc=False) + # sapcontrol returns c(0 - 5) exit codes only c(1) is unavailable + if check_instance[0] != 1: + hana_list.append({'NR': instance_nr, 'SID': sid, 'TYPE': 'HDB', 'InstanceType': 'HANA'}) + return hana_list + + +def get_nw_nr(sids, module): + nw_list = list() + type = "" + for sid in sids: + for instance in os.listdir('/usr/sap/' + sid): + instance_nr = instance[-2:] + command = [module.get_bin_path('/usr/sap/hostctrl/exe/sapcontrol', required=True)] + # check if returned instance_nr is a number because sapcontrol returns all if a random string is provided + if instance_nr.isdigit(): + command.extend(['-nr', instance_nr, '-function', 'GetInstanceProperties']) + check_instance = module.run_command(command, check_rc=False) + if check_instance[0] != 1: + for line in check_instance[1].splitlines(): + if re.search('INSTANCE_NAME', line): + # convert to list and extract last + type_raw = (line.strip('][').split(', '))[-1] + # split instance number + type = type_raw[:-2] + nw_list.append({'NR': instance_nr, 'SID': sid, 'TYPE': get_instance_type(type), 'InstanceType': 'NW'}) + return nw_list + + +def get_instance_type(raw_type): + if raw_type[0] == "D": + # It's a PAS + type = "PAS" + elif raw_type[0] == "A": + # It's an ASCS + type = "ASCS" + elif raw_type[0] == "W": + # It's a Webdisp + type = "WebDisp" + elif raw_type[0] == "J": + # It's a Java + type = "Java" + elif raw_type[0] == "S": + # It's an SCS + type = "SCS" + elif raw_type[0] == "E": + # It's an ERS + type = "ERS" + else: + # Unknown instance type + type = "XXX" + return type + + +def run_module(): + module_args = dict() + system_result = list() + + result = dict( + changed=False, + ansible_facts=dict(), + ) + + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True, + ) + + hana_sid = get_all_hana_sid() + if hana_sid: + system_result = system_result + get_hana_nr(hana_sid, module) + + nw_sid = get_all_nw_sid() + if nw_sid: + system_result = system_result + get_nw_nr(nw_sid, module) + + if system_result: + result['ansible_facts'] = {'sap': system_result} + else: + result['ansible_facts'] + + if module.check_mode: + module.exit_json(**result) + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/sap/plugins/modules/sap_task_list_execute.py b/ansible_collections/community/sap/plugins/modules/sap_task_list_execute.py new file mode 100644 index 000000000..0ae25903f --- /dev/null +++ b/ansible_collections/community/sap/plugins/modules/sap_task_list_execute.py @@ -0,0 +1,340 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: sap_task_list_execute +short_description: Perform SAP Task list execution +version_added: "0.1.0" +description: + - The M(community.sap.sap_task_list_execute) module depends on C(pyrfc) Python library (version 2.4.0 and upwards). + Depending on distribution you are using, you may need to install additional packages to + have these available. + - Tasks in the task list which requires manual activities will be confirmed automatically. + - This module will use the RFC package C(STC_TM_API). + +requirements: + - pyrfc >= 2.4.0 + - xmltodict + +options: + conn_username: + description: The required username for the SAP system. + required: true + type: str + conn_password: + description: The required password for the SAP system. + required: true + type: str + host: + description: The required host for the SAP system. Can be either an FQDN or IP Address. + required: true + type: str + sysnr: + description: + - The system number of the SAP system. + - You must quote the value to ensure retaining the leading zeros. + default: '00' + type: str + client: + description: + - The client number to connect to. + - You must quote the value to ensure retaining the leading zeros. + default: '000' + type: str + task_to_execute: + description: The task list which will be executed. + required: true + type: str + task_parameters: + description: + - The tasks and the parameters for execution. + - If the task list does not need any parameters, this could be empty. + - If only specific tasks from the task list should be executed, + the tasks even when no parameter is needed must be provided + alongside with the module parameter I(task_skip=true). + type: list + elements: dict + suboptions: + TASKNAME: + description: The name of the task in the task list. + type: str + required: true + FIELDNAME: + description: The name of the field of the task. + type: str + VALUE: + description: The value which have to be set. + type: raw + task_settings: + description: + - Setting for the execution of the task list. This can be the following as in TCODE SE80 described. + Check Mode C(CHECKRUN), Background Processing Active C(BATCH) (this is the default value), + Asynchronous Execution C(ASYNC), Trace Mode C(TRACE), Server Name C(BATCH_TARGET). + default: ['BATCH'] + type: list + elements: str + task_skip: + description: + - If this parameter is C(true), not defined tasks in I(task_parameters) are skipped. + - This could be the case when only certain tasks should run from the task list. + default: false + type: bool + +notes: + - Does not support C(check_mode). Always returns that the state has changed. +author: + - Rainer Leber (@rainerleber) +''' + +EXAMPLES = r''' +# Pass in a message +- name: Test task execution + community.sap.sap_task_list_execute: + conn_username: DDIC + conn_password: Passwd1234 + host: 10.1.8.10 + sysnr: '01' + client: '000' + task_to_execute: SAP_BASIS_SSL_CHECK + task_settings: batch + +- name: Pass in input parameters + community.sap.sap_task_list_execute: + conn_username: DDIC + conn_password: Passwd1234 + host: 10.1.8.10 + sysnr: '00' + client: '000' + task_to_execute: SAP_BASIS_SSL_CHECK + task_parameters : + - { 'TASKNAME': 'CL_STCT_CHECK_SEC_CRYPTO', 'FIELDNAME': 'P_OPT2', 'VALUE': 'X' } + - TASKNAME: CL_STCT_CHECK_SEC_CRYPTO + FIELDNAME: P_OPT3 + VALUE: X + task_settings: batch + +# Exported environment variables +- name: Hint if module will fail with error message like ImportError libsapnwrfc.so... + community.sap.sap_task_list_execute: + conn_username: DDIC + conn_password: Passwd1234 + host: 10.1.8.10 + sysnr: '00' + client: '000' + task_to_execute: SAP_BASIS_SSL_CHECK + task_settings: batch + environment: + SAPNWRFC_HOME: /usr/local/sap/nwrfcsdk + LD_LIBRARY_PATH: /usr/local/sap/nwrfcsdk/lib +''' + +RETURN = r''' +msg: + description: A small execution description. + type: str + returned: always + sample: 'Successful' +out: + description: A complete description of the executed tasks. If this is available. + type: list + elements: dict + returned: on success + sample: [...,{ + "LOG": { + "STCTM_S_LOG": [ + { + "ACTIVITY": "U_CONFIG", + "ACTIVITY_DESCR": "Configuration changed", + "DETAILS": null, + "EXEC_ID": "20210728184903.815739", + "FIELD": null, + "ID": "STC_TASK", + "LOG_MSG_NO": "000000", + "LOG_NO": null, + "MESSAGE": "For radiobutton group ICM too many options are set; choose only one option", + "MESSAGE_V1": "ICM", + "MESSAGE_V2": null, + "MESSAGE_V3": null, + "MESSAGE_V4": null, + "NUMBER": "048", + "PARAMETER": null, + "PERIOD": "M", + "PERIOD_DESCR": "Maintenance", + "ROW": "0", + "SRC_LINE": "170", + "SRC_OBJECT": "CL_STCTM_REPORT_UI IF_STCTM_UI_TASK~SET_PARAMETERS", + "SYSTEM": null, + "TIMESTMP": "20210728184903", + "TSTPNM": "DDIC", + "TYPE": "E" + },... + ]}}] +''' + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +import traceback +try: + from pyrfc import Connection +except ImportError: + HAS_PYRFC_LIBRARY = False + PYRFC_LIBRARY_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYRFC_LIBRARY = True +try: + import xmltodict +except ImportError: + HAS_XMLTODICT_LIBRARY = False + XMLTODICT_LIBRARY_IMPORT_ERROR = traceback.format_exc() +else: + HAS_XMLTODICT_LIBRARY = True + + +def call_rfc_method(connection, method_name, kwargs): + # PyRFC call function + return connection.call(method_name, **kwargs) + + +def process_exec_settings(task_settings): + # processes task settings to objects + exec_settings = {} + for settings in task_settings: + temp_dict = {settings.upper(): 'X'} + for key, value in temp_dict.items(): + exec_settings[key] = value + return exec_settings + + +def xml_to_dict(xml_raw): + try: + xml_parsed = xmltodict.parse(xml_raw, dict_constructor=dict) + xml_dict = xml_parsed['asx:abap']['asx:values']['SESSION']['TASKLIST'] + except KeyError: + xml_dict = "No logs available." + return xml_dict + + +def run_module(): + + params_spec = dict( + TASKNAME=dict(type='str', required=True), + FIELDNAME=dict(type='str'), + VALUE=dict(type='raw'), + ) + + # define available arguments/parameters a user can pass to the module + module = AnsibleModule( + argument_spec=dict( + # values for connection + conn_username=dict(type='str', required=True), + conn_password=dict(type='str', required=True, no_log=True), + host=dict(type='str', required=True), + sysnr=dict(type='str', default="00"), + client=dict(type='str', default="000"), + # values for execution tasks + task_to_execute=dict(type='str', required=True), + task_parameters=dict(type='list', elements='dict', options=params_spec), + task_settings=dict(type='list', elements='str', default=['BATCH']), + task_skip=dict(type='bool', default=False), + ), + supports_check_mode=False, + ) + result = dict(changed=False, msg='', out={}) + + params = module.params + + username = params['conn_username'].upper() + password = params['conn_password'] + host = params['host'] + sysnr = params['sysnr'] + client = params['client'] + + task_parameters = params['task_parameters'] + task_to_execute = params['task_to_execute'] + task_settings = params['task_settings'] + task_skip = params['task_skip'] + + if not HAS_PYRFC_LIBRARY: + module.fail_json( + msg=missing_required_lib('pyrfc'), + exception=PYRFC_LIBRARY_IMPORT_ERROR) + + if not HAS_XMLTODICT_LIBRARY: + module.fail_json( + msg=missing_required_lib('xmltodict'), + exception=XMLTODICT_LIBRARY_IMPORT_ERROR) + + # basic RFC connection with pyrfc + try: + conn = Connection(user=username, passwd=password, ashost=host, sysnr=sysnr, client=client) + except Exception as err: + result['error'] = str(err) + result['msg'] = 'Something went wrong connecting to the SAP system.' + module.fail_json(**result) + + try: + raw_params = call_rfc_method(conn, 'STC_TM_SCENARIO_GET_PARAMETERS', + {'I_SCENARIO_ID': task_to_execute}) + except Exception as err: + result['error'] = str(err) + result['msg'] = 'The task list does not exist.' + module.fail_json(**result) + exec_settings = process_exec_settings(task_settings) + # initialize session task + session_init = call_rfc_method(conn, 'STC_TM_SESSION_BEGIN', + {'I_SCENARIO_ID': task_to_execute, + 'I_INIT_ONLY': 'X'}) + # Confirm Tasks which requires manual activities from Task List Run + for task in raw_params['ET_PARAMETER']: + call_rfc_method(conn, 'STC_TM_TASK_CONFIRM', + {'I_SESSION_ID': session_init['E_SESSION_ID'], + 'I_TASKNAME': task['TASKNAME']}) + if task_skip: + for task in raw_params['ET_PARAMETER']: + call_rfc_method(conn, 'STC_TM_TASK_SKIP', + {'I_SESSION_ID': session_init['E_SESSION_ID'], + 'I_TASKNAME': task['TASKNAME'], 'I_SKIP_DEP_TASKS': 'X'}) + # unskip defined tasks and set parameters + if task_parameters is not None: + for task in task_parameters: + call_rfc_method(conn, 'STC_TM_TASK_UNSKIP', + {'I_SESSION_ID': session_init['E_SESSION_ID'], + 'I_TASKNAME': task['TASKNAME'], 'I_UNSKIP_DEP_TASKS': 'X'}) + + call_rfc_method(conn, 'STC_TM_SESSION_SET_PARAMETERS', + {'I_SESSION_ID': session_init['E_SESSION_ID'], + 'IT_PARAMETER': task_parameters}) + # start the task + try: + session_start = call_rfc_method(conn, 'STC_TM_SESSION_RESUME', + {'I_SESSION_ID': session_init['E_SESSION_ID'], + 'IS_EXEC_SETTINGS': exec_settings}) + except Exception as err: + result['error'] = str(err) + result['msg'] = 'Something went wrong. See error.' + module.fail_json(**result) + # get task logs because the execution may successfully but the tasks shows errors or warnings + # returned value is ABAPXML https://help.sap.com/doc/abapdocu_755_index_htm/7.55/en-US/abenabap_xslt_asxml_general.htm + session_log = call_rfc_method(conn, 'STC_TM_SESSION_GET_LOG', + {'I_SESSION_ID': session_init['E_SESSION_ID']}) + + task_list = xml_to_dict(session_log['E_LOG']) + + result['changed'] = True + result['msg'] = session_start['E_STATUS_DESCR'] + result['out'] = task_list + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/sap/plugins/modules/sap_user.py b/ansible_collections/community/sap/plugins/modules/sap_user.py new file mode 100644 index 000000000..f83472657 --- /dev/null +++ b/ansible_collections/community/sap/plugins/modules/sap_user.py @@ -0,0 +1,499 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> <rainer.leber@sva.de> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: sap_user +short_description: This module will manage a user entities in a SAP S4/HANA environment +version_added: "1.0.0" +description: + - The M(community.sap.sap_user) module depends on C(pyrfc) Python library (version 2.4.0 and upwards). + Depending on distribution you are using, you may need to install additional packages to + have these available. + - This module will use the following user BAPIs to manage user entities. + - C(BAPI_USER_GET_DETAIL) + - C(BAPI_USER_DELETE) + - C(BAPI_USER_CREATE1) + - C(BAPI_USER_CHANGE) + - C(BAPI_USER_ACTGROUPS_ASSIGN) + - C(BAPI_USER_PROFILES_ASSIGN) + - C(BAPI_USER_UNLOCK) + - C(BAPI_USER_LOCK) +options: + state: + description: + - The decision what to do with the user. + default: 'present' + choices: + - 'present' + - 'absent' + - 'lock' + - 'unlock' + required: false + type: str + force: + description: + - Must be C('True') if the password or type should be overwritten. + default: False + required: false + type: bool + conn_username: + description: The required username for the SAP system. + required: true + type: str + conn_password: + description: The required password for the SAP system. + required: true + type: str + host: + description: The required host for the SAP system. Can be either an FQDN or IP Address. + required: true + type: str + sysnr: + description: + - The system number of the SAP system. + - You must quote the value to ensure retaining the leading zeros. + default: '00' + type: str + client: + description: + - The client number to connect to. + - You must quote the value to ensure retaining the leading zeros. + default: '000' + type: str + username: + description: + - The username. + type: str + required: true + firstname: + description: + - The Firstname of the user in the SAP system. + type: str + required: false + lastname: + description: + - The lastname of the user in the SAP system. + type: str + required: false + email: + description: + - The email address of the user in the SAP system. + type: str + required: false + password: + description: + - The password for the user in the SAP system. + type: str + required: false + useralias: + description: + - The alias for the user in the SAP system. + type: str + required: false + user_type: + description: + - The type for the user in the SAP system. + - C('A') Dialog user, C('B') System User, C('C') Communication User, + C('S') Service User, C('L') Reference User. + - Must be in uppercase. + type: str + required: false + default: 'A' + choices: ['A', 'B', 'C', 'S', 'L'] + company: + description: + - The specific company the user belongs to. + - The company name must be available in the SAP system. + type: str + required: false + profiles: + description: + - Assign profiles to the user. + - Should be in uppercase, for example C('SAP_NEW') or C('SAP_ALL'). + type: list + elements: str + default: [''] + required: false + roles: + description: + - Assign roles to the user. + type: list + elements: str + default: [''] + required: false + +requirements: + - pyrfc >= 2.4.0 +author: + - Rainer Leber (@rainerleber) +notes: + - Does not support C(check_mode). +''' + +EXAMPLES = r''' +- name: Create SAP User + community.sap.sap_user: + conn_username: 'DDIC' + conn_password: 'Test123' + host: 192.168.1.150 + sysnr: '01' + client: '000' + state: present + username: ADMIN + firstname: first_admin + lastname: last_admin + email: admin@test.de + password: Test123456 + useralias: ADMIN + company: DEFAULT_COMPANY + roles: + - "SAP_ALL" + +- name: Force change SAP User + community.sap.sap_user: + conn_username: 'DDIC' + conn_password: 'Test123' + host: 192.168.1.150 + sysnr: '01' + client: '000' + state: present + force: true + username: ADMIN + firstname: first_admin + lastname: last_admin + email: admin@test.de + password: Test123456 + useralias: ADMIN + company: DEFAULT_COMPANY + roles: + - "SAP_ALL" + +- name: Delete SAP User + community.sap.sap_user: + conn_username: 'DDIC' + conn_password: 'Test123' + host: 192.168.1.150 + sysnr: '01' + client: '000' + state: absent + force: true + username: ADMIN + +- name: Unlock SAP User + community.sap.sap_user: + conn_username: 'DDIC' + conn_password: 'Test123' + host: 192.168.1.150 + sysnr: '01' + client: '000' + state: unlock + force: true + username: ADMIN +''' + +RETURN = r''' +msg: + description: A small execution description about the user action. + type: str + returned: always + sample: 'User ADMIN created' +out: + description: A detailed description about the user action. + type: list + elements: dict + returned: on success + sample: [...,{ + "RETURN": [ + { + "FIELD": "BNAME", + "ID": "01", + "LOG_MSG_NO": "000000", + "LOG_NO": "", + "MESSAGE": "User ADMIN created", + "MESSAGE_V1": "ADMIN", + "MESSAGE_V2": "", + "MESSAGE_V3": "", + "MESSAGE_V4": "", + "NUMBER": "102", + "PARAMETER": "", + "ROW": 0, + "SYSTEM": "", + "TYPE": "S" + } + ], + "SAPUSER_UUID_HIST": []}] +''' +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +import traceback +import datetime +try: + from pyrfc import Connection +except ImportError: + HAS_PYRFC_LIBRARY = False + PYRFC_LIBRARY_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYRFC_LIBRARY = True + + +def add_to_dict(target_dict, target_key, value): + # Adds the given value to a dict as the key + # check if the given key is in the given dict yet + if target_key in target_dict: + return False + target_dict[target_key] = value + return True + + +def call_rfc_method(connection, method_name, kwargs): + # PyRFC call function + return connection.call(method_name, **kwargs) + + +def build_rfc_user_params(username, firstname, lastname, email, raw_password, + useralias, user_type, raw_company, user_change, force): + """Creates RFC parameters for Creating users""" + # define dicts in batch + params = dict() + address = dict() + password = dict() + alias = dict() + logondata = dict() + company = dict() + # for change parameters + addressx = dict() + passwordx = dict() + logondatax = dict() + companyx = dict() + # define username + add_to_dict(params, 'USERNAME', username) + # define Address + add_to_dict(address, 'FIRSTNAME', firstname) + add_to_dict(address, 'LASTNAME', lastname) + add_to_dict(address, 'E_MAIL', email) + # define Password + add_to_dict(password, 'BAPIPWD', raw_password) + # define Alias + add_to_dict(alias, 'USERALIAS', useralias) + # define LogonData + add_to_dict(logondata, 'GLTGV', datetime.date.today()) + add_to_dict(logondata, 'GLTGB', '20991231') + add_to_dict(logondata, 'USTYP', user_type) + # define company + add_to_dict(company, 'COMPANY', raw_company) + params['LOGONDATA'] = logondata + params['ADDRESS'] = address + params['COMPANY'] = company + params['ALIAS'] = alias + params['PASSWORD'] = password + # add change if user exists + if user_change and force: + add_to_dict(addressx, 'FIRSTNAME', 'X') + add_to_dict(addressx, 'LASTNAME', 'X') + add_to_dict(addressx, 'E_MAIL', 'X') + # define Password + add_to_dict(passwordx, 'BAPIPWD', 'X') + # define LogonData + add_to_dict(logondatax, 'USTYP', 'X') + # define company + add_to_dict(companyx, 'COMPANY', 'X') + params['LOGONDATAX'] = logondatax + params['ADDRESSX'] = addressx + params['COMPANYX'] = companyx + params['PASSWORDX'] = passwordx + return params + + +def user_role_assignment_build_rfc_params(roles, username): + rfc_table = [] + + for role_name in roles: + table_row = {'AGR_NAME': role_name} + + add_to_dict(table_row, 'FROM_DAT', datetime.date.today()) + add_to_dict(table_row, 'TO_DAT', '20991231') + + rfc_table.append(table_row) + + return { + 'USERNAME': username, + 'ACTIVITYGROUPS': rfc_table + } + + +def user_profile_assignment_build_rfc_params(profiles, username): + rfc_table = [] + + for profile_name in profiles: + table_row = {'BAPIPROF': profile_name} + rfc_table.append(table_row) + + return { + 'USERNAME': username, + 'PROFILES': rfc_table + } + + +def check_user(user_detail): + if len(user_detail['RETURN']) > 0: + for sub in user_detail['RETURN']: + if sub['NUMBER'] == '124': + return False + return True + + +def return_analysis(raw): + change = False + failed = False + for state in raw['RETURN']: + if state['TYPE'] == "E": + if state['NUMBER'] == '224' or state['NUMBER'] == '124': + change = False + else: + failed = True + if state['TYPE'] == "S": + if state['NUMBER'] != '029': + change = True + if state['TYPE'] == "W": + if state['NUMBER'] == '049' or state['NUMBER'] == '047': + change = True + if state['NUMBER'] == '255': + change = True + return [{"change": change}, {"failed": failed}] + + +def run_module(): + module = AnsibleModule( + argument_spec=dict( + # logical values + state=dict(default='present', choices=[ + 'absent', 'present', 'lock', 'unlock']), + force=dict(type='bool', default=False), + # values for connection + conn_username=dict(type='str', required=True), + conn_password=dict(type='str', required=True, no_log=True), + host=dict(type='str', required=True), + sysnr=dict(type='str', default="00"), + client=dict(type='str', default="000"), + # values for the new or existing user + username=dict(type='str', required=True), + firstname=dict(type='str', required=False), + lastname=dict(type='str', required=False), + email=dict(type='str', required=False), + password=dict(type='str', required=False, no_log=True), + useralias=dict(type='str', required=False), + user_type=dict(default="A", + choices=['A', 'B', 'C', 'S', 'L']), + company=dict(type='str', required=False), + # values for profile must a list + # Example ["SAP_NEW", "SAP_ALL"] + profiles=dict(type='list', elements='str', default=[""]), + # values for roles must a list + roles=dict(type='list', elements='str', default=[""]), + ), + supports_check_mode=False, + required_if=[('state', 'present', ['useralias', 'company'])] + ) + result = dict(changed=False, msg='', out='') + count = 0 + raw = "" + + params = module.params + + state = params['state'] + conn_username = (params['conn_username']).upper() + conn_password = params['conn_password'] + host = params['host'] + sysnr = params['sysnr'] + client = params['client'] + + username = (params['username']).upper() + firstname = params['firstname'] + lastname = params['lastname'] + email = params['email'] + password = params['password'] + force = params['force'] + if not params['useralias'] is None: + useralias = (params['useralias']).upper() + user_type = (params['user_type']).upper() + company = params['company'] + + profiles = params['profiles'] + roles = params['roles'] + + if not HAS_PYRFC_LIBRARY: + module.fail_json( + msg=missing_required_lib('pyrfc'), + exception=PYRFC_LIBRARY_IMPORT_ERROR) + + # basic RFC connection with pyrfc + try: + conn = Connection(user=conn_username, passwd=conn_password, ashost=host, sysnr=sysnr, client=client) + except Exception as err: + result['error'] = str(err) + result['msg'] = 'Something went wrong connecting to the SAP system.' + module.fail_json(**result) + + # user details + user_detail = call_rfc_method(conn, 'BAPI_USER_GET_DETAIL', {'USERNAME': username}) + user_exists = check_user(user_detail) + + if state == "absent": + if user_exists: + raw = call_rfc_method(conn, 'BAPI_USER_DELETE', {'USERNAME': username}) + + if state == "present": + user_params = build_rfc_user_params(username, firstname, lastname, email, password, useralias, user_type, company, user_exists, force) + if not user_exists: + raw = call_rfc_method(conn, 'BAPI_USER_CREATE1', user_params) + + if user_exists: + # check for address changes when user exists + user_no_changes = all((user_detail.get('ADDRESS')).get(k) == v for k, v in (user_params.get('ADDRESS')).items()) + if not user_no_changes or force: + raw = call_rfc_method(conn, 'BAPI_USER_CHANGE', user_params) + + call_rfc_method(conn, 'BAPI_USER_ACTGROUPS_ASSIGN', user_role_assignment_build_rfc_params(roles, username)) + + call_rfc_method(conn, 'BAPI_USER_PROFILES_ASSIGN', user_profile_assignment_build_rfc_params(profiles, username)) + + if state == "unlock": + if user_exists: + raw = call_rfc_method(conn, 'BAPI_USER_UNLOCK', {'USERNAME': username}) + + if state == "lock": + if user_exists: + raw = call_rfc_method(conn, 'BAPI_USER_LOCK', {'USERNAME': username}) + + # analyse return value + if raw != '': + analysed = return_analysis(raw) + + result['out'] = raw + + result['changed'] = analysed[0]['change'] + for msgs in raw['RETURN']: + if count > 0: + result['msg'] = result['msg'] + '\n' + result['msg'] = result['msg'] + msgs['MESSAGE'] + count = count + 1 + + if analysed[1]['failed']: + module.fail_json(**result) + else: + result['msg'] = "No changes where made." + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/sap/plugins/modules/sapcar_extract.py b/ansible_collections/community/sap/plugins/modules/sapcar_extract.py new file mode 100644 index 000000000..d586dd330 --- /dev/null +++ b/ansible_collections/community/sap/plugins/modules/sapcar_extract.py @@ -0,0 +1,220 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: sapcar_extract +short_description: Manages SAP SAPCAR archives +version_added: "0.1.0" +description: + - Provides support for unpacking C(sar)/C(car) files with the SAPCAR binary from SAP and pulling + information back into Ansible. +options: + path: + description: The path to the SAR/CAR file. + type: path + required: true + dest: + description: + - The destination where SAPCAR extracts the SAR file. Missing folders will be created. + If this parameter is not provided, it will unpack in the same folder as the SAR file. + type: path + binary_path: + description: + - The path to the SAPCAR binary, for example, C(/home/dummy/sapcar) or C(https://myserver/SAPCAR). + If this parameter is not provided, the module will look in C(PATH). + type: path + signature: + description: + - If C(true), the signature will be extracted. + default: false + type: bool + security_library: + description: + - The path to the security library, for example, C(/usr/sap/hostctrl/exe/libsapcrytp.so), for signature operations. + type: path + manifest: + description: + - The name of the manifest. + default: "SIGNATURE.SMF" + type: str + remove: + description: + - If C(true), the SAR/CAR file will be removed. B(This should be used with caution!) + default: false + type: bool +author: + - Rainer Leber (@RainerLeber) +notes: + - Always returns C(changed=true) in C(check_mode). +''' + +EXAMPLES = r""" +- name: Extract SAR file + community.sap.sapcar_extract: + path: "~/source/hana.sar" + +- name: Extract SAR file with destination + community.sap.sapcar_extract: + path: "~/source/hana.sar" + dest: "~/test/" + +- name: Extract SAR file with destination and download from webserver can be a fileshare as well + community.sap.sapcar_extract: + path: "~/source/hana.sar" + dest: "~/dest/" + binary_path: "https://myserver/SAPCAR" + +- name: Extract SAR file and delete SAR after extract + community.sap.sapcar_extract: + path: "~/source/hana.sar" + remove: true + +- name: Extract SAR file with manifest + community.sap.sapcar_extract: + path: "~/source/hana.sar" + signature: true + +- name: Extract SAR file with manifest and rename it + community.sap.sapcar_extract: + path: "~/source/hana.sar" + manifest: "MyNewSignature.SMF" + signature: true +""" + +import os +from tempfile import NamedTemporaryFile +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.urls import open_url +from ansible.module_utils.common.text.converters import to_native + + +def get_list_of_files(dir_name): + # create a list of file and directories + # names in the given directory + list_of_file = os.listdir(dir_name) + allFiles = list() + # Iterate over all the entries + for entry in list_of_file: + # Create full path + fullPath = os.path.join(dir_name, entry) + # If entry is a directory then get the list of files in this directory + if os.path.isdir(fullPath): + allFiles = allFiles + [fullPath] + allFiles = allFiles + get_list_of_files(fullPath) + else: + allFiles.append(fullPath) + return allFiles + + +def download_SAPCAR(binary_path, module): + bin_path = None + # download sapcar binary if url is provided otherwise path is returned + if binary_path is not None: + if binary_path.startswith('https://') or binary_path.startswith('http://'): + random_file = NamedTemporaryFile(delete=False) + with open_url(binary_path) as response: + with random_file as out_file: + data = response.read() + out_file.write(data) + os.chmod(out_file.name, 0o700) + bin_path = out_file.name + module.add_cleanup_file(bin_path) + else: + bin_path = binary_path + return bin_path + + +def check_if_present(command, path, dest, signature, manifest, module): + # manipulating output from SAR file for compare with already extracted files + iter_command = [command, '-tvf', path] + sar_out = module.run_command(iter_command)[1] + sar_raw = sar_out.split("\n")[1:] + if dest[-1] != "/": + dest = dest + "/" + sar_files = [dest + x.split(" ")[-1] for x in sar_raw if x] + # remove any SIGNATURE.SMF from list because it will not unpacked if signature is false + if not signature: + sar_files = [item for item in sar_files if not item.endswith('.SMF')] + # if signature is renamed manipulate files in list of sar file for compare. + if manifest != "SIGNATURE.SMF": + sar_files = [item for item in sar_files if not item.endswith('.SMF')] + sar_files = sar_files + [manifest] + # get extracted files if present + files_extracted = get_list_of_files(dest) + # compare extracted files with files in sar file + present = all(elem in files_extracted for elem in sar_files) + return present + + +def main(): + module = AnsibleModule( + argument_spec=dict( + path=dict(type='path', required=True), + dest=dict(type='path'), + binary_path=dict(type='path'), + signature=dict(type='bool', default=False), + security_library=dict(type='path'), + manifest=dict(type='str', default="SIGNATURE.SMF"), + remove=dict(type='bool', default=False), + ), + supports_check_mode=True, + ) + rc, out, err = [0, "", ""] + params = module.params + check_mode = module.check_mode + + path = params['path'] + dest = params['dest'] + signature = params['signature'] + security_library = params['security_library'] + manifest = params['manifest'] + remove = params['remove'] + + bin_path = download_SAPCAR(params['binary_path'], module) + + if dest is None: + dest_head_tail = os.path.split(path) + dest = dest_head_tail[0] + '/' + else: + if not os.path.exists(dest): + os.makedirs(dest, 0o755) + + if bin_path is not None: + command = [module.get_bin_path(bin_path, required=True)] + else: + try: + command = [module.get_bin_path('sapcar', required=True)] + except Exception as e: + module.fail_json(msg='Failed to find SAPCAR at the expected path or URL "{0}". Please check whether it is available: {1}' + .format(bin_path, to_native(e))) + + present = check_if_present(command[0], path, dest, signature, manifest, module) + + if not present: + command.extend(['-xvf', path, '-R', dest]) + if security_library: + command.extend(['-L', security_library]) + if signature: + command.extend(['-manifest', manifest]) + if not check_mode: + (rc, out, err) = module.run_command(command, check_rc=True) + changed = True + else: + changed = False + out = "already unpacked" + + if remove: + os.remove(path) + + module.exit_json(changed=changed, message=rc, stdout=out, + stderr=err, command=' '.join(command)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/sap/plugins/modules/system/sap_snote.py b/ansible_collections/community/sap/plugins/modules/system/sap_snote.py new file mode 100644 index 000000000..24f393927 --- /dev/null +++ b/ansible_collections/community/sap/plugins/modules/system/sap_snote.py @@ -0,0 +1,258 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> <rainer.leber@sva.de> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: sap_snote + +short_description: This module will upload and (de)implements C(SNOTES) in a SAP S4HANA environment. + +version_added: "1.0.0" + +description: + - The C(sap_snote) module depends on C(pyrfc) Python library (version 2.4.0 and upwards). + Depending on distribution you are using, you may need to install additional packages to + have these available. + - This module will use the Function Group C(SCWB_API). + - The C(TMS) must be configured at first. + - Integrating SNOTES cannot be done via C(DDIC)- or C(SAP*)-User. +options: + state: + description: + - The decision what to do with the SNOTE. + - Could be C('present'), C('absent') + default: 'present' + choices: + - 'present' + - 'absent' + required: false + type: str + conn_username: + description: The required username for the SAP system. + required: true + type: str + conn_password: + description: The required password for the SAP system. + required: true + type: str + host: + description: The required host for the SAP system. Can be either an FQDN or IP Address. + required: true + type: str + sysnr: + description: + - The system number of the SAP system. + - You must quote the value to ensure retaining the leading zeros. + required: false + default: '01' + type: str + client: + description: + - The client number to connect to. + - You must quote the value to ensure retaining the leading zeros. + required: false + default: '000' + type: str + snote_path: + description: + - The path to the extracted SNOTE txt file. + - The File could be extracted from SAR package. + - If C(snote_path) is not provided, the C(snote) parameter must be defined. + - The SNOTE txt file must be at a place where the SAP System is authorized for. For example C(/usr/sap/trans/files). + required: false + type: str + snote: + description: + - With the C(snote) paramter only implementation and deimplementation will work. + - Upload SNOTES to the System is only available if C(snote_path) is provided. + required: false + type: str + +requirements: + - pyrfc >= 2.4.0 + +author: + - Rainer Leber (@rainerleber) +''' + +EXAMPLES = r''' +- name: test snote module + hosts: localhost + tasks: + - name: implement SNOTE + community.sap.sap_snote: + conn_username: 'DDIC' + conn_password: 'Passwd1234' + host: 192.168.1.100 + sysnr: '01' + client: '000' + state: present + snote_path: /usr/sap/trans/tmp/0002949148.txt + +- name: test snote module without path + hosts: localhost + tasks: + - name: deimplement SNOTE + community.sap.sap_snote: + conn_username: 'DDIC' + conn_password: 'Passwd1234' + host: 192.168.1.100 + sysnr: '01' + client: '000' + state: absent + snote: 0002949148 + +''' + +RETURN = r''' +msg: + description: A small execution description. + type: str + returned: always + sample: 'SNOTE 000298026 implemented.' +out: + description: A complete description of the SNOTE implementation. If this is available. + type: list + elements: dict + returned: always + sample: '{ + "RETURN": [{"ES_MSG": { "MSGNO": "000", "MSGTY": "", "MSGTXT": "", "MSGV1": "" }, + "ET_MSG": [], + "EV_RC": 0, + "ET_MISSING_NOTES": [], + "IT_FILENAME": [{"FILENAME": "/usr/sap/trans/tmp/0002980265.txt"}], + "IT_NOTES": [{"NUMM": "0002980265", "VERSNO": "0000"}] + }]}' +''' + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from os import path as os_path +import traceback +try: + from pyrfc import Connection +except ImportError: + HAS_PYRFC_LIBRARY = False + ANOTHER_LIBRARY_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYRFC_LIBRARY = True + + +def call_rfc_method(connection, method_name, kwargs): + # PyRFC call function + return connection.call(method_name, **kwargs) + + +def check_implementation(conn, snote): + check_implemented = call_rfc_method(conn, 'SCWB_API_GET_NOTES_IMPLEMENTED', {}) + for snote_list in check_implemented['ET_NOTES_IMPL']: + if snote in snote_list['NUMM']: + return True + return False + + +def run_module(): + module = AnsibleModule( + argument_spec=dict( + state=dict(default='present', choices=['absent', 'present']), + conn_username=dict(type='str', required=True), + conn_password=dict(type='str', required=True, no_log=True), + host=dict(type='str', required=True), + sysnr=dict(type='str', default="01"), + client=dict(type='str', default="000"), + snote_path=dict(type='str', required=False), + snote=dict(type='str', required=False), + ), + required_one_of=[('snote_path', 'snote')], + supports_check_mode=False, + ) + result = dict(changed=False, msg='', out={}, error='') + raw = "" + post_check = False + + params = module.params + + state = params['state'] + conn_username = (params['conn_username']).upper() + conn_password = params['conn_password'] + host = params['host'] + sysnr = (params['sysnr']).zfill(2) + client = params['client'] + + path = params['snote_path'] + snote = params['snote'] + + if not HAS_PYRFC_LIBRARY: + module.fail_json( + msg=missing_required_lib('pyrfc'), + exception=ANOTHER_LIBRARY_IMPORT_ERROR) + + if conn_username == "DDIC" or conn_username == "SAP*": + result['msg'] = 'User C(DDIC) or C(SAP*) not allowed for this operation.' + module.fail_json(**result) + + # basic RFC connection with pyrfc + try: + conn = Connection(user=conn_username, passwd=conn_password, ashost=host, sysnr=sysnr, client=client) + except Exception as err: + result['error'] = str(err) + result['msg'] = 'Something went wrong connecting to the SAP system.' + module.fail_json(**result) + + # pre evaluation of parameters + if path is not None: + if path.endswith('.txt'): + # splits snote number from path and txt extension + snote = os_path.basename(os_path.normpath(path)).split('.')[0] + else: + result['msg'] = 'The path must include the extracted snote file and ends with txt.' + module.fail_json(**result) + + pre_check = check_implementation(conn, snote) + + if state == "absent" and pre_check: + raw = call_rfc_method(conn, 'SCWB_API_NOTES_DEIMPLEMENT', {'IT_NOTES': [snote]}) + + if state == "present" and not pre_check: + if path: + raw_upload = call_rfc_method(conn, 'SCWB_API_UPLOAD_NOTES', {'IT_FILENAME': [path], 'IT_NOTES': [snote]}) + if raw_upload['EV_RC'] != 0: + result['out'] = raw_upload + result['msg'] = raw_upload['ES_MSG']['MSGTXT'] + module.fail_json(**result) + + raw = call_rfc_method(conn, 'SCWB_API_NOTES_IMPLEMENT', {'IT_NOTES': [snote]}) + queued = call_rfc_method(conn, 'SCWB_API_CINST_QUEUE_GET', {}) + + if queued['ET_MANUAL_ACTIVITIES']: + raw = call_rfc_method(conn, 'SCWB_API_CONFIRM_MAN_ACTIVITY', {}) + + if raw: + if raw['EV_RC'] == 0: + post_check = check_implementation(conn, snote) + if post_check and state == "present": + result['changed'] = True + result['msg'] = 'SNOTE "{0}" implemented.'.format(snote) + if not post_check and state == "absent": + result['changed'] = True + result['msg'] = 'SNOTE "{0}" deimplemented.'.format(snote) + else: + result['msg'] = "Something went wrong." + module.fail_json(**result) + result['out'] = raw + else: + result['msg'] = "Nothing to do." + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/sap/plugins/modules/system/sap_system_facts.py b/ansible_collections/community/sap/plugins/modules/system/sap_system_facts.py new file mode 100644 index 000000000..b5f4eb9b6 --- /dev/null +++ b/ansible_collections/community/sap/plugins/modules/system/sap_system_facts.py @@ -0,0 +1,206 @@ +#!/usr/bin/python + +# Copyright: (c) 2022, Rainer Leber rainerleber@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: sap_system_facts + +short_description: Gathers SAP facts in a host + +version_added: "1.0.0" + +description: + - This facts module gathers SAP system facts about the running instance. + +author: + - Rainer Leber (@rainerleber) + +notes: + - Supports C(check_mode). +''' + +EXAMPLES = r''' +- name: Return SAP system ansible_facts + community.sap.sap_system_fact: +''' + +RETURN = r''' +# These are examples of possible return values, +# and in general should use other names for return values. +ansible_facts: + description: Facts to add to ansible_facts. + returned: always + type: list + elements: dict + contains: + sap: + description: Facts about the running SAP system. + type: list + elements: dict + returned: When SAP system fact is present + sample: [ + { + "InstanceType": "NW", + "NR": "00", + "SID": "ABC", + "TYPE": "ASCS" + }, + { + "InstanceType": "NW", + "NR": "01", + "SID": "ABC", + "TYPE": "PAS" + }, + { + "InstanceType": "HANA", + "NR": "02", + "SID": "HDB", + "TYPE": "HDB" + }, + { + "InstanceType": "NW", + "NR": "80", + "SID": "WEB", + "TYPE": "WebDisp" + } + ] +''' + +from ansible.module_utils.basic import AnsibleModule +import os +import re + + +def get_all_hana_sid(): + hana_sid = list() + if os.path.isdir("/hana/shared"): + # /hana/shared directory exists + for sid in os.listdir('/hana/shared'): + if os.path.isdir("/usr/sap/" + sid): + hana_sid = hana_sid + [sid] + if hana_sid: + return hana_sid + + +def get_all_nw_sid(): + nw_sid = list() + if os.path.isdir("/sapmnt"): + # /sapmnt directory exists + for sid in os.listdir('/sapmnt'): + if os.path.isdir("/usr/sap/" + sid): + nw_sid = nw_sid + [sid] + else: + # Check to see if /sapmnt/SID/sap_bobj exists + if os.path.isdir("/sapmnt/" + sid + "/sap_bobj"): + # is a bobj system + nw_sid = nw_sid + [sid] + if nw_sid: + return nw_sid + + +def get_hana_nr(sids, module): + hana_list = list() + for sid in sids: + for instance in os.listdir('/usr/sap/' + sid): + if 'HDB' in instance: + instance_nr = instance[-2:] + # check if instance number exists + command = [module.get_bin_path('/usr/sap/hostctrl/exe/sapcontrol', required=True)] + command.extend(['-nr', instance_nr, '-function', 'GetProcessList']) + check_instance = module.run_command(command, check_rc=False) + # sapcontrol returns c(0 - 5) exit codes only c(1) is unavailable + if check_instance[0] != 1: + hana_list.append({'NR': instance_nr, 'SID': sid, 'TYPE': 'HDB', 'InstanceType': 'HANA'}) + return hana_list + + +def get_nw_nr(sids, module): + nw_list = list() + type = "" + for sid in sids: + for instance in os.listdir('/usr/sap/' + sid): + instance_nr = instance[-2:] + command = [module.get_bin_path('/usr/sap/hostctrl/exe/sapcontrol', required=True)] + # check if returned instance_nr is a number because sapcontrol returns all if a random string is provided + if instance_nr.isdigit(): + command.extend(['-nr', instance_nr, '-function', 'GetInstanceProperties']) + check_instance = module.run_command(command, check_rc=False) + if check_instance[0] != 1: + for line in check_instance[1].splitlines(): + if re.search('INSTANCE_NAME', line): + # convert to list and extract last + type_raw = (line.strip('][').split(', '))[-1] + # split instance number + type = type_raw[:-2] + nw_list.append({'NR': instance_nr, 'SID': sid, 'TYPE': get_instance_type(type), 'InstanceType': 'NW'}) + return nw_list + + +def get_instance_type(raw_type): + if raw_type[0] == "D": + # It's a PAS + type = "PAS" + elif raw_type[0] == "A": + # It's an ASCS + type = "ASCS" + elif raw_type[0] == "W": + # It's a Webdisp + type = "WebDisp" + elif raw_type[0] == "J": + # It's a Java + type = "Java" + elif raw_type[0] == "S": + # It's an SCS + type = "SCS" + elif raw_type[0] == "E": + # It's an ERS + type = "ERS" + else: + # Unknown instance type + type = "XXX" + return type + + +def run_module(): + module_args = dict() + system_result = list() + + result = dict( + changed=False, + ansible_facts=dict(), + ) + + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True, + ) + + hana_sid = get_all_hana_sid() + if hana_sid: + system_result = system_result + get_hana_nr(hana_sid, module) + + nw_sid = get_all_nw_sid() + if nw_sid: + system_result = system_result + get_nw_nr(nw_sid, module) + + if system_result: + result['ansible_facts'] = {'sap': system_result} + else: + result['ansible_facts'] + + if module.check_mode: + module.exit_json(**result) + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/sap/plugins/modules/system/sap_task_list_execute.py b/ansible_collections/community/sap/plugins/modules/system/sap_task_list_execute.py new file mode 100644 index 000000000..0ae25903f --- /dev/null +++ b/ansible_collections/community/sap/plugins/modules/system/sap_task_list_execute.py @@ -0,0 +1,340 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: sap_task_list_execute +short_description: Perform SAP Task list execution +version_added: "0.1.0" +description: + - The M(community.sap.sap_task_list_execute) module depends on C(pyrfc) Python library (version 2.4.0 and upwards). + Depending on distribution you are using, you may need to install additional packages to + have these available. + - Tasks in the task list which requires manual activities will be confirmed automatically. + - This module will use the RFC package C(STC_TM_API). + +requirements: + - pyrfc >= 2.4.0 + - xmltodict + +options: + conn_username: + description: The required username for the SAP system. + required: true + type: str + conn_password: + description: The required password for the SAP system. + required: true + type: str + host: + description: The required host for the SAP system. Can be either an FQDN or IP Address. + required: true + type: str + sysnr: + description: + - The system number of the SAP system. + - You must quote the value to ensure retaining the leading zeros. + default: '00' + type: str + client: + description: + - The client number to connect to. + - You must quote the value to ensure retaining the leading zeros. + default: '000' + type: str + task_to_execute: + description: The task list which will be executed. + required: true + type: str + task_parameters: + description: + - The tasks and the parameters for execution. + - If the task list does not need any parameters, this could be empty. + - If only specific tasks from the task list should be executed, + the tasks even when no parameter is needed must be provided + alongside with the module parameter I(task_skip=true). + type: list + elements: dict + suboptions: + TASKNAME: + description: The name of the task in the task list. + type: str + required: true + FIELDNAME: + description: The name of the field of the task. + type: str + VALUE: + description: The value which have to be set. + type: raw + task_settings: + description: + - Setting for the execution of the task list. This can be the following as in TCODE SE80 described. + Check Mode C(CHECKRUN), Background Processing Active C(BATCH) (this is the default value), + Asynchronous Execution C(ASYNC), Trace Mode C(TRACE), Server Name C(BATCH_TARGET). + default: ['BATCH'] + type: list + elements: str + task_skip: + description: + - If this parameter is C(true), not defined tasks in I(task_parameters) are skipped. + - This could be the case when only certain tasks should run from the task list. + default: false + type: bool + +notes: + - Does not support C(check_mode). Always returns that the state has changed. +author: + - Rainer Leber (@rainerleber) +''' + +EXAMPLES = r''' +# Pass in a message +- name: Test task execution + community.sap.sap_task_list_execute: + conn_username: DDIC + conn_password: Passwd1234 + host: 10.1.8.10 + sysnr: '01' + client: '000' + task_to_execute: SAP_BASIS_SSL_CHECK + task_settings: batch + +- name: Pass in input parameters + community.sap.sap_task_list_execute: + conn_username: DDIC + conn_password: Passwd1234 + host: 10.1.8.10 + sysnr: '00' + client: '000' + task_to_execute: SAP_BASIS_SSL_CHECK + task_parameters : + - { 'TASKNAME': 'CL_STCT_CHECK_SEC_CRYPTO', 'FIELDNAME': 'P_OPT2', 'VALUE': 'X' } + - TASKNAME: CL_STCT_CHECK_SEC_CRYPTO + FIELDNAME: P_OPT3 + VALUE: X + task_settings: batch + +# Exported environment variables +- name: Hint if module will fail with error message like ImportError libsapnwrfc.so... + community.sap.sap_task_list_execute: + conn_username: DDIC + conn_password: Passwd1234 + host: 10.1.8.10 + sysnr: '00' + client: '000' + task_to_execute: SAP_BASIS_SSL_CHECK + task_settings: batch + environment: + SAPNWRFC_HOME: /usr/local/sap/nwrfcsdk + LD_LIBRARY_PATH: /usr/local/sap/nwrfcsdk/lib +''' + +RETURN = r''' +msg: + description: A small execution description. + type: str + returned: always + sample: 'Successful' +out: + description: A complete description of the executed tasks. If this is available. + type: list + elements: dict + returned: on success + sample: [...,{ + "LOG": { + "STCTM_S_LOG": [ + { + "ACTIVITY": "U_CONFIG", + "ACTIVITY_DESCR": "Configuration changed", + "DETAILS": null, + "EXEC_ID": "20210728184903.815739", + "FIELD": null, + "ID": "STC_TASK", + "LOG_MSG_NO": "000000", + "LOG_NO": null, + "MESSAGE": "For radiobutton group ICM too many options are set; choose only one option", + "MESSAGE_V1": "ICM", + "MESSAGE_V2": null, + "MESSAGE_V3": null, + "MESSAGE_V4": null, + "NUMBER": "048", + "PARAMETER": null, + "PERIOD": "M", + "PERIOD_DESCR": "Maintenance", + "ROW": "0", + "SRC_LINE": "170", + "SRC_OBJECT": "CL_STCTM_REPORT_UI IF_STCTM_UI_TASK~SET_PARAMETERS", + "SYSTEM": null, + "TIMESTMP": "20210728184903", + "TSTPNM": "DDIC", + "TYPE": "E" + },... + ]}}] +''' + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +import traceback +try: + from pyrfc import Connection +except ImportError: + HAS_PYRFC_LIBRARY = False + PYRFC_LIBRARY_IMPORT_ERROR = traceback.format_exc() +else: + HAS_PYRFC_LIBRARY = True +try: + import xmltodict +except ImportError: + HAS_XMLTODICT_LIBRARY = False + XMLTODICT_LIBRARY_IMPORT_ERROR = traceback.format_exc() +else: + HAS_XMLTODICT_LIBRARY = True + + +def call_rfc_method(connection, method_name, kwargs): + # PyRFC call function + return connection.call(method_name, **kwargs) + + +def process_exec_settings(task_settings): + # processes task settings to objects + exec_settings = {} + for settings in task_settings: + temp_dict = {settings.upper(): 'X'} + for key, value in temp_dict.items(): + exec_settings[key] = value + return exec_settings + + +def xml_to_dict(xml_raw): + try: + xml_parsed = xmltodict.parse(xml_raw, dict_constructor=dict) + xml_dict = xml_parsed['asx:abap']['asx:values']['SESSION']['TASKLIST'] + except KeyError: + xml_dict = "No logs available." + return xml_dict + + +def run_module(): + + params_spec = dict( + TASKNAME=dict(type='str', required=True), + FIELDNAME=dict(type='str'), + VALUE=dict(type='raw'), + ) + + # define available arguments/parameters a user can pass to the module + module = AnsibleModule( + argument_spec=dict( + # values for connection + conn_username=dict(type='str', required=True), + conn_password=dict(type='str', required=True, no_log=True), + host=dict(type='str', required=True), + sysnr=dict(type='str', default="00"), + client=dict(type='str', default="000"), + # values for execution tasks + task_to_execute=dict(type='str', required=True), + task_parameters=dict(type='list', elements='dict', options=params_spec), + task_settings=dict(type='list', elements='str', default=['BATCH']), + task_skip=dict(type='bool', default=False), + ), + supports_check_mode=False, + ) + result = dict(changed=False, msg='', out={}) + + params = module.params + + username = params['conn_username'].upper() + password = params['conn_password'] + host = params['host'] + sysnr = params['sysnr'] + client = params['client'] + + task_parameters = params['task_parameters'] + task_to_execute = params['task_to_execute'] + task_settings = params['task_settings'] + task_skip = params['task_skip'] + + if not HAS_PYRFC_LIBRARY: + module.fail_json( + msg=missing_required_lib('pyrfc'), + exception=PYRFC_LIBRARY_IMPORT_ERROR) + + if not HAS_XMLTODICT_LIBRARY: + module.fail_json( + msg=missing_required_lib('xmltodict'), + exception=XMLTODICT_LIBRARY_IMPORT_ERROR) + + # basic RFC connection with pyrfc + try: + conn = Connection(user=username, passwd=password, ashost=host, sysnr=sysnr, client=client) + except Exception as err: + result['error'] = str(err) + result['msg'] = 'Something went wrong connecting to the SAP system.' + module.fail_json(**result) + + try: + raw_params = call_rfc_method(conn, 'STC_TM_SCENARIO_GET_PARAMETERS', + {'I_SCENARIO_ID': task_to_execute}) + except Exception as err: + result['error'] = str(err) + result['msg'] = 'The task list does not exist.' + module.fail_json(**result) + exec_settings = process_exec_settings(task_settings) + # initialize session task + session_init = call_rfc_method(conn, 'STC_TM_SESSION_BEGIN', + {'I_SCENARIO_ID': task_to_execute, + 'I_INIT_ONLY': 'X'}) + # Confirm Tasks which requires manual activities from Task List Run + for task in raw_params['ET_PARAMETER']: + call_rfc_method(conn, 'STC_TM_TASK_CONFIRM', + {'I_SESSION_ID': session_init['E_SESSION_ID'], + 'I_TASKNAME': task['TASKNAME']}) + if task_skip: + for task in raw_params['ET_PARAMETER']: + call_rfc_method(conn, 'STC_TM_TASK_SKIP', + {'I_SESSION_ID': session_init['E_SESSION_ID'], + 'I_TASKNAME': task['TASKNAME'], 'I_SKIP_DEP_TASKS': 'X'}) + # unskip defined tasks and set parameters + if task_parameters is not None: + for task in task_parameters: + call_rfc_method(conn, 'STC_TM_TASK_UNSKIP', + {'I_SESSION_ID': session_init['E_SESSION_ID'], + 'I_TASKNAME': task['TASKNAME'], 'I_UNSKIP_DEP_TASKS': 'X'}) + + call_rfc_method(conn, 'STC_TM_SESSION_SET_PARAMETERS', + {'I_SESSION_ID': session_init['E_SESSION_ID'], + 'IT_PARAMETER': task_parameters}) + # start the task + try: + session_start = call_rfc_method(conn, 'STC_TM_SESSION_RESUME', + {'I_SESSION_ID': session_init['E_SESSION_ID'], + 'IS_EXEC_SETTINGS': exec_settings}) + except Exception as err: + result['error'] = str(err) + result['msg'] = 'Something went wrong. See error.' + module.fail_json(**result) + # get task logs because the execution may successfully but the tasks shows errors or warnings + # returned value is ABAPXML https://help.sap.com/doc/abapdocu_755_index_htm/7.55/en-US/abenabap_xslt_asxml_general.htm + session_log = call_rfc_method(conn, 'STC_TM_SESSION_GET_LOG', + {'I_SESSION_ID': session_init['E_SESSION_ID']}) + + task_list = xml_to_dict(session_log['E_LOG']) + + result['changed'] = True + result['msg'] = session_start['E_STATUS_DESCR'] + result['out'] = task_list + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() |