summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/sap_libs/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'ansible_collections/community/sap_libs/plugins')
-rw-r--r--ansible_collections/community/sap_libs/plugins/doc_fragments/__init__.py0
-rw-r--r--ansible_collections/community/sap_libs/plugins/module_utils/__init__.py0
-rw-r--r--ansible_collections/community/sap_libs/plugins/module_utils/pyrfc_handler.py48
-rwxr-xr-xansible_collections/community/sap_libs/plugins/module_utils/swpm2_parameters_inifile_generate.py271
-rw-r--r--ansible_collections/community/sap_libs/plugins/modules/sap_company.py335
-rw-r--r--ansible_collections/community/sap_libs/plugins/modules/sap_control_exec.py401
-rw-r--r--ansible_collections/community/sap_libs/plugins/modules/sap_hdbsql.py246
-rw-r--r--ansible_collections/community/sap_libs/plugins/modules/sap_pyrfc.py187
-rw-r--r--ansible_collections/community/sap_libs/plugins/modules/sap_snote.py267
-rw-r--r--ansible_collections/community/sap_libs/plugins/modules/sap_system_facts.py213
-rw-r--r--ansible_collections/community/sap_libs/plugins/modules/sap_task_list_execute.py350
-rw-r--r--ansible_collections/community/sap_libs/plugins/modules/sap_user.py508
-rw-r--r--ansible_collections/community/sap_libs/plugins/modules/sapcar_extract.py228
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()