diff options
Diffstat (limited to 'ansible_collections/community/sap_libs/plugins')
13 files changed, 3054 insertions, 0 deletions
diff --git a/ansible_collections/community/sap_libs/plugins/doc_fragments/__init__.py b/ansible_collections/community/sap_libs/plugins/doc_fragments/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/ansible_collections/community/sap_libs/plugins/doc_fragments/__init__.py diff --git a/ansible_collections/community/sap_libs/plugins/module_utils/__init__.py b/ansible_collections/community/sap_libs/plugins/module_utils/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/ansible_collections/community/sap_libs/plugins/module_utils/__init__.py diff --git a/ansible_collections/community/sap_libs/plugins/module_utils/pyrfc_handler.py b/ansible_collections/community/sap_libs/plugins/module_utils/pyrfc_handler.py new file mode 100644 index 00000000..4f4e5b4e --- /dev/null +++ b/ansible_collections/community/sap_libs/plugins/module_utils/pyrfc_handler.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2022, Sean Freeman , +# Rainer Leber <rainerleber@gmail.com> <rainer.leber@sva.de> +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.module_utils.basic import missing_required_lib + +import traceback + +PYRFC_LIBRARY_IMPORT_ERROR = None +try: + import pyrfc +except ImportError: + PYRFC_LIBRARY_IMPORT_ERROR = traceback.format_exc() + HAS_PYRFC_LIBRARY = False +else: + HAS_PYRFC_LIBRARY = True + + +def get_connection(module, conn_params): + if not HAS_PYRFC_LIBRARY: + module.fail_json(msg=missing_required_lib( + "pyrfc"), exception=PYRFC_LIBRARY_IMPORT_ERROR) + + module.warn('Connecting ... %s' % conn_params['ashost']) + if "saprouter" in conn_params: + module.warn("...via SAPRouter to SAP System") + elif "gwhost" in conn_params: + module.warn("...via Gateway to SAP System") + else: + module.warn("...direct to SAP System") + + conn = pyrfc.Connection(**conn_params) + + module.warn("Verifying connection is open/alive: %s" % conn.alive) + return conn diff --git a/ansible_collections/community/sap_libs/plugins/module_utils/swpm2_parameters_inifile_generate.py b/ansible_collections/community/sap_libs/plugins/module_utils/swpm2_parameters_inifile_generate.py new file mode 100755 index 00000000..f7b72d52 --- /dev/null +++ b/ansible_collections/community/sap_libs/plugins/module_utils/swpm2_parameters_inifile_generate.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2022, Sean Freeman , +# Rainer Leber <rainerleber@gmail.com> <rainer.leber@sva.de> +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.module_utils.basic import missing_required_lib +import traceback +import sys +import os + + +BS4_LIBRARY_IMPORT_ERROR = None +try: + from bs4 import BeautifulSoup +except ImportError: + BS4_LIBRARY_IMPORT_ERROR = traceback.format_exc() + HAS_BS4_LIBRARY = False +else: + HAS_BS4_LIBRARY = True + +LXML_LIBRARY_IMPORT_ERROR = None +try: + from lxml import etree +except ImportError: + LXML_LIBRARY_IMPORT_ERROR = traceback.format_exc() + HAS_LXML_LIBRARY = False +else: + HAS_LXML_LIBRARY = True + + +def debug_bs4(module): + # Diagnose XML file parsing errors in Beautiful Soup + # https://stackoverflow.com/questions/56942892/cannot-parse-iso-8859-15-encoded-xml-with-bs4/56947172#56947172 + if not HAS_BS4_LIBRARY: + module.fail_json(msg=missing_required_lib( + "bs4"), exception=BS4_LIBRARY_IMPORT_ERROR) + from bs4.diagnose import diagnose + with open('control.xml', 'rb') as f: + diagnose(f) + + +# SWPM2 control.xml conversion to utf8 +def control_xml_utf8(filepath, module): + if not HAS_LXML_LIBRARY: + module.fail_json(msg=missing_required_lib( + "lxml"), exception=LXML_LIBRARY_IMPORT_ERROR) + source = filepath + "/control.xml" + + # Convert control.xml from iso-8859-1 to UTF-8, so it can be used with Beautiful Soup lxml-xml parser + # https://stackoverflow.com/questions/64629600/how-can-you-convert-a-xml-iso-8859-1-to-utf-8-using-python-3-7-7/64634454#64634454 + with open(source, 'rb') as source: + parser = etree.XMLParser(encoding="iso-8859-1", strip_cdata=False) + root = etree.parse(source, parser) + + string = etree.tostring(root, xml_declaration=True, encoding="UTF-8", + pretty_print=True).decode('utf8').encode('iso-8859-1') + +# string1 = etree.tostring(root, xml_declaration=True, encoding="UTF-8", +# pretty_print=True).decode('utf8').encode('utf-8').strip() + + with open('control_utf8.xml', 'wb') as target: + target.write(string) + + +# SWPM2 Component and Parameters extract all as CSV +def control_xml_to_csv(filepath, module): + if not HAS_BS4_LIBRARY: + module.fail_json(msg=missing_required_lib( + "bs4"), exception=BS4_LIBRARY_IMPORT_ERROR) + + infile = open(filepath + "/control_utf8.xml", "r") + contents = infile.read() + + soup = BeautifulSoup(markup=contents, features='lxml-xml') + space = soup.find('components') + + component_list = space.findChildren("component", recursive=False) + + csv_output = open('control_output.csv', 'w') + csv_header = '"' + 'Component Name' + '","' + 'Component Display Name' + '","' + 'Parameter Name' + '","' + 'Parameter Inifile Key' + \ + '","' + 'Parameter Access' + '","' + 'Parameter Encode' + '","' + \ + 'Parameter Default Value' + '","' + 'Parameter Inifile description' + '"' + csv_output.write("%s\n" % csv_header) + + for component in component_list: + for parameter in component.findChildren("parameter"): + component_key = parameter.findParent("component") + component_key_name_text = component_key["name"] + for child in component_key.findChildren("display-name"): + component_key_display_name_text = child.get_text().replace('\n', '') + component_parameter_key_name = parameter["name"] + component_parameter_key_inifile_name = parameter.get( + "defval-for-inifile-generation", "") + component_parameter_key_access = parameter.get("access", "") + component_parameter_key_encode = parameter.get("encode", "") + component_parameter_key_defval = parameter.get("defval", "") + component_parameter_contents_doclong_text = parameter.get_text().replace('\n', '') + component_parameter_contents_doclong_text_quote_replacement = component_parameter_contents_doclong_text.replace( + '"', '\'') + csv_string = '"' + component_key_name_text + '","' + component_key_display_name_text + '","' + \ + component_parameter_key_name + '","' + component_parameter_key_inifile_name + '","' + \ + component_parameter_key_access + '","' + component_parameter_key_encode + '","' + \ + component_parameter_key_defval + '","' + \ + component_parameter_contents_doclong_text_quote_replacement + '"' + csv_output.write("%s\n" % csv_string) + + csv_output.close() + + +# SWPM2 Component and Parameters extract all and generate template inifile.params +def control_xml_to_inifile_params(filepath, module): + if not HAS_BS4_LIBRARY: + module.fail_json(msg=missing_required_lib( + "bs4"), exception=BS4_LIBRARY_IMPORT_ERROR) + + infile = open(filepath + "/control_utf8.xml", "r") + contents = infile.read() + + soup = BeautifulSoup(markup=contents, features='lxml-xml') + space = soup.find('components') + + component_list = space.findChildren("component", recursive=False) + + inifile_output = open('generated_inifile_params', 'w') + + inifile_params_header = """############ + # SWPM Unattended Parameters inifile.params generated export + # + # + # Export of all SWPM Component and the SWPM Unattended Parameters. Not all components have SWPM Unattended Parameters. + # + # All parameters are commented-out, each hash # before the parameter is removed to activate the parameter. + # When running SWPM in Unattended Mode, the activated parameters will create a new SWPM file in the sapinst directory. + # If any parameter is marked as 'encode', the plaintext value will be coverted to DES hash + # for this parameter in the new SWPM file (in the sapinst directory). + # + # An inifile.params is otherwise obtained after running SWPM as GUI or Unattended install, + # and will be generated for a specific Product ID (such as 'NW_ABAP_OneHost:S4HANA1809.CORE.HDB.CP'). + ############ + + + + ############ + # MANUAL + ############ + + # The folder containing all archives that have been downloaded from http://support.sap.com/swdc and are supposed to be used in this procedure + # archives.downloadBasket = + """ + + inifile_output.write(inifile_params_header) + + for component in component_list: + component_key_name_text = component["name"] + component_key_display_name = component.find("display-name") + if component_key_display_name is not None: + component_key_display_name_text = component_key_display_name.get_text() + inifile_output.write("\n\n\n\n############\n# Component: %s\n# Component Display Name: %s\n############\n" % ( + component_key_name_text, component_key_display_name_text)) + for parameter in component.findChildren("parameter"): + # component_key=parameter.findParent("component") + component_parameter_key_encode = parameter.get("encode", None) + component_parameter_key_inifile_name = parameter.get( + "defval-for-inifile-generation", None) + component_parameter_key_defval = parameter.get("defval", "") + component_parameter_contents_doclong_text = parameter.get_text().replace('\n', '') +# component_parameter_contents_doclong_text_quote_replacement=component_parameter_contents_doclong_text.replace('"','\'') + if component_parameter_key_inifile_name is not None: + inifile_output.write("\n# %s" % ( + component_parameter_contents_doclong_text)) + if component_parameter_key_encode == "true": + inifile_output.write( + "\n# Encoded parameter. Plaintext values will be coverted to DES hash") + inifile_output.write("\n# %s = %s\n" % ( + component_parameter_key_inifile_name, component_parameter_key_defval)) + + inifile_output.close() + +# SWPM2 product.catalog conversion to utf8 + + +def product_catalog_xml_utf8(filepath, module): + if not HAS_LXML_LIBRARY: + module.fail_json(msg=missing_required_lib( + "lxml"), exception=LXML_LIBRARY_IMPORT_ERROR) + + source = filepath + "/product.catalog" + + # Convert control.xml from iso-8859-1 to UTF-8, so it can be used with Beautiful Soup lxml-xml parser + # https://stackoverflow.com/questions/64629600/how-can-you-convert-a-xml-iso-8859-1-to-utf-8-using-python-3-7-7/64634454#64634454 + with open(source, 'rb') as source: + parser = etree.XMLParser(encoding="iso-8859-1", strip_cdata=False) + root = etree.parse(source, parser) + + string = etree.tostring(root, xml_declaration=True, encoding="UTF-8", + pretty_print=True).decode('utf8').encode('iso-8859-1') + + with open('product_catalog_utf8.xml', 'wb') as target: + target.write(string) + +# SWPM2 Product Catalog entries to CSV +# Each Product Catalog entry is part of a components group, which may have attributes: +# output-dir, control-file, product-dir (link to SWPM directory of param file etc) +# Attributes possible for each entry = control-file, db, id, name, os, os-type, output-dir, +# ppms-component, ppms-component-release, product, product-dir, release, table + + +def product_catalog_xml_to_csv(filepath, module): + if not HAS_BS4_LIBRARY: + module.fail_json(msg=missing_required_lib( + "bs4"), exception=BS4_LIBRARY_IMPORT_ERROR) + + infile = open(filepath + "/product_catalog_utf8.xml", "r") + contents = infile.read() + + soup = BeautifulSoup(markup=contents, features='lxml-xml') + space = soup.find_all('component') + + csv_output = open('product_catalog_output.csv', 'w') + csv_header = '"' + 'Product Catalog Component Name' + '","' + 'Product Catalog Component ID' + '","' + 'Product Catalog Component Table' + '","' + \ + 'Product Catalog Component Output Dir' + '","' + 'Product Catalog Component Display Name' + \ + '","' + 'Product Catalog Component UserInfo' + '"' + csv_output.write("%s\n" % csv_header) + + for component in space: + component_name = component.get("name", "") + component_id = component.get("id", "") + component_table = component.get("table", "") + component_output_dir = component.get("output-dir", "") + for displayname in component.findChildren("display-name"): + component_displayname = displayname.get_text().strip() + for userinfo in component.findChildren("user-info"): + html_raw = userinfo.get_text().strip() + html_parsed = BeautifulSoup(html_raw, 'html.parser') + component_userinfo = html_parsed.get_text().replace('"', '\'') + csv_string = '"' + component_name + '","' + component_id + '","' + component_table + '","' + \ + component_output_dir + '","' + component_displayname + \ + '","' + component_userinfo + '"' + csv_output.write("%s\n" % csv_string) + + csv_output.close() + + +# Get arguments passed to Python script session +# Define path to control.xml, else assume in /tmp directory + +if len(sys.argv) > 1: + control_xml_path = sys.argv[1] +else: + control_xml_path = "/tmp" + +if control_xml_path == "": + control_xml_path = os.getcwd() + +if os.path.exists(control_xml_path + '/control.xml'): + control_xml_utf8(control_xml_path, '') + control_xml_to_csv(control_xml_path, '') + control_xml_to_inifile_params(control_xml_path, '') diff --git a/ansible_collections/community/sap_libs/plugins/modules/sap_company.py b/ansible_collections/community/sap_libs/plugins/modules/sap_company.py new file mode 100644 index 00000000..29b214f6 --- /dev/null +++ b/ansible_collections/community/sap_libs/plugins/modules/sap_company.py @@ -0,0 +1,335 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> <rainer.leber@sva.de> +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +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_libs.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_libs.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_libs.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: + ANOTHER_LIBRARY_IMPORT_ERROR = None + 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_libs/plugins/modules/sap_control_exec.py b/ansible_collections/community/sap_libs/plugins/modules/sap_control_exec.py new file mode 100644 index 00000000..c24fa609 --- /dev/null +++ b/ansible_collections/community/sap_libs/plugins/modules/sap_control_exec.py @@ -0,0 +1,401 @@ +#!/usr/bin/python + +# Copyright: (c) 2022, Rainer Leber rainerleber@gmail.com, rainer.leber@sva.de, +# Robert Kraemer @rkpobe, robert.kraemer@sva.de +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: sap_control_exec + +short_description: Ansible Module to execute SAPCONTROL + +version_added: "1.1.0" + +description: + - Provides support for sapstartsrv formaly known as sapcontrol + - A complete information of all functions and the parameters can be found here + U(https://www.sap.com/documents/2016/09/0a40e60d-8b7c-0010-82c7-eda71af511fa.html) + +options: + sysnr: + description: + - The system number of the instance. + required: false + type: str + port: + description: + - The port number of the sapstartsrv. + required: false + type: int + username: + description: + - The username to connect to the sapstartsrv. + required: false + type: str + password: + description: + - The password to connect to the sapstartsrv. + required: false + type: str + hostname: + description: + - The hostname to connect to the sapstartsrv. + - Could be an IP address, FQDN or hostname. + required: false + default: localhost + type: str + function: + description: + - The function to execute. + required: true + choices: + - Start + - Stop + - RestartInstance + - Shutdown + - InstanceStart + - GetProcessList + - Bootstrap + - InstanceStop + - StopService + - StartService + - RestartService + - ParameterValue + - GetStartProfile + - GetTraceFile + - GetAlertTree + - GetAlerts + - GetEnvironment + - GetVersionInfo + - GetQueueStatistic + - GetInstanceProperties + - ListDeveloperTraces + - ReadDeveloperTrace + - ListLogFiles + - ReadLogFile + - AnalyseLogFiles + - ConfigureLogFileList + - GetLogFileList + - CreateSnapshot + - ReadSnapshot + - ListSnapshots + - DeleteSnapshots + - GetAccessPointList + - GetProcessParameter + - SetProcessParameter + - SetProcessParameter2 + - CheckParameter + - OSExecute + - SendSignal + - GetCallstack + - GetSystemInstanceList + - StartSystem + - StopSystem + - RestartSystem + - GetSystemUpdateList + - UpdateSystem + - UpdateSCSInstance + - CheckUpdateSystem + - AccessCheck + - GetSecNetworkId + - GetNetworkId + - RequestLogonFile + - UpdateSystemPKI + - UpdateInstancePSE + - StorePSE + - DeletePSE + - CheckPSE + - CreatePSECredential + - HACheckConfig + - HACheckFailoverConfig + - HAGetFailoverConfig + - HAFailoverToNode + - HASetMaintenanceMode + - HACheckMaintenanceMode + - ABAPReadSyslog + - ABAPReadRawSyslog + - ABAPGetWPTable + - ABAPGetComponentList + - ABAPCheckRFCDestinations + - ABAPGetSystemWPTable + - J2EEControlProcess + - J2EEControlCluster + - J2EEEnableDbgSession + - J2EEDisableDbgSession + - J2EEGetProcessList + - J2EEGetProcessList2 + - J2EEGetThreadList + - J2EEGetThreadList2 + - J2EEGetThreadCallStack + - J2EEGetThreadTaskStack + - J2EEGetSessionList + - J2EEGetCacheStatistic + - J2EEGetCacheStatistic2 + - J2EEGetApplicationAliasList + - J2EEGetComponentList + - J2EEControlComponents + - J2EEGetWebSessionList + - J2EEGetWebSessionList2 + - J2EEGetEJBSessionList + - J2EEGetRemoteObjectList + - J2EEGetVMGCHistory + - J2EEGetVMGCHistory2 + - J2EEGetVMHeapInfo + - J2EEGetClusterMsgList + - J2EEGetSharedTableInfo + - ICMGetThreadList + - ICMGetConnectionList + - ICMGetProxyConnectionList + - ICMGetCacheEntries + - WebDispGetServerList + - WebDispGetGroupList + - WebDispGetVirtHostList + - WebDispGetUrlPrefixList + - EnqGetStatistic + - EnqGetLockTable + - EnqRemoveUserLocks + - StartWait + - StopWait + - WaitforStarted + - WaitforStopped + - RestartServiceWait + - WaitforServiceStarted + - CheckHostAgent + type: str + parameter: + description: + - The parameter to pass to the function. + required: false + type: str + force: + description: + - Forces the execution of the function C(Stop). + required: false + default: false + type: bool +author: + - Rainer Leber (@RainerLeber) + - Robert Kraemer (@rkpobe) +notes: + - Does not support C(check_mode). +''' + +EXAMPLES = r""" +- name: GetProcessList with sysnr + community.sap_libs.sap_control_exec: + hostname: 192.168.8.15 + sysnr: "01" + function: GetProcessList + +- name: GetProcessList with custom port + community.sap_libs.sap_control_exec: + hostname: 192.168.8.15 + function: GetProcessList + port: 50113 + +- name: ParameterValue + community.sap_libs.sap_control_exec: + hostname: 192.168.8.15 + sysnr: "01" + username: hdbadm + password: test1234# + function: ParameterValue + parameter: ztta +""" + +RETURN = r''' +msg: + description: Success-message with functionname. + type: str + returned: always + sample: 'Succesful execution of: GetProcessList' +out: + description: The full output of the required function. + type: list + elements: dict + returned: always + sample: [{ + "item": [ + { + "description": "MessageServer", + "dispstatus": "SAPControl-GREEN", + "elapsedtime": "412:30:50", + "name": "msg_server", + "pid": 70643, + "starttime": "2022 03 13 15:22:42", + "textstatus": "Running" + }, + { + "description": "EnqueueServer", + "dispstatus": "SAPControl-GREEN", + "elapsedtime": "412:30:50", + "name": "enserver", + "pid": 70644, + "starttime": "2022 03 13 15:22:42", + "textstatus": "Running" + }, + { + "description": "Gateway", + "dispstatus": "SAPControl-GREEN", + "elapsedtime": "412:30:50", + "name": "gwrd", + "pid": 70645, + "starttime": "2022 03 13 15:22:42", + "textstatus": "Running" + } + ] + }] +''' + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +import traceback +try: + from suds.client import Client + from suds.sudsobject import asdict +except ImportError: + HAS_SUDS_LIBRARY = False + SUDS_LIBRARY_IMPORT_ERROR = traceback.format_exc() +else: + SUDS_LIBRARY_IMPORT_ERROR = None + HAS_SUDS_LIBRARY = True + + +def choices(): + retlist = ["Start", "Stop", "RestartInstance", "Shutdown", "InstanceStart", 'GetProcessList', + 'Bootstrap', 'InstanceStop', 'StopService', 'StartService', 'RestartService', 'ParameterValue', + 'GetStartProfile', 'GetTraceFile', 'GetAlertTree', 'GetAlerts', 'GetEnvironment', 'GetVersionInfo', + 'GetQueueStatistic', 'GetInstanceProperties', 'ListDeveloperTraces', 'ReadDeveloperTrace', + 'ListLogFiles', 'ReadLogFile', 'AnalyseLogFiles', 'ConfigureLogFileList', 'GetLogFileList', 'CreateSnapshot', 'ReadSnapshot', + 'ListSnapshots', 'DeleteSnapshots', 'GetAccessPointList', 'GetProcessParameter', 'SetProcessParameter', + 'SetProcessParameter2', 'CheckParameter', 'OSExecute', 'SendSignal', 'GetCallstack', 'GetSystemInstanceList', + 'StartSystem', 'StopSystem', 'RestartSystem', 'GetSystemUpdateList', 'UpdateSystem', 'UpdateSCSInstance', + 'CheckUpdateSystem', 'AccessCheck', 'GetSecNetworkId', 'GetNetworkId', 'RequestLogonFile', + 'UpdateSystemPKI', 'UpdateInstancePSE', 'StorePSE', 'DeletePSE', 'CheckPSE', 'CreatePSECredential', + 'HACheckConfig', 'HACheckFailoverConfig', 'HAGetFailoverConfig', 'HAFailoverToNode', + 'HASetMaintenanceMode', 'HACheckMaintenanceMode', 'ABAPReadSyslog', 'ABAPReadRawSyslog', + 'ABAPGetWPTable', 'ABAPGetComponentList', 'ABAPCheckRFCDestinations', + 'ABAPGetSystemWPTable', 'J2EEControlProcess', 'J2EEControlCluster', 'J2EEEnableDbgSession', + 'J2EEDisableDbgSession', 'J2EEGetProcessList', 'J2EEGetProcessList2', 'J2EEGetThreadList', 'J2EEGetThreadList2', + 'J2EEGetThreadCallStack', 'J2EEGetThreadTaskStack', 'J2EEGetSessionList', 'J2EEGetCacheStatistic', + 'J2EEGetCacheStatistic2', 'J2EEGetApplicationAliasList', 'J2EEGetComponentList', + 'J2EEControlComponents', 'J2EEGetWebSessionList', 'J2EEGetWebSessionList2', 'J2EEGetEJBSessionList', 'J2EEGetRemoteObjectList', + 'J2EEGetVMGCHistory', 'J2EEGetVMGCHistory2', 'J2EEGetVMHeapInfo', 'J2EEGetClusterMsgList', 'J2EEGetSharedTableInfo', + 'ICMGetThreadList', 'ICMGetConnectionList', 'ICMGetProxyConnectionList', 'ICMGetCacheEntries', 'WebDispGetServerList', + 'WebDispGetGroupList', 'WebDispGetVirtHostList', 'WebDispGetUrlPrefixList', 'EnqGetStatistic', 'EnqGetLockTable', + 'EnqRemoveUserLocks', 'StartWait', 'StopWait', 'WaitforStarted', 'WaitforStopped', 'RestartServiceWait', + 'WaitforServiceStarted', 'CheckHostAgent'] + return retlist + + +# converts recursively the suds object to a dictionary e.g. {'item': [{'name': hdbdaemon, 'value': '1'}]} +def recursive_dict(suds_object): + out = {} + if isinstance(suds_object, str): + return suds_object + for k, v in asdict(suds_object).items(): + if hasattr(v, '__keylist__'): + out[k] = recursive_dict(v) + elif isinstance(v, list): + out[k] = [] + for item in v: + if hasattr(item, '__keylist__'): + out[k].append(recursive_dict(item)) + else: + out[k].append(item) + else: + out[k] = v + return out + + +def connection(hostname, port, username, password, function, parameter): + url = 'http://{0}:{1}/sapcontrol?wsdl'.format(hostname, port) + client = Client(url, username=username, password=password) + _function = getattr(client.service, function) + if parameter is not None: + result = _function(parameter) + else: + result = _function() + + return result + + +def main(): + module = AnsibleModule( + argument_spec=dict( + sysnr=dict(type='str', required=False), + port=dict(type='int', required=False), + username=dict(type='str', required=False), + password=dict(type='str', no_log=True, required=False), + hostname=dict(type='str', default="localhost"), + function=dict(type='str', required=True, choices=choices()), + parameter=dict(type='str', required=False), + force=dict(type='bool', default=False), + ), + required_one_of=[('sysnr', 'port')], + mutually_exclusive=[('sysnr', 'port')], + supports_check_mode=False, + ) + result = dict(changed=False, msg='', out={}, error='') + params = module.params + + sysnr = params['sysnr'] + port = params['port'] + username = params['username'] + password = params['password'] + hostname = params['hostname'] + function = params['function'] + parameter = params['parameter'] + force = params['force'] + + if not HAS_SUDS_LIBRARY: + module.fail_json( + msg=missing_required_lib('suds'), + exception=SUDS_LIBRARY_IMPORT_ERROR) + + if function == "Stop": + if force is False: + module.fail_json(msg="Stop function requires force: True") + + if port is None: + try: + try: + conn = connection(hostname, "5{0}14".format((sysnr).zfill(2)), username, password, function, parameter) + except Exception: + conn = connection(hostname, "5{0}13".format((sysnr).zfill(2)), username, password, function, parameter) + except Exception as err: + result['error'] = str(err) + else: + try: + conn = connection(hostname, port, username, password, function, parameter) + except Exception as err: + result['error'] = str(err) + + if result['error'] != '': + result['msg'] = 'Something went wrong connecting to the SAPCONTROL SOAP API.' + module.fail_json(**result) + + if conn is not None: + returned_data = recursive_dict(conn) + else: + returned_data = conn + + result['changed'] = True + result['msg'] = "Succesful execution of: " + function + result['out'] = [returned_data] + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/sap_libs/plugins/modules/sap_hdbsql.py b/ansible_collections/community/sap_libs/plugins/modules/sap_hdbsql.py new file mode 100644 index 00000000..994db704 --- /dev/null +++ b/ansible_collections/community/sap_libs/plugins/modules/sap_hdbsql.py @@ -0,0 +1,246 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: sap_hdbsql +short_description: Ansible Module to execute SQL on SAP HANA +version_added: "1.0.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_libs.sap_hdbsql: + sid: "hdb" + instance: "01" + password: "Test123" + query: select user_name from users + +- name: RUN select query with host port + community.sap_libs.sap_hdbsql: + sid: "hdb" + instance: "01" + password: "Test123" + host: "10.10.2.4:30001" + query: select user_name from users + +- name: Run several queries + community.sap_libs.sap_hdbsql: + 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_libs.sap_hdbsql: + 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_libs.sap_hdbsql: + 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_libs.sap_hdbsql: + 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_libs/plugins/modules/sap_pyrfc.py b/ansible_collections/community/sap_libs/plugins/modules/sap_pyrfc.py new file mode 100644 index 00000000..51dbcea2 --- /dev/null +++ b/ansible_collections/community/sap_libs/plugins/modules/sap_pyrfc.py @@ -0,0 +1,187 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2022, Sean Freeman , +# Rainer Leber <rainerleber@gmail.com> <rainer.leber@sva.de> +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: sap_pyrfc + +short_description: Ansible Module for use of SAP PyRFC to execute SAP RFCs (Remote Function Calls) to SAP remote-enabled function modules + +version_added: "1.2.0" + +description: + - This module will executes rfc calls on a sap system. + - It is a generic approach to call rfc functions on a SAP System. + - This module should be used where no module or role is provided. + +options: + function: + description: The SAP RFC function to call. + required: true + type: str + parameters: + description: The parameters which are needed by the function. + required: true + type: dict + connection: + description: The required connection details. + required: true + type: dict + suboptions: + ashost: + description: The required host for the SAP system. Can be either an FQDN or IP Address. + type: str + required: true + sysid: + description: The systemid of the SAP system. + type: str + required: false + sysnr: + description: + - The system number of the SAP system. + - You must quote the value to ensure retaining the leading zeros. + type: str + required: true + client: + description: + - The client number to connect to. + - You must quote the value to ensure retaining the leading zeros. + type: str + required: true + user: + description: The required username for the SAP system. + type: str + required: true + passwd: + description: The required password for the SAP system. + type: str + required: true + lang: + description: The used language to execute. + type: str + required: false + +requirements: + - pyrfc >= 2.4.0 + +author: + - Sean Freeman (@seanfreeman) + - Rainer Leber (@rainerleber) +''' + +EXAMPLES = ''' +- name: test the pyrfc module + community.sap_libs.sap_pyrfc: + function: STFC_CONNECTION + parameters: + REQUTEXT: "Hello SAP!" + connection: + ashost: s4hana.poc.cloud + sysid: TDT + sysnr: "01" + client: "400" + user: DDIC + passwd: Password1 + lang: EN +''' + +RETURN = r''' +result: + description: The execution description. + type: dict + returned: always + sample: {"ECHOTEXT": "Hello SAP!", + "RESPTEXT": "SAP R/3 Rel. 756 Sysid: TST Date: 20220710 Time: 140717 Logon_Data: 000/DDIC/E"} +''' + +import traceback +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ..module_utils.pyrfc_handler import get_connection + +try: + from pyrfc import ABAPApplicationError, ABAPRuntimeError, CommunicationError, Connection, LogonError +except ImportError: + HAS_PYRFC_LIBRARY = False + PYRFC_LIBRARY_IMPORT_ERROR = traceback.format_exc() +else: + PYRFC_LIBRARY_IMPORT_ERROR = None + HAS_PYRFC_LIBRARY = True + + +def main(): + msg = None + params_spec = dict( + ashost=dict(type='str', required=True), + sysid=dict(type='str', required=False), + sysnr=dict(type='str', required=True), + client=dict(type='str', required=True), + user=dict(type='str', required=True), + passwd=dict(type='str', required=True, no_log=True), + lang=dict(type='str', required=False), + ) + + argument_spec = dict(function=dict(required=True, type='str'), + parameters=dict(required=True, type='dict'), + connection=dict( + required=True, type='dict', options=params_spec), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + function = module.params.get('function') + func_params = module.params.get('parameters') + conn_params = module.params.get('connection') + + if not HAS_PYRFC_LIBRARY: + module.fail_json( + msg=missing_required_lib('pyrfc'), + exception=PYRFC_LIBRARY_IMPORT_ERROR) + + # Check mode + if module.check_mode: + msg = "function: %s; params: %s; login: %s" % ( + function, func_params, conn_params) + module.exit_json(msg=msg, changed=True) + + try: + conn = get_connection(module, conn_params) + result = conn.call(function, **func_params) + error_msg = None + except CommunicationError as err: + msg = "Could not connect to server" + error_msg = err.message + except LogonError as err: + msg = "Could not log in" + error_msg = err.message + except (ABAPApplicationError, ABAPRuntimeError) as err: + msg = "ABAP error occurred" + error_msg = err.message + except Exception as err: + msg = "Something went wrong." + error_msg = err + else: + module.exit_json(changed=True, result=result) + + if msg: + module.fail_json(msg=msg, exception=error_msg) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/sap_libs/plugins/modules/sap_snote.py b/ansible_collections/community/sap_libs/plugins/modules/sap_snote.py new file mode 100644 index 00000000..b97e9a25 --- /dev/null +++ b/ansible_collections/community/sap_libs/plugins/modules/sap_snote.py @@ -0,0 +1,267 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> <rainer.leber@sva.de> +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +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_libs.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_libs.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: + ANOTHER_LIBRARY_IMPORT_ERROR = None + 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_libs/plugins/modules/sap_system_facts.py b/ansible_collections/community/sap_libs/plugins/modules/sap_system_facts.py new file mode 100644 index 00000000..82e7c0a8 --- /dev/null +++ b/ansible_collections/community/sap_libs/plugins/modules/sap_system_facts.py @@ -0,0 +1,213 @@ +#!/usr/bin/python + +# Copyright: (c) 2022, Rainer Leber rainerleber@gmail.com> +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +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_libs.sap_system_facts: +''' + +RETURN = r''' +# These are examples of possible return values, +# and in general should use other names for return values. +ansible_facts: + description: Facts about the running SAP systems. + returned: always + type: dict + contains: + sap: + description: Facts about the running SAP systems. + 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_libs/plugins/modules/sap_task_list_execute.py b/ansible_collections/community/sap_libs/plugins/modules/sap_task_list_execute.py new file mode 100644 index 00000000..f46a5d6f --- /dev/null +++ b/ansible_collections/community/sap_libs/plugins/modules/sap_task_list_execute.py @@ -0,0 +1,350 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +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_libs.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_libs.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_libs.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_libs.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: + PYRFC_LIBRARY_IMPORT_ERROR = None + HAS_PYRFC_LIBRARY = True +try: + import xmltodict +except ImportError: + HAS_XMLTODICT_LIBRARY = False + XMLTODICT_LIBRARY_IMPORT_ERROR = traceback.format_exc() +else: + XMLTODICT_LIBRARY_IMPORT_ERROR = None + 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_libs/plugins/modules/sap_user.py b/ansible_collections/community/sap_libs/plugins/modules/sap_user.py new file mode 100644 index 00000000..93d465b2 --- /dev/null +++ b/ansible_collections/community/sap_libs/plugins/modules/sap_user.py @@ -0,0 +1,508 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> <rainer.leber@sva.de> +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +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_libs.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_libs.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_libs.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_libs.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_libs.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: + PYRFC_LIBRARY_IMPORT_ERROR = None + 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_libs/plugins/modules/sapcar_extract.py b/ansible_collections/community/sap_libs/plugins/modules/sapcar_extract.py new file mode 100644 index 00000000..4a1ed9ba --- /dev/null +++ b/ansible_collections/community/sap_libs/plugins/modules/sapcar_extract.py @@ -0,0 +1,228 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: sapcar_extract +short_description: Manages SAP SAPCAR archives +version_added: "1.0.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_libs.sapcar_extract: + path: "~/source/hana.sar" + +- name: Extract SAR file with destination + community.sap_libs.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_libs.sapcar_extract: + path: "~/source/hana.sar" + dest: "~/dest/" + binary_path: "https://myserver/SAPCAR" + +- name: Extract SAR file and delete SAR after extract + community.sap_libs.sapcar_extract: + path: "~/source/hana.sar" + remove: true + +- name: Extract SAR file with manifest + community.sap_libs.sapcar_extract: + path: "~/source/hana.sar" + signature: true + +- name: Extract SAR file with manifest and rename it + community.sap_libs.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() |