Init: mediaserver

This commit is contained in:
2023-02-08 12:13:28 +01:00
parent 848bc9739c
commit f7c23d4ba9
31914 changed files with 6175775 additions and 0 deletions

View File

@@ -0,0 +1,238 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: hana_query
short_description: Execute SQL on HANA
version_added: "0.1.0"
description: This module executes SQL statements on HANA with hdbsql.
options:
sid:
description: The system ID.
type: str
required: false
bin_path:
description: The path to the hdbsql binary.
type: str
required: false
instance:
description: The instance number.
type: str
required: true
user:
description: A dedicated username. The user could be also in hdbuserstore.
type: str
default: SYSTEM
userstore:
description: If C(true), the user must be in hdbuserstore.
type: bool
default: false
password:
description:
- The password to connect to the database.
- "B(Note:) Since the passwords have to be passed as command line arguments, I(userstore=true) should
be used whenever possible, as command line arguments can be seen by other users
on the same machine."
type: str
autocommit:
description: Autocommit the statement.
type: bool
default: true
host:
description: The Host IP address. The port can be defined as well.
type: str
database:
description: Define the database on which to connect.
type: str
encrypted:
description: Use encrypted connection.
type: bool
default: false
filepath:
description:
- One or more files each containing one SQL query to run.
- Must be a string or list containing strings.
type: list
elements: path
query:
description:
- SQL query to run.
- Must be a string or list containing strings. Please note that if you supply a string, it will be split by commas (C(,)) to a list.
It is better to supply a one-element list instead to avoid mangled input.
type: list
elements: str
notes:
- Does not support C(check_mode). Always reports that the state has changed even if no changes have been made.
author:
- Rainer Leber (@rainerleber)
'''
EXAMPLES = r'''
- name: Simple select query
community.sap.hana_query:
sid: "hdb"
instance: "01"
password: "Test123"
query: select user_name from users
- name: RUN select query with host port
community.sap.hana_query:
sid: "hdb"
instance: "01"
password: "Test123"
host: "10.10.2.4:30001"
query: select user_name from users
- name: Run several queries
community.sap.hana_query:
sid: "hdb"
instance: "01"
password: "Test123"
query:
- select user_name from users
- select * from SYSTEM
host: "localhost"
autocommit: False
- name: Run several queries with path
community.sap.hana_query:
bin_path: "/usr/sap/HDB/HDB01/exe/hdbsql"
instance: "01"
password: "Test123"
query:
- select user_name from users
- select * from users
host: "localhost"
autocommit: False
- name: Run several queries from file
community.sap.hana_query:
sid: "hdb"
instance: "01"
password: "Test123"
filepath:
- /tmp/HANA_CPU_UtilizationPerCore_2.00.020+.txt
- /tmp/HANA.txt
host: "localhost"
- name: Run several queries from user store
community.sap.hana_query:
sid: "hdb"
instance: "01"
user: hdbstoreuser
userstore: true
query:
- select user_name from users
- select * from users
autocommit: False
'''
RETURN = r'''
query_result:
description: List containing results of all queries executed (one sublist for every query).
returned: on success
type: list
elements: list
sample: [[{"Column": "Value1"}, {"Column": "Value2"}], [{"Column": "Value1"}, {"Column": "Value2"}]]
'''
import csv
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import StringIO
from ansible.module_utils.common.text.converters import to_native
def csv_to_list(rawcsv):
reader_raw = csv.DictReader(StringIO(rawcsv))
reader = [dict((k, v.strip()) for k, v in row.items()) for row in reader_raw]
return list(reader)
def main():
module = AnsibleModule(
argument_spec=dict(
sid=dict(type='str', required=False),
bin_path=dict(type='str', required=False),
instance=dict(type='str', required=True),
encrypted=dict(type='bool', default=False),
host=dict(type='str', required=False),
user=dict(type='str', default="SYSTEM"),
userstore=dict(type='bool', default=False),
password=dict(type='str', no_log=True),
database=dict(type='str', required=False),
query=dict(type='list', elements='str', required=False),
filepath=dict(type='list', elements='path', required=False),
autocommit=dict(type='bool', default=True),
),
required_one_of=[('query', 'filepath'), ('sid', 'instance')],
required_if=[('userstore', False, ['password'])],
supports_check_mode=False,
)
rc, out, err, out_raw = [0, [], "", ""]
params = module.params
sid = params['sid']
bin_path = params['bin_path']
instance = params['instance']
user = params['user']
userstore = params['userstore']
password = params['password']
autocommit = params['autocommit']
host = params['host']
database = params['database']
encrypted = params['encrypted']
filepath = params['filepath']
query = params['query']
if bin_path is None:
bin_path = "/usr/sap/{sid}/HDB{instance}/exe/hdbsql".format(sid=sid.upper(), instance=instance)
try:
command = [module.get_bin_path(bin_path, required=True)]
except Exception as e:
module.fail_json(msg='Failed to find hdbsql at the expected path "{0}".Please check SID and instance number: "{1}"'.format(bin_path, to_native(e)))
if encrypted is True:
command.extend(['-attemptencrypt'])
if autocommit is False:
command.extend(['-z'])
if host is not None:
command.extend(['-n', host])
if database is not None:
command.extend(['-d', database])
# -x Suppresses additional output, such as the number of selected rows in a result set.
if userstore:
command.extend(['-x', '-U', user])
else:
command.extend(['-x', '-i', instance, '-u', user, '-p', password])
if filepath is not None:
command.extend(['-I'])
for p in filepath:
# makes a command like hdbsql -i 01 -u SYSTEM -p secret123# -I /tmp/HANA_CPU_UtilizationPerCore_2.00.020+.txt,
# iterates through files and append the output to var out.
query_command = command + [p]
(rc, out_raw, err) = module.run_command(query_command)
out.append(csv_to_list(out_raw))
if query is not None:
for q in query:
# makes a command like hdbsql -i 01 -u SYSTEM -p secret123# "select user_name from users",
# iterates through multiple commands and append the output to var out.
query_command = command + [q]
(rc, out_raw, err) = module.run_command(query_command)
out.append(csv_to_list(out_raw))
changed = True
module.exit_json(changed=changed, rc=rc, query_result=out, stderr=err)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,220 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: sapcar_extract
short_description: Manages SAP SAPCAR archives
version_added: "0.1.0"
description:
- Provides support for unpacking C(sar)/C(car) files with the SAPCAR binary from SAP and pulling
information back into Ansible.
options:
path:
description: The path to the SAR/CAR file.
type: path
required: true
dest:
description:
- The destination where SAPCAR extracts the SAR file. Missing folders will be created.
If this parameter is not provided, it will unpack in the same folder as the SAR file.
type: path
binary_path:
description:
- The path to the SAPCAR binary, for example, C(/home/dummy/sapcar) or C(https://myserver/SAPCAR).
If this parameter is not provided, the module will look in C(PATH).
type: path
signature:
description:
- If C(true), the signature will be extracted.
default: false
type: bool
security_library:
description:
- The path to the security library, for example, C(/usr/sap/hostctrl/exe/libsapcrytp.so), for signature operations.
type: path
manifest:
description:
- The name of the manifest.
default: "SIGNATURE.SMF"
type: str
remove:
description:
- If C(true), the SAR/CAR file will be removed. B(This should be used with caution!)
default: false
type: bool
author:
- Rainer Leber (@RainerLeber)
notes:
- Always returns C(changed=true) in C(check_mode).
'''
EXAMPLES = r"""
- name: Extract SAR file
community.sap.sapcar_extract:
path: "~/source/hana.sar"
- name: Extract SAR file with destination
community.sap.sapcar_extract:
path: "~/source/hana.sar"
dest: "~/test/"
- name: Extract SAR file with destination and download from webserver can be a fileshare as well
community.sap.sapcar_extract:
path: "~/source/hana.sar"
dest: "~/dest/"
binary_path: "https://myserver/SAPCAR"
- name: Extract SAR file and delete SAR after extract
community.sap.sapcar_extract:
path: "~/source/hana.sar"
remove: true
- name: Extract SAR file with manifest
community.sap.sapcar_extract:
path: "~/source/hana.sar"
signature: true
- name: Extract SAR file with manifest and rename it
community.sap.sapcar_extract:
path: "~/source/hana.sar"
manifest: "MyNewSignature.SMF"
signature: true
"""
import os
from tempfile import NamedTemporaryFile
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.urls import open_url
from ansible.module_utils.common.text.converters import to_native
def get_list_of_files(dir_name):
# create a list of file and directories
# names in the given directory
list_of_file = os.listdir(dir_name)
allFiles = list()
# Iterate over all the entries
for entry in list_of_file:
# Create full path
fullPath = os.path.join(dir_name, entry)
# If entry is a directory then get the list of files in this directory
if os.path.isdir(fullPath):
allFiles = allFiles + [fullPath]
allFiles = allFiles + get_list_of_files(fullPath)
else:
allFiles.append(fullPath)
return allFiles
def download_SAPCAR(binary_path, module):
bin_path = None
# download sapcar binary if url is provided otherwise path is returned
if binary_path is not None:
if binary_path.startswith('https://') or binary_path.startswith('http://'):
random_file = NamedTemporaryFile(delete=False)
with open_url(binary_path) as response:
with random_file as out_file:
data = response.read()
out_file.write(data)
os.chmod(out_file.name, 0o700)
bin_path = out_file.name
module.add_cleanup_file(bin_path)
else:
bin_path = binary_path
return bin_path
def check_if_present(command, path, dest, signature, manifest, module):
# manipulating output from SAR file for compare with already extracted files
iter_command = [command, '-tvf', path]
sar_out = module.run_command(iter_command)[1]
sar_raw = sar_out.split("\n")[1:]
if dest[-1] != "/":
dest = dest + "/"
sar_files = [dest + x.split(" ")[-1] for x in sar_raw if x]
# remove any SIGNATURE.SMF from list because it will not unpacked if signature is false
if not signature:
sar_files = [item for item in sar_files if not item.endswith('.SMF')]
# if signature is renamed manipulate files in list of sar file for compare.
if manifest != "SIGNATURE.SMF":
sar_files = [item for item in sar_files if not item.endswith('.SMF')]
sar_files = sar_files + [manifest]
# get extracted files if present
files_extracted = get_list_of_files(dest)
# compare extracted files with files in sar file
present = all(elem in files_extracted for elem in sar_files)
return present
def main():
module = AnsibleModule(
argument_spec=dict(
path=dict(type='path', required=True),
dest=dict(type='path'),
binary_path=dict(type='path'),
signature=dict(type='bool', default=False),
security_library=dict(type='path'),
manifest=dict(type='str', default="SIGNATURE.SMF"),
remove=dict(type='bool', default=False),
),
supports_check_mode=True,
)
rc, out, err = [0, "", ""]
params = module.params
check_mode = module.check_mode
path = params['path']
dest = params['dest']
signature = params['signature']
security_library = params['security_library']
manifest = params['manifest']
remove = params['remove']
bin_path = download_SAPCAR(params['binary_path'], module)
if dest is None:
dest_head_tail = os.path.split(path)
dest = dest_head_tail[0] + '/'
else:
if not os.path.exists(dest):
os.makedirs(dest, 0o755)
if bin_path is not None:
command = [module.get_bin_path(bin_path, required=True)]
else:
try:
command = [module.get_bin_path('sapcar', required=True)]
except Exception as e:
module.fail_json(msg='Failed to find SAPCAR at the expected path or URL "{0}". Please check whether it is available: {1}'
.format(bin_path, to_native(e)))
present = check_if_present(command[0], path, dest, signature, manifest, module)
if not present:
command.extend(['-xvf', path, '-R', dest])
if security_library:
command.extend(['-L', security_library])
if signature:
command.extend(['-manifest', manifest])
if not check_mode:
(rc, out, err) = module.run_command(command, check_rc=True)
changed = True
else:
changed = False
out = "already unpacked"
if remove:
os.remove(path)
module.exit_json(changed=changed, message=rc, stdout=out,
stderr=err, command=' '.join(command))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,238 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: hana_query
short_description: Execute SQL on HANA
version_added: "0.1.0"
description: This module executes SQL statements on HANA with hdbsql.
options:
sid:
description: The system ID.
type: str
required: false
bin_path:
description: The path to the hdbsql binary.
type: str
required: false
instance:
description: The instance number.
type: str
required: true
user:
description: A dedicated username. The user could be also in hdbuserstore.
type: str
default: SYSTEM
userstore:
description: If C(true), the user must be in hdbuserstore.
type: bool
default: false
password:
description:
- The password to connect to the database.
- "B(Note:) Since the passwords have to be passed as command line arguments, I(userstore=true) should
be used whenever possible, as command line arguments can be seen by other users
on the same machine."
type: str
autocommit:
description: Autocommit the statement.
type: bool
default: true
host:
description: The Host IP address. The port can be defined as well.
type: str
database:
description: Define the database on which to connect.
type: str
encrypted:
description: Use encrypted connection.
type: bool
default: false
filepath:
description:
- One or more files each containing one SQL query to run.
- Must be a string or list containing strings.
type: list
elements: path
query:
description:
- SQL query to run.
- Must be a string or list containing strings. Please note that if you supply a string, it will be split by commas (C(,)) to a list.
It is better to supply a one-element list instead to avoid mangled input.
type: list
elements: str
notes:
- Does not support C(check_mode). Always reports that the state has changed even if no changes have been made.
author:
- Rainer Leber (@rainerleber)
'''
EXAMPLES = r'''
- name: Simple select query
community.sap.hana_query:
sid: "hdb"
instance: "01"
password: "Test123"
query: select user_name from users
- name: RUN select query with host port
community.sap.hana_query:
sid: "hdb"
instance: "01"
password: "Test123"
host: "10.10.2.4:30001"
query: select user_name from users
- name: Run several queries
community.sap.hana_query:
sid: "hdb"
instance: "01"
password: "Test123"
query:
- select user_name from users
- select * from SYSTEM
host: "localhost"
autocommit: False
- name: Run several queries with path
community.sap.hana_query:
bin_path: "/usr/sap/HDB/HDB01/exe/hdbsql"
instance: "01"
password: "Test123"
query:
- select user_name from users
- select * from users
host: "localhost"
autocommit: False
- name: Run several queries from file
community.sap.hana_query:
sid: "hdb"
instance: "01"
password: "Test123"
filepath:
- /tmp/HANA_CPU_UtilizationPerCore_2.00.020+.txt
- /tmp/HANA.txt
host: "localhost"
- name: Run several queries from user store
community.sap.hana_query:
sid: "hdb"
instance: "01"
user: hdbstoreuser
userstore: true
query:
- select user_name from users
- select * from users
autocommit: False
'''
RETURN = r'''
query_result:
description: List containing results of all queries executed (one sublist for every query).
returned: on success
type: list
elements: list
sample: [[{"Column": "Value1"}, {"Column": "Value2"}], [{"Column": "Value1"}, {"Column": "Value2"}]]
'''
import csv
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import StringIO
from ansible.module_utils.common.text.converters import to_native
def csv_to_list(rawcsv):
reader_raw = csv.DictReader(StringIO(rawcsv))
reader = [dict((k, v.strip()) for k, v in row.items()) for row in reader_raw]
return list(reader)
def main():
module = AnsibleModule(
argument_spec=dict(
sid=dict(type='str', required=False),
bin_path=dict(type='str', required=False),
instance=dict(type='str', required=True),
encrypted=dict(type='bool', default=False),
host=dict(type='str', required=False),
user=dict(type='str', default="SYSTEM"),
userstore=dict(type='bool', default=False),
password=dict(type='str', no_log=True),
database=dict(type='str', required=False),
query=dict(type='list', elements='str', required=False),
filepath=dict(type='list', elements='path', required=False),
autocommit=dict(type='bool', default=True),
),
required_one_of=[('query', 'filepath'), ('sid', 'instance')],
required_if=[('userstore', False, ['password'])],
supports_check_mode=False,
)
rc, out, err, out_raw = [0, [], "", ""]
params = module.params
sid = params['sid']
bin_path = params['bin_path']
instance = params['instance']
user = params['user']
userstore = params['userstore']
password = params['password']
autocommit = params['autocommit']
host = params['host']
database = params['database']
encrypted = params['encrypted']
filepath = params['filepath']
query = params['query']
if bin_path is None:
bin_path = "/usr/sap/{sid}/HDB{instance}/exe/hdbsql".format(sid=sid.upper(), instance=instance)
try:
command = [module.get_bin_path(bin_path, required=True)]
except Exception as e:
module.fail_json(msg='Failed to find hdbsql at the expected path "{0}".Please check SID and instance number: "{1}"'.format(bin_path, to_native(e)))
if encrypted is True:
command.extend(['-attemptencrypt'])
if autocommit is False:
command.extend(['-z'])
if host is not None:
command.extend(['-n', host])
if database is not None:
command.extend(['-d', database])
# -x Suppresses additional output, such as the number of selected rows in a result set.
if userstore:
command.extend(['-x', '-U', user])
else:
command.extend(['-x', '-i', instance, '-u', user, '-p', password])
if filepath is not None:
command.extend(['-I'])
for p in filepath:
# makes a command like hdbsql -i 01 -u SYSTEM -p secret123# -I /tmp/HANA_CPU_UtilizationPerCore_2.00.020+.txt,
# iterates through files and append the output to var out.
query_command = command + [p]
(rc, out_raw, err) = module.run_command(query_command)
out.append(csv_to_list(out_raw))
if query is not None:
for q in query:
# makes a command like hdbsql -i 01 -u SYSTEM -p secret123# "select user_name from users",
# iterates through multiple commands and append the output to var out.
query_command = command + [q]
(rc, out_raw, err) = module.run_command(query_command)
out.append(csv_to_list(out_raw))
changed = True
module.exit_json(changed=changed, rc=rc, query_result=out, stderr=err)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,326 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> <rainer.leber@sva.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: sap_company
short_description: This module will manage a company entities in a SAP S4HANA environment
version_added: "1.0.0"
description:
- The M(community.sap.sap_user) module depends on C(pyrfc) Python library (version 2.4.0 and upwards).
Depending on distribution you are using, you may need to install additional packages to
have these available.
- This module will use the company BAPIs C(BAPI_COMPANY_CLONE) and C(BAPI_COMPANY_DELETE) to manage company entities.
options:
state:
description:
- The decision what to do with the company.
default: 'present'
choices:
- 'present'
- 'absent'
required: false
type: str
conn_username:
description: The required username for the SAP system.
required: true
type: str
conn_password:
description: The required password for the SAP system.
required: true
type: str
host:
description: The required host for the SAP system. Can be either an FQDN or IP Address.
required: true
type: str
sysnr:
description:
- The system number of the SAP system.
- You must quote the value to ensure retaining the leading zeros.
required: false
default: '01'
type: str
client:
description:
- The client number to connect to.
- You must quote the value to ensure retaining the leading zeros.
required: false
default: '000'
type: str
company_id:
description: The company id.
required: true
type: str
name:
description: The company name.
required: false
type: str
name_2:
description: Additional company name.
required: false
type: str
country:
description: The country code for the company. For example, C('DE').
required: false
type: str
time_zone:
description: The timezone.
required: false
type: str
city:
description: The city where the company is located.
required: false
type: str
post_code:
description: The post code from the city.
required: false
type: str
street:
description: Street where the company is located.
required: false
type: str
street_no:
description: Street number.
required: false
type: str
e_mail:
description: General E-Mail address.
required: false
type: str
requirements:
- pyrfc >= 2.4.0
author:
- Rainer Leber (@rainerleber)
notes:
- Does not support C(check_mode).
'''
EXAMPLES = r'''
- name: Create SAP Company
community.sap.sap_company:
conn_username: 'DDIC'
conn_password: 'HECtna2021#'
host: 100.0.201.20
sysnr: '01'
client: '000'
state: present
company_id: "Comp_ID"
name: "Test_comp"
name_2: "LTD"
country: "DE"
time_zone: "UTC"
city: "City"
post_code: "12345"
street: "test_street"
street_no: "1"
e_mail: "test@test.de"
# pass in a message and have changed true
- name: Delete SAP Company
community.sap.sap_company:
conn_username: 'DDIC'
conn_password: 'HECtna2021#'
host: 100.0.201.20
sysnr: '01'
client: '000'
state: absent
company_id: "Comp_ID"
name: "Test_comp"
name_2: "LTD"
country: "DE"
time_zone: "UTC"
city: "City"
post_code: "12345"
street: "test_street"
street_no: "1"
e_mail: "test@test.de"
'''
RETURN = r'''
# These are examples of possible return values, and in general should use other names for return values.
msg:
description: A small execution description.
type: str
returned: always
sample: 'Company address COMP_ID created'
out:
description: A complete description of the executed tasks. If this is available.
type: list
elements: dict
returned: always
sample: '{
"RETURN": [
{
"FIELD": "",
"ID": "01",
"LOG_MSG_NO": "000000",
"LOG_NO": "",
"MESSAGE": "Company address COMP_ID created",
"MESSAGE_V1": "COMP_ID",
"MESSAGE_V2": "",
"MESSAGE_V3": "",
"MESSAGE_V4": "",
"NUMBER": "078",
"PARAMETER": "",
"ROW": 0,
"SYSTEM": "",
"TYPE": "S"
}
]
}
}'
'''
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
import traceback
try:
from pyrfc import Connection
except ImportError:
HAS_PYRFC_LIBRARY = False
ANOTHER_LIBRARY_IMPORT_ERROR = traceback.format_exc()
else:
HAS_PYRFC_LIBRARY = True
def call_rfc_method(connection, method_name, kwargs):
# PyRFC call function
return connection.call(method_name, **kwargs)
def build_company_params(name, name_2, country, time_zone, city, post_code, street, street_no, e_mail):
# Creates RFC parameters for creating organizations
# define dicts in batch
params = dict()
# define company name
params['NAME'] = name
params['NAME_2'] = name_2
# define location
params['COUNTRY'] = country
params['TIME_ZONE'] = time_zone
params['CITY'] = city
params['POSTL_COD1'] = post_code
params['STREET'] = street
params['STREET_NO'] = street_no
# define communication
params['E_MAIL'] = e_mail
# return dict
return params
def return_analysis(raw):
change = False
failed = False
msg = raw['RETURN'][0]['MESSAGE']
for state in raw['RETURN']:
if state['TYPE'] == "E":
if state['NUMBER'] == '081':
change = False
else:
failed = True
if state['TYPE'] == "S":
if state['NUMBER'] != '079':
change = True
else:
msg = "No changes where made."
return [{"change": change}, {"failed": failed}, {"msg": msg}]
def run_module():
module = AnsibleModule(
argument_spec=dict(
state=dict(default='present', choices=['absent', 'present']),
conn_username=dict(type='str', required=True),
conn_password=dict(type='str', required=True, no_log=True),
host=dict(type='str', required=True),
sysnr=dict(type='str', default="01"),
client=dict(type='str', default="000"),
company_id=dict(type='str', required=True),
name=dict(type='str', required=False),
name_2=dict(type='str', required=False),
country=dict(type='str', required=False),
time_zone=dict(type='str', required=False),
city=dict(type='str', required=False),
post_code=dict(type='str', required=False),
street=dict(type='str', required=False),
street_no=dict(type='str', required=False),
e_mail=dict(type='str', required=False),
),
supports_check_mode=False,
)
result = dict(changed=False, msg='', out={})
raw = ""
params = module.params
state = params['state']
conn_username = (params['conn_username']).upper()
conn_password = params['conn_password']
host = params['host']
sysnr = params['sysnr']
client = params['client']
company_id = (params['company_id']).upper()
name = params['name']
name_2 = params['name_2']
country = params['country']
time_zone = params['time_zone']
city = params['city']
post_code = params['post_code']
street = params['street']
street_no = params['street_no']
e_mail = params['e_mail']
if not HAS_PYRFC_LIBRARY:
module.fail_json(
msg=missing_required_lib('pyrfc'),
exception=ANOTHER_LIBRARY_IMPORT_ERROR)
# basic RFC connection with pyrfc
try:
conn = Connection(user=conn_username, passwd=conn_password, ashost=host, sysnr=sysnr, client=client)
except Exception as err:
result['error'] = str(err)
result['msg'] = 'Something went wrong connecting to the SAP system.'
module.fail_json(**result)
# build parameter dict of dict
company_params = build_company_params(name, name_2, country, time_zone, city, post_code, street, street_no, e_mail)
if state == "absent":
raw = call_rfc_method(conn, 'BAPI_COMPANY_DELETE', {'COMPANY': company_id})
if state == "present":
raw = call_rfc_method(conn, 'BAPI_COMPANY_CLONE',
{'METHOD': {'USMETHOD': 'COMPANY_CLONE'}, 'COMPANY': company_id, 'COMP_DATA': company_params})
analysed = return_analysis(raw)
result['out'] = raw
result['changed'] = analysed[0]['change']
result['msg'] = analysed[2]['msg']
if analysed[1]['failed']:
module.fail_json(**result)
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,499 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> <rainer.leber@sva.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: sap_user
short_description: This module will manage a user entities in a SAP S4/HANA environment
version_added: "1.0.0"
description:
- The M(community.sap.sap_user) module depends on C(pyrfc) Python library (version 2.4.0 and upwards).
Depending on distribution you are using, you may need to install additional packages to
have these available.
- This module will use the following user BAPIs to manage user entities.
- C(BAPI_USER_GET_DETAIL)
- C(BAPI_USER_DELETE)
- C(BAPI_USER_CREATE1)
- C(BAPI_USER_CHANGE)
- C(BAPI_USER_ACTGROUPS_ASSIGN)
- C(BAPI_USER_PROFILES_ASSIGN)
- C(BAPI_USER_UNLOCK)
- C(BAPI_USER_LOCK)
options:
state:
description:
- The decision what to do with the user.
default: 'present'
choices:
- 'present'
- 'absent'
- 'lock'
- 'unlock'
required: false
type: str
force:
description:
- Must be C('True') if the password or type should be overwritten.
default: False
required: false
type: bool
conn_username:
description: The required username for the SAP system.
required: true
type: str
conn_password:
description: The required password for the SAP system.
required: true
type: str
host:
description: The required host for the SAP system. Can be either an FQDN or IP Address.
required: true
type: str
sysnr:
description:
- The system number of the SAP system.
- You must quote the value to ensure retaining the leading zeros.
default: '00'
type: str
client:
description:
- The client number to connect to.
- You must quote the value to ensure retaining the leading zeros.
default: '000'
type: str
username:
description:
- The username.
type: str
required: true
firstname:
description:
- The Firstname of the user in the SAP system.
type: str
required: false
lastname:
description:
- The lastname of the user in the SAP system.
type: str
required: false
email:
description:
- The email address of the user in the SAP system.
type: str
required: false
password:
description:
- The password for the user in the SAP system.
type: str
required: false
useralias:
description:
- The alias for the user in the SAP system.
type: str
required: false
user_type:
description:
- The type for the user in the SAP system.
- C('A') Dialog user, C('B') System User, C('C') Communication User,
C('S') Service User, C('L') Reference User.
- Must be in uppercase.
type: str
required: false
default: 'A'
choices: ['A', 'B', 'C', 'S', 'L']
company:
description:
- The specific company the user belongs to.
- The company name must be available in the SAP system.
type: str
required: false
profiles:
description:
- Assign profiles to the user.
- Should be in uppercase, for example C('SAP_NEW') or C('SAP_ALL').
type: list
elements: str
default: ['']
required: false
roles:
description:
- Assign roles to the user.
type: list
elements: str
default: ['']
required: false
requirements:
- pyrfc >= 2.4.0
author:
- Rainer Leber (@rainerleber)
notes:
- Does not support C(check_mode).
'''
EXAMPLES = r'''
- name: Create SAP User
community.sap.sap_user:
conn_username: 'DDIC'
conn_password: 'Test123'
host: 192.168.1.150
sysnr: '01'
client: '000'
state: present
username: ADMIN
firstname: first_admin
lastname: last_admin
email: admin@test.de
password: Test123456
useralias: ADMIN
company: DEFAULT_COMPANY
roles:
- "SAP_ALL"
- name: Force change SAP User
community.sap.sap_user:
conn_username: 'DDIC'
conn_password: 'Test123'
host: 192.168.1.150
sysnr: '01'
client: '000'
state: present
force: true
username: ADMIN
firstname: first_admin
lastname: last_admin
email: admin@test.de
password: Test123456
useralias: ADMIN
company: DEFAULT_COMPANY
roles:
- "SAP_ALL"
- name: Delete SAP User
community.sap.sap_user:
conn_username: 'DDIC'
conn_password: 'Test123'
host: 192.168.1.150
sysnr: '01'
client: '000'
state: absent
force: true
username: ADMIN
- name: Unlock SAP User
community.sap.sap_user:
conn_username: 'DDIC'
conn_password: 'Test123'
host: 192.168.1.150
sysnr: '01'
client: '000'
state: unlock
force: true
username: ADMIN
'''
RETURN = r'''
msg:
description: A small execution description about the user action.
type: str
returned: always
sample: 'User ADMIN created'
out:
description: A detailed description about the user action.
type: list
elements: dict
returned: on success
sample: [...,{
"RETURN": [
{
"FIELD": "BNAME",
"ID": "01",
"LOG_MSG_NO": "000000",
"LOG_NO": "",
"MESSAGE": "User ADMIN created",
"MESSAGE_V1": "ADMIN",
"MESSAGE_V2": "",
"MESSAGE_V3": "",
"MESSAGE_V4": "",
"NUMBER": "102",
"PARAMETER": "",
"ROW": 0,
"SYSTEM": "",
"TYPE": "S"
}
],
"SAPUSER_UUID_HIST": []}]
'''
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
import traceback
import datetime
try:
from pyrfc import Connection
except ImportError:
HAS_PYRFC_LIBRARY = False
PYRFC_LIBRARY_IMPORT_ERROR = traceback.format_exc()
else:
HAS_PYRFC_LIBRARY = True
def add_to_dict(target_dict, target_key, value):
# Adds the given value to a dict as the key
# check if the given key is in the given dict yet
if target_key in target_dict:
return False
target_dict[target_key] = value
return True
def call_rfc_method(connection, method_name, kwargs):
# PyRFC call function
return connection.call(method_name, **kwargs)
def build_rfc_user_params(username, firstname, lastname, email, raw_password,
useralias, user_type, raw_company, user_change, force):
"""Creates RFC parameters for Creating users"""
# define dicts in batch
params = dict()
address = dict()
password = dict()
alias = dict()
logondata = dict()
company = dict()
# for change parameters
addressx = dict()
passwordx = dict()
logondatax = dict()
companyx = dict()
# define username
add_to_dict(params, 'USERNAME', username)
# define Address
add_to_dict(address, 'FIRSTNAME', firstname)
add_to_dict(address, 'LASTNAME', lastname)
add_to_dict(address, 'E_MAIL', email)
# define Password
add_to_dict(password, 'BAPIPWD', raw_password)
# define Alias
add_to_dict(alias, 'USERALIAS', useralias)
# define LogonData
add_to_dict(logondata, 'GLTGV', datetime.date.today())
add_to_dict(logondata, 'GLTGB', '20991231')
add_to_dict(logondata, 'USTYP', user_type)
# define company
add_to_dict(company, 'COMPANY', raw_company)
params['LOGONDATA'] = logondata
params['ADDRESS'] = address
params['COMPANY'] = company
params['ALIAS'] = alias
params['PASSWORD'] = password
# add change if user exists
if user_change and force:
add_to_dict(addressx, 'FIRSTNAME', 'X')
add_to_dict(addressx, 'LASTNAME', 'X')
add_to_dict(addressx, 'E_MAIL', 'X')
# define Password
add_to_dict(passwordx, 'BAPIPWD', 'X')
# define LogonData
add_to_dict(logondatax, 'USTYP', 'X')
# define company
add_to_dict(companyx, 'COMPANY', 'X')
params['LOGONDATAX'] = logondatax
params['ADDRESSX'] = addressx
params['COMPANYX'] = companyx
params['PASSWORDX'] = passwordx
return params
def user_role_assignment_build_rfc_params(roles, username):
rfc_table = []
for role_name in roles:
table_row = {'AGR_NAME': role_name}
add_to_dict(table_row, 'FROM_DAT', datetime.date.today())
add_to_dict(table_row, 'TO_DAT', '20991231')
rfc_table.append(table_row)
return {
'USERNAME': username,
'ACTIVITYGROUPS': rfc_table
}
def user_profile_assignment_build_rfc_params(profiles, username):
rfc_table = []
for profile_name in profiles:
table_row = {'BAPIPROF': profile_name}
rfc_table.append(table_row)
return {
'USERNAME': username,
'PROFILES': rfc_table
}
def check_user(user_detail):
if len(user_detail['RETURN']) > 0:
for sub in user_detail['RETURN']:
if sub['NUMBER'] == '124':
return False
return True
def return_analysis(raw):
change = False
failed = False
for state in raw['RETURN']:
if state['TYPE'] == "E":
if state['NUMBER'] == '224' or state['NUMBER'] == '124':
change = False
else:
failed = True
if state['TYPE'] == "S":
if state['NUMBER'] != '029':
change = True
if state['TYPE'] == "W":
if state['NUMBER'] == '049' or state['NUMBER'] == '047':
change = True
if state['NUMBER'] == '255':
change = True
return [{"change": change}, {"failed": failed}]
def run_module():
module = AnsibleModule(
argument_spec=dict(
# logical values
state=dict(default='present', choices=[
'absent', 'present', 'lock', 'unlock']),
force=dict(type='bool', default=False),
# values for connection
conn_username=dict(type='str', required=True),
conn_password=dict(type='str', required=True, no_log=True),
host=dict(type='str', required=True),
sysnr=dict(type='str', default="00"),
client=dict(type='str', default="000"),
# values for the new or existing user
username=dict(type='str', required=True),
firstname=dict(type='str', required=False),
lastname=dict(type='str', required=False),
email=dict(type='str', required=False),
password=dict(type='str', required=False, no_log=True),
useralias=dict(type='str', required=False),
user_type=dict(default="A",
choices=['A', 'B', 'C', 'S', 'L']),
company=dict(type='str', required=False),
# values for profile must a list
# Example ["SAP_NEW", "SAP_ALL"]
profiles=dict(type='list', elements='str', default=[""]),
# values for roles must a list
roles=dict(type='list', elements='str', default=[""]),
),
supports_check_mode=False,
required_if=[('state', 'present', ['useralias', 'company'])]
)
result = dict(changed=False, msg='', out='')
count = 0
raw = ""
params = module.params
state = params['state']
conn_username = (params['conn_username']).upper()
conn_password = params['conn_password']
host = params['host']
sysnr = params['sysnr']
client = params['client']
username = (params['username']).upper()
firstname = params['firstname']
lastname = params['lastname']
email = params['email']
password = params['password']
force = params['force']
if not params['useralias'] is None:
useralias = (params['useralias']).upper()
user_type = (params['user_type']).upper()
company = params['company']
profiles = params['profiles']
roles = params['roles']
if not HAS_PYRFC_LIBRARY:
module.fail_json(
msg=missing_required_lib('pyrfc'),
exception=PYRFC_LIBRARY_IMPORT_ERROR)
# basic RFC connection with pyrfc
try:
conn = Connection(user=conn_username, passwd=conn_password, ashost=host, sysnr=sysnr, client=client)
except Exception as err:
result['error'] = str(err)
result['msg'] = 'Something went wrong connecting to the SAP system.'
module.fail_json(**result)
# user details
user_detail = call_rfc_method(conn, 'BAPI_USER_GET_DETAIL', {'USERNAME': username})
user_exists = check_user(user_detail)
if state == "absent":
if user_exists:
raw = call_rfc_method(conn, 'BAPI_USER_DELETE', {'USERNAME': username})
if state == "present":
user_params = build_rfc_user_params(username, firstname, lastname, email, password, useralias, user_type, company, user_exists, force)
if not user_exists:
raw = call_rfc_method(conn, 'BAPI_USER_CREATE1', user_params)
if user_exists:
# check for address changes when user exists
user_no_changes = all((user_detail.get('ADDRESS')).get(k) == v for k, v in (user_params.get('ADDRESS')).items())
if not user_no_changes or force:
raw = call_rfc_method(conn, 'BAPI_USER_CHANGE', user_params)
call_rfc_method(conn, 'BAPI_USER_ACTGROUPS_ASSIGN', user_role_assignment_build_rfc_params(roles, username))
call_rfc_method(conn, 'BAPI_USER_PROFILES_ASSIGN', user_profile_assignment_build_rfc_params(profiles, username))
if state == "unlock":
if user_exists:
raw = call_rfc_method(conn, 'BAPI_USER_UNLOCK', {'USERNAME': username})
if state == "lock":
if user_exists:
raw = call_rfc_method(conn, 'BAPI_USER_LOCK', {'USERNAME': username})
# analyse return value
if raw != '':
analysed = return_analysis(raw)
result['out'] = raw
result['changed'] = analysed[0]['change']
for msgs in raw['RETURN']:
if count > 0:
result['msg'] = result['msg'] + '\n'
result['msg'] = result['msg'] + msgs['MESSAGE']
count = count + 1
if analysed[1]['failed']:
module.fail_json(**result)
else:
result['msg'] = "No changes where made."
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,326 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> <rainer.leber@sva.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: sap_company
short_description: This module will manage a company entities in a SAP S4HANA environment
version_added: "1.0.0"
description:
- The M(community.sap.sap_user) module depends on C(pyrfc) Python library (version 2.4.0 and upwards).
Depending on distribution you are using, you may need to install additional packages to
have these available.
- This module will use the company BAPIs C(BAPI_COMPANY_CLONE) and C(BAPI_COMPANY_DELETE) to manage company entities.
options:
state:
description:
- The decision what to do with the company.
default: 'present'
choices:
- 'present'
- 'absent'
required: false
type: str
conn_username:
description: The required username for the SAP system.
required: true
type: str
conn_password:
description: The required password for the SAP system.
required: true
type: str
host:
description: The required host for the SAP system. Can be either an FQDN or IP Address.
required: true
type: str
sysnr:
description:
- The system number of the SAP system.
- You must quote the value to ensure retaining the leading zeros.
required: false
default: '01'
type: str
client:
description:
- The client number to connect to.
- You must quote the value to ensure retaining the leading zeros.
required: false
default: '000'
type: str
company_id:
description: The company id.
required: true
type: str
name:
description: The company name.
required: false
type: str
name_2:
description: Additional company name.
required: false
type: str
country:
description: The country code for the company. For example, C('DE').
required: false
type: str
time_zone:
description: The timezone.
required: false
type: str
city:
description: The city where the company is located.
required: false
type: str
post_code:
description: The post code from the city.
required: false
type: str
street:
description: Street where the company is located.
required: false
type: str
street_no:
description: Street number.
required: false
type: str
e_mail:
description: General E-Mail address.
required: false
type: str
requirements:
- pyrfc >= 2.4.0
author:
- Rainer Leber (@rainerleber)
notes:
- Does not support C(check_mode).
'''
EXAMPLES = r'''
- name: Create SAP Company
community.sap.sap_company:
conn_username: 'DDIC'
conn_password: 'HECtna2021#'
host: 100.0.201.20
sysnr: '01'
client: '000'
state: present
company_id: "Comp_ID"
name: "Test_comp"
name_2: "LTD"
country: "DE"
time_zone: "UTC"
city: "City"
post_code: "12345"
street: "test_street"
street_no: "1"
e_mail: "test@test.de"
# pass in a message and have changed true
- name: Delete SAP Company
community.sap.sap_company:
conn_username: 'DDIC'
conn_password: 'HECtna2021#'
host: 100.0.201.20
sysnr: '01'
client: '000'
state: absent
company_id: "Comp_ID"
name: "Test_comp"
name_2: "LTD"
country: "DE"
time_zone: "UTC"
city: "City"
post_code: "12345"
street: "test_street"
street_no: "1"
e_mail: "test@test.de"
'''
RETURN = r'''
# These are examples of possible return values, and in general should use other names for return values.
msg:
description: A small execution description.
type: str
returned: always
sample: 'Company address COMP_ID created'
out:
description: A complete description of the executed tasks. If this is available.
type: list
elements: dict
returned: always
sample: '{
"RETURN": [
{
"FIELD": "",
"ID": "01",
"LOG_MSG_NO": "000000",
"LOG_NO": "",
"MESSAGE": "Company address COMP_ID created",
"MESSAGE_V1": "COMP_ID",
"MESSAGE_V2": "",
"MESSAGE_V3": "",
"MESSAGE_V4": "",
"NUMBER": "078",
"PARAMETER": "",
"ROW": 0,
"SYSTEM": "",
"TYPE": "S"
}
]
}
}'
'''
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
import traceback
try:
from pyrfc import Connection
except ImportError:
HAS_PYRFC_LIBRARY = False
ANOTHER_LIBRARY_IMPORT_ERROR = traceback.format_exc()
else:
HAS_PYRFC_LIBRARY = True
def call_rfc_method(connection, method_name, kwargs):
# PyRFC call function
return connection.call(method_name, **kwargs)
def build_company_params(name, name_2, country, time_zone, city, post_code, street, street_no, e_mail):
# Creates RFC parameters for creating organizations
# define dicts in batch
params = dict()
# define company name
params['NAME'] = name
params['NAME_2'] = name_2
# define location
params['COUNTRY'] = country
params['TIME_ZONE'] = time_zone
params['CITY'] = city
params['POSTL_COD1'] = post_code
params['STREET'] = street
params['STREET_NO'] = street_no
# define communication
params['E_MAIL'] = e_mail
# return dict
return params
def return_analysis(raw):
change = False
failed = False
msg = raw['RETURN'][0]['MESSAGE']
for state in raw['RETURN']:
if state['TYPE'] == "E":
if state['NUMBER'] == '081':
change = False
else:
failed = True
if state['TYPE'] == "S":
if state['NUMBER'] != '079':
change = True
else:
msg = "No changes where made."
return [{"change": change}, {"failed": failed}, {"msg": msg}]
def run_module():
module = AnsibleModule(
argument_spec=dict(
state=dict(default='present', choices=['absent', 'present']),
conn_username=dict(type='str', required=True),
conn_password=dict(type='str', required=True, no_log=True),
host=dict(type='str', required=True),
sysnr=dict(type='str', default="01"),
client=dict(type='str', default="000"),
company_id=dict(type='str', required=True),
name=dict(type='str', required=False),
name_2=dict(type='str', required=False),
country=dict(type='str', required=False),
time_zone=dict(type='str', required=False),
city=dict(type='str', required=False),
post_code=dict(type='str', required=False),
street=dict(type='str', required=False),
street_no=dict(type='str', required=False),
e_mail=dict(type='str', required=False),
),
supports_check_mode=False,
)
result = dict(changed=False, msg='', out={})
raw = ""
params = module.params
state = params['state']
conn_username = (params['conn_username']).upper()
conn_password = params['conn_password']
host = params['host']
sysnr = params['sysnr']
client = params['client']
company_id = (params['company_id']).upper()
name = params['name']
name_2 = params['name_2']
country = params['country']
time_zone = params['time_zone']
city = params['city']
post_code = params['post_code']
street = params['street']
street_no = params['street_no']
e_mail = params['e_mail']
if not HAS_PYRFC_LIBRARY:
module.fail_json(
msg=missing_required_lib('pyrfc'),
exception=ANOTHER_LIBRARY_IMPORT_ERROR)
# basic RFC connection with pyrfc
try:
conn = Connection(user=conn_username, passwd=conn_password, ashost=host, sysnr=sysnr, client=client)
except Exception as err:
result['error'] = str(err)
result['msg'] = 'Something went wrong connecting to the SAP system.'
module.fail_json(**result)
# build parameter dict of dict
company_params = build_company_params(name, name_2, country, time_zone, city, post_code, street, street_no, e_mail)
if state == "absent":
raw = call_rfc_method(conn, 'BAPI_COMPANY_DELETE', {'COMPANY': company_id})
if state == "present":
raw = call_rfc_method(conn, 'BAPI_COMPANY_CLONE',
{'METHOD': {'USMETHOD': 'COMPANY_CLONE'}, 'COMPANY': company_id, 'COMP_DATA': company_params})
analysed = return_analysis(raw)
result['out'] = raw
result['changed'] = analysed[0]['change']
result['msg'] = analysed[2]['msg']
if analysed[1]['failed']:
module.fail_json(**result)
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,258 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> <rainer.leber@sva.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: sap_snote
short_description: This module will upload and (de)implements C(SNOTES) in a SAP S4HANA environment.
version_added: "1.0.0"
description:
- The C(sap_snote) module depends on C(pyrfc) Python library (version 2.4.0 and upwards).
Depending on distribution you are using, you may need to install additional packages to
have these available.
- This module will use the Function Group C(SCWB_API).
- The C(TMS) must be configured at first.
- Integrating SNOTES cannot be done via C(DDIC)- or C(SAP*)-User.
options:
state:
description:
- The decision what to do with the SNOTE.
- Could be C('present'), C('absent')
default: 'present'
choices:
- 'present'
- 'absent'
required: false
type: str
conn_username:
description: The required username for the SAP system.
required: true
type: str
conn_password:
description: The required password for the SAP system.
required: true
type: str
host:
description: The required host for the SAP system. Can be either an FQDN or IP Address.
required: true
type: str
sysnr:
description:
- The system number of the SAP system.
- You must quote the value to ensure retaining the leading zeros.
required: false
default: '01'
type: str
client:
description:
- The client number to connect to.
- You must quote the value to ensure retaining the leading zeros.
required: false
default: '000'
type: str
snote_path:
description:
- The path to the extracted SNOTE txt file.
- The File could be extracted from SAR package.
- If C(snote_path) is not provided, the C(snote) parameter must be defined.
- The SNOTE txt file must be at a place where the SAP System is authorized for. For example C(/usr/sap/trans/files).
required: false
type: str
snote:
description:
- With the C(snote) paramter only implementation and deimplementation will work.
- Upload SNOTES to the System is only available if C(snote_path) is provided.
required: false
type: str
requirements:
- pyrfc >= 2.4.0
author:
- Rainer Leber (@rainerleber)
'''
EXAMPLES = r'''
- name: test snote module
hosts: localhost
tasks:
- name: implement SNOTE
community.sap.sap_snote:
conn_username: 'DDIC'
conn_password: 'Passwd1234'
host: 192.168.1.100
sysnr: '01'
client: '000'
state: present
snote_path: /usr/sap/trans/tmp/0002949148.txt
- name: test snote module without path
hosts: localhost
tasks:
- name: deimplement SNOTE
community.sap.sap_snote:
conn_username: 'DDIC'
conn_password: 'Passwd1234'
host: 192.168.1.100
sysnr: '01'
client: '000'
state: absent
snote: 0002949148
'''
RETURN = r'''
msg:
description: A small execution description.
type: str
returned: always
sample: 'SNOTE 000298026 implemented.'
out:
description: A complete description of the SNOTE implementation. If this is available.
type: list
elements: dict
returned: always
sample: '{
"RETURN": [{"ES_MSG": { "MSGNO": "000", "MSGTY": "", "MSGTXT": "", "MSGV1": "" },
"ET_MSG": [],
"EV_RC": 0,
"ET_MISSING_NOTES": [],
"IT_FILENAME": [{"FILENAME": "/usr/sap/trans/tmp/0002980265.txt"}],
"IT_NOTES": [{"NUMM": "0002980265", "VERSNO": "0000"}]
}]}'
'''
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from os import path as os_path
import traceback
try:
from pyrfc import Connection
except ImportError:
HAS_PYRFC_LIBRARY = False
ANOTHER_LIBRARY_IMPORT_ERROR = traceback.format_exc()
else:
HAS_PYRFC_LIBRARY = True
def call_rfc_method(connection, method_name, kwargs):
# PyRFC call function
return connection.call(method_name, **kwargs)
def check_implementation(conn, snote):
check_implemented = call_rfc_method(conn, 'SCWB_API_GET_NOTES_IMPLEMENTED', {})
for snote_list in check_implemented['ET_NOTES_IMPL']:
if snote in snote_list['NUMM']:
return True
return False
def run_module():
module = AnsibleModule(
argument_spec=dict(
state=dict(default='present', choices=['absent', 'present']),
conn_username=dict(type='str', required=True),
conn_password=dict(type='str', required=True, no_log=True),
host=dict(type='str', required=True),
sysnr=dict(type='str', default="01"),
client=dict(type='str', default="000"),
snote_path=dict(type='str', required=False),
snote=dict(type='str', required=False),
),
required_one_of=[('snote_path', 'snote')],
supports_check_mode=False,
)
result = dict(changed=False, msg='', out={}, error='')
raw = ""
post_check = False
params = module.params
state = params['state']
conn_username = (params['conn_username']).upper()
conn_password = params['conn_password']
host = params['host']
sysnr = (params['sysnr']).zfill(2)
client = params['client']
path = params['snote_path']
snote = params['snote']
if not HAS_PYRFC_LIBRARY:
module.fail_json(
msg=missing_required_lib('pyrfc'),
exception=ANOTHER_LIBRARY_IMPORT_ERROR)
if conn_username == "DDIC" or conn_username == "SAP*":
result['msg'] = 'User C(DDIC) or C(SAP*) not allowed for this operation.'
module.fail_json(**result)
# basic RFC connection with pyrfc
try:
conn = Connection(user=conn_username, passwd=conn_password, ashost=host, sysnr=sysnr, client=client)
except Exception as err:
result['error'] = str(err)
result['msg'] = 'Something went wrong connecting to the SAP system.'
module.fail_json(**result)
# pre evaluation of parameters
if path is not None:
if path.endswith('.txt'):
# splits snote number from path and txt extension
snote = os_path.basename(os_path.normpath(path)).split('.')[0]
else:
result['msg'] = 'The path must include the extracted snote file and ends with txt.'
module.fail_json(**result)
pre_check = check_implementation(conn, snote)
if state == "absent" and pre_check:
raw = call_rfc_method(conn, 'SCWB_API_NOTES_DEIMPLEMENT', {'IT_NOTES': [snote]})
if state == "present" and not pre_check:
if path:
raw_upload = call_rfc_method(conn, 'SCWB_API_UPLOAD_NOTES', {'IT_FILENAME': [path], 'IT_NOTES': [snote]})
if raw_upload['EV_RC'] != 0:
result['out'] = raw_upload
result['msg'] = raw_upload['ES_MSG']['MSGTXT']
module.fail_json(**result)
raw = call_rfc_method(conn, 'SCWB_API_NOTES_IMPLEMENT', {'IT_NOTES': [snote]})
queued = call_rfc_method(conn, 'SCWB_API_CINST_QUEUE_GET', {})
if queued['ET_MANUAL_ACTIVITIES']:
raw = call_rfc_method(conn, 'SCWB_API_CONFIRM_MAN_ACTIVITY', {})
if raw:
if raw['EV_RC'] == 0:
post_check = check_implementation(conn, snote)
if post_check and state == "present":
result['changed'] = True
result['msg'] = 'SNOTE "{0}" implemented.'.format(snote)
if not post_check and state == "absent":
result['changed'] = True
result['msg'] = 'SNOTE "{0}" deimplemented.'.format(snote)
else:
result['msg'] = "Something went wrong."
module.fail_json(**result)
result['out'] = raw
else:
result['msg'] = "Nothing to do."
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,206 @@
#!/usr/bin/python
# Copyright: (c) 2022, Rainer Leber rainerleber@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: sap_system_facts
short_description: Gathers SAP facts in a host
version_added: "1.0.0"
description:
- This facts module gathers SAP system facts about the running instance.
author:
- Rainer Leber (@rainerleber)
notes:
- Supports C(check_mode).
'''
EXAMPLES = r'''
- name: Return SAP system ansible_facts
community.sap.sap_system_fact:
'''
RETURN = r'''
# These are examples of possible return values,
# and in general should use other names for return values.
ansible_facts:
description: Facts to add to ansible_facts.
returned: always
type: list
elements: dict
contains:
sap:
description: Facts about the running SAP system.
type: list
elements: dict
returned: When SAP system fact is present
sample: [
{
"InstanceType": "NW",
"NR": "00",
"SID": "ABC",
"TYPE": "ASCS"
},
{
"InstanceType": "NW",
"NR": "01",
"SID": "ABC",
"TYPE": "PAS"
},
{
"InstanceType": "HANA",
"NR": "02",
"SID": "HDB",
"TYPE": "HDB"
},
{
"InstanceType": "NW",
"NR": "80",
"SID": "WEB",
"TYPE": "WebDisp"
}
]
'''
from ansible.module_utils.basic import AnsibleModule
import os
import re
def get_all_hana_sid():
hana_sid = list()
if os.path.isdir("/hana/shared"):
# /hana/shared directory exists
for sid in os.listdir('/hana/shared'):
if os.path.isdir("/usr/sap/" + sid):
hana_sid = hana_sid + [sid]
if hana_sid:
return hana_sid
def get_all_nw_sid():
nw_sid = list()
if os.path.isdir("/sapmnt"):
# /sapmnt directory exists
for sid in os.listdir('/sapmnt'):
if os.path.isdir("/usr/sap/" + sid):
nw_sid = nw_sid + [sid]
else:
# Check to see if /sapmnt/SID/sap_bobj exists
if os.path.isdir("/sapmnt/" + sid + "/sap_bobj"):
# is a bobj system
nw_sid = nw_sid + [sid]
if nw_sid:
return nw_sid
def get_hana_nr(sids, module):
hana_list = list()
for sid in sids:
for instance in os.listdir('/usr/sap/' + sid):
if 'HDB' in instance:
instance_nr = instance[-2:]
# check if instance number exists
command = [module.get_bin_path('/usr/sap/hostctrl/exe/sapcontrol', required=True)]
command.extend(['-nr', instance_nr, '-function', 'GetProcessList'])
check_instance = module.run_command(command, check_rc=False)
# sapcontrol returns c(0 - 5) exit codes only c(1) is unavailable
if check_instance[0] != 1:
hana_list.append({'NR': instance_nr, 'SID': sid, 'TYPE': 'HDB', 'InstanceType': 'HANA'})
return hana_list
def get_nw_nr(sids, module):
nw_list = list()
type = ""
for sid in sids:
for instance in os.listdir('/usr/sap/' + sid):
instance_nr = instance[-2:]
command = [module.get_bin_path('/usr/sap/hostctrl/exe/sapcontrol', required=True)]
# check if returned instance_nr is a number because sapcontrol returns all if a random string is provided
if instance_nr.isdigit():
command.extend(['-nr', instance_nr, '-function', 'GetInstanceProperties'])
check_instance = module.run_command(command, check_rc=False)
if check_instance[0] != 1:
for line in check_instance[1].splitlines():
if re.search('INSTANCE_NAME', line):
# convert to list and extract last
type_raw = (line.strip('][').split(', '))[-1]
# split instance number
type = type_raw[:-2]
nw_list.append({'NR': instance_nr, 'SID': sid, 'TYPE': get_instance_type(type), 'InstanceType': 'NW'})
return nw_list
def get_instance_type(raw_type):
if raw_type[0] == "D":
# It's a PAS
type = "PAS"
elif raw_type[0] == "A":
# It's an ASCS
type = "ASCS"
elif raw_type[0] == "W":
# It's a Webdisp
type = "WebDisp"
elif raw_type[0] == "J":
# It's a Java
type = "Java"
elif raw_type[0] == "S":
# It's an SCS
type = "SCS"
elif raw_type[0] == "E":
# It's an ERS
type = "ERS"
else:
# Unknown instance type
type = "XXX"
return type
def run_module():
module_args = dict()
system_result = list()
result = dict(
changed=False,
ansible_facts=dict(),
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True,
)
hana_sid = get_all_hana_sid()
if hana_sid:
system_result = system_result + get_hana_nr(hana_sid, module)
nw_sid = get_all_nw_sid()
if nw_sid:
system_result = system_result + get_nw_nr(nw_sid, module)
if system_result:
result['ansible_facts'] = {'sap': system_result}
else:
result['ansible_facts']
if module.check_mode:
module.exit_json(**result)
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,340 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: sap_task_list_execute
short_description: Perform SAP Task list execution
version_added: "0.1.0"
description:
- The M(community.sap.sap_task_list_execute) module depends on C(pyrfc) Python library (version 2.4.0 and upwards).
Depending on distribution you are using, you may need to install additional packages to
have these available.
- Tasks in the task list which requires manual activities will be confirmed automatically.
- This module will use the RFC package C(STC_TM_API).
requirements:
- pyrfc >= 2.4.0
- xmltodict
options:
conn_username:
description: The required username for the SAP system.
required: true
type: str
conn_password:
description: The required password for the SAP system.
required: true
type: str
host:
description: The required host for the SAP system. Can be either an FQDN or IP Address.
required: true
type: str
sysnr:
description:
- The system number of the SAP system.
- You must quote the value to ensure retaining the leading zeros.
default: '00'
type: str
client:
description:
- The client number to connect to.
- You must quote the value to ensure retaining the leading zeros.
default: '000'
type: str
task_to_execute:
description: The task list which will be executed.
required: true
type: str
task_parameters:
description:
- The tasks and the parameters for execution.
- If the task list does not need any parameters, this could be empty.
- If only specific tasks from the task list should be executed,
the tasks even when no parameter is needed must be provided
alongside with the module parameter I(task_skip=true).
type: list
elements: dict
suboptions:
TASKNAME:
description: The name of the task in the task list.
type: str
required: true
FIELDNAME:
description: The name of the field of the task.
type: str
VALUE:
description: The value which have to be set.
type: raw
task_settings:
description:
- Setting for the execution of the task list. This can be the following as in TCODE SE80 described.
Check Mode C(CHECKRUN), Background Processing Active C(BATCH) (this is the default value),
Asynchronous Execution C(ASYNC), Trace Mode C(TRACE), Server Name C(BATCH_TARGET).
default: ['BATCH']
type: list
elements: str
task_skip:
description:
- If this parameter is C(true), not defined tasks in I(task_parameters) are skipped.
- This could be the case when only certain tasks should run from the task list.
default: false
type: bool
notes:
- Does not support C(check_mode). Always returns that the state has changed.
author:
- Rainer Leber (@rainerleber)
'''
EXAMPLES = r'''
# Pass in a message
- name: Test task execution
community.sap.sap_task_list_execute:
conn_username: DDIC
conn_password: Passwd1234
host: 10.1.8.10
sysnr: '01'
client: '000'
task_to_execute: SAP_BASIS_SSL_CHECK
task_settings: batch
- name: Pass in input parameters
community.sap.sap_task_list_execute:
conn_username: DDIC
conn_password: Passwd1234
host: 10.1.8.10
sysnr: '00'
client: '000'
task_to_execute: SAP_BASIS_SSL_CHECK
task_parameters :
- { 'TASKNAME': 'CL_STCT_CHECK_SEC_CRYPTO', 'FIELDNAME': 'P_OPT2', 'VALUE': 'X' }
- TASKNAME: CL_STCT_CHECK_SEC_CRYPTO
FIELDNAME: P_OPT3
VALUE: X
task_settings: batch
# Exported environment variables
- name: Hint if module will fail with error message like ImportError libsapnwrfc.so...
community.sap.sap_task_list_execute:
conn_username: DDIC
conn_password: Passwd1234
host: 10.1.8.10
sysnr: '00'
client: '000'
task_to_execute: SAP_BASIS_SSL_CHECK
task_settings: batch
environment:
SAPNWRFC_HOME: /usr/local/sap/nwrfcsdk
LD_LIBRARY_PATH: /usr/local/sap/nwrfcsdk/lib
'''
RETURN = r'''
msg:
description: A small execution description.
type: str
returned: always
sample: 'Successful'
out:
description: A complete description of the executed tasks. If this is available.
type: list
elements: dict
returned: on success
sample: [...,{
"LOG": {
"STCTM_S_LOG": [
{
"ACTIVITY": "U_CONFIG",
"ACTIVITY_DESCR": "Configuration changed",
"DETAILS": null,
"EXEC_ID": "20210728184903.815739",
"FIELD": null,
"ID": "STC_TASK",
"LOG_MSG_NO": "000000",
"LOG_NO": null,
"MESSAGE": "For radiobutton group ICM too many options are set; choose only one option",
"MESSAGE_V1": "ICM",
"MESSAGE_V2": null,
"MESSAGE_V3": null,
"MESSAGE_V4": null,
"NUMBER": "048",
"PARAMETER": null,
"PERIOD": "M",
"PERIOD_DESCR": "Maintenance",
"ROW": "0",
"SRC_LINE": "170",
"SRC_OBJECT": "CL_STCTM_REPORT_UI IF_STCTM_UI_TASK~SET_PARAMETERS",
"SYSTEM": null,
"TIMESTMP": "20210728184903",
"TSTPNM": "DDIC",
"TYPE": "E"
},...
]}}]
'''
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
import traceback
try:
from pyrfc import Connection
except ImportError:
HAS_PYRFC_LIBRARY = False
PYRFC_LIBRARY_IMPORT_ERROR = traceback.format_exc()
else:
HAS_PYRFC_LIBRARY = True
try:
import xmltodict
except ImportError:
HAS_XMLTODICT_LIBRARY = False
XMLTODICT_LIBRARY_IMPORT_ERROR = traceback.format_exc()
else:
HAS_XMLTODICT_LIBRARY = True
def call_rfc_method(connection, method_name, kwargs):
# PyRFC call function
return connection.call(method_name, **kwargs)
def process_exec_settings(task_settings):
# processes task settings to objects
exec_settings = {}
for settings in task_settings:
temp_dict = {settings.upper(): 'X'}
for key, value in temp_dict.items():
exec_settings[key] = value
return exec_settings
def xml_to_dict(xml_raw):
try:
xml_parsed = xmltodict.parse(xml_raw, dict_constructor=dict)
xml_dict = xml_parsed['asx:abap']['asx:values']['SESSION']['TASKLIST']
except KeyError:
xml_dict = "No logs available."
return xml_dict
def run_module():
params_spec = dict(
TASKNAME=dict(type='str', required=True),
FIELDNAME=dict(type='str'),
VALUE=dict(type='raw'),
)
# define available arguments/parameters a user can pass to the module
module = AnsibleModule(
argument_spec=dict(
# values for connection
conn_username=dict(type='str', required=True),
conn_password=dict(type='str', required=True, no_log=True),
host=dict(type='str', required=True),
sysnr=dict(type='str', default="00"),
client=dict(type='str', default="000"),
# values for execution tasks
task_to_execute=dict(type='str', required=True),
task_parameters=dict(type='list', elements='dict', options=params_spec),
task_settings=dict(type='list', elements='str', default=['BATCH']),
task_skip=dict(type='bool', default=False),
),
supports_check_mode=False,
)
result = dict(changed=False, msg='', out={})
params = module.params
username = params['conn_username'].upper()
password = params['conn_password']
host = params['host']
sysnr = params['sysnr']
client = params['client']
task_parameters = params['task_parameters']
task_to_execute = params['task_to_execute']
task_settings = params['task_settings']
task_skip = params['task_skip']
if not HAS_PYRFC_LIBRARY:
module.fail_json(
msg=missing_required_lib('pyrfc'),
exception=PYRFC_LIBRARY_IMPORT_ERROR)
if not HAS_XMLTODICT_LIBRARY:
module.fail_json(
msg=missing_required_lib('xmltodict'),
exception=XMLTODICT_LIBRARY_IMPORT_ERROR)
# basic RFC connection with pyrfc
try:
conn = Connection(user=username, passwd=password, ashost=host, sysnr=sysnr, client=client)
except Exception as err:
result['error'] = str(err)
result['msg'] = 'Something went wrong connecting to the SAP system.'
module.fail_json(**result)
try:
raw_params = call_rfc_method(conn, 'STC_TM_SCENARIO_GET_PARAMETERS',
{'I_SCENARIO_ID': task_to_execute})
except Exception as err:
result['error'] = str(err)
result['msg'] = 'The task list does not exist.'
module.fail_json(**result)
exec_settings = process_exec_settings(task_settings)
# initialize session task
session_init = call_rfc_method(conn, 'STC_TM_SESSION_BEGIN',
{'I_SCENARIO_ID': task_to_execute,
'I_INIT_ONLY': 'X'})
# Confirm Tasks which requires manual activities from Task List Run
for task in raw_params['ET_PARAMETER']:
call_rfc_method(conn, 'STC_TM_TASK_CONFIRM',
{'I_SESSION_ID': session_init['E_SESSION_ID'],
'I_TASKNAME': task['TASKNAME']})
if task_skip:
for task in raw_params['ET_PARAMETER']:
call_rfc_method(conn, 'STC_TM_TASK_SKIP',
{'I_SESSION_ID': session_init['E_SESSION_ID'],
'I_TASKNAME': task['TASKNAME'], 'I_SKIP_DEP_TASKS': 'X'})
# unskip defined tasks and set parameters
if task_parameters is not None:
for task in task_parameters:
call_rfc_method(conn, 'STC_TM_TASK_UNSKIP',
{'I_SESSION_ID': session_init['E_SESSION_ID'],
'I_TASKNAME': task['TASKNAME'], 'I_UNSKIP_DEP_TASKS': 'X'})
call_rfc_method(conn, 'STC_TM_SESSION_SET_PARAMETERS',
{'I_SESSION_ID': session_init['E_SESSION_ID'],
'IT_PARAMETER': task_parameters})
# start the task
try:
session_start = call_rfc_method(conn, 'STC_TM_SESSION_RESUME',
{'I_SESSION_ID': session_init['E_SESSION_ID'],
'IS_EXEC_SETTINGS': exec_settings})
except Exception as err:
result['error'] = str(err)
result['msg'] = 'Something went wrong. See error.'
module.fail_json(**result)
# get task logs because the execution may successfully but the tasks shows errors or warnings
# returned value is ABAPXML https://help.sap.com/doc/abapdocu_755_index_htm/7.55/en-US/abenabap_xslt_asxml_general.htm
session_log = call_rfc_method(conn, 'STC_TM_SESSION_GET_LOG',
{'I_SESSION_ID': session_init['E_SESSION_ID']})
task_list = xml_to_dict(session_log['E_LOG'])
result['changed'] = True
result['msg'] = session_start['E_STATUS_DESCR']
result['out'] = task_list
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,499 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> <rainer.leber@sva.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: sap_user
short_description: This module will manage a user entities in a SAP S4/HANA environment
version_added: "1.0.0"
description:
- The M(community.sap.sap_user) module depends on C(pyrfc) Python library (version 2.4.0 and upwards).
Depending on distribution you are using, you may need to install additional packages to
have these available.
- This module will use the following user BAPIs to manage user entities.
- C(BAPI_USER_GET_DETAIL)
- C(BAPI_USER_DELETE)
- C(BAPI_USER_CREATE1)
- C(BAPI_USER_CHANGE)
- C(BAPI_USER_ACTGROUPS_ASSIGN)
- C(BAPI_USER_PROFILES_ASSIGN)
- C(BAPI_USER_UNLOCK)
- C(BAPI_USER_LOCK)
options:
state:
description:
- The decision what to do with the user.
default: 'present'
choices:
- 'present'
- 'absent'
- 'lock'
- 'unlock'
required: false
type: str
force:
description:
- Must be C('True') if the password or type should be overwritten.
default: False
required: false
type: bool
conn_username:
description: The required username for the SAP system.
required: true
type: str
conn_password:
description: The required password for the SAP system.
required: true
type: str
host:
description: The required host for the SAP system. Can be either an FQDN or IP Address.
required: true
type: str
sysnr:
description:
- The system number of the SAP system.
- You must quote the value to ensure retaining the leading zeros.
default: '00'
type: str
client:
description:
- The client number to connect to.
- You must quote the value to ensure retaining the leading zeros.
default: '000'
type: str
username:
description:
- The username.
type: str
required: true
firstname:
description:
- The Firstname of the user in the SAP system.
type: str
required: false
lastname:
description:
- The lastname of the user in the SAP system.
type: str
required: false
email:
description:
- The email address of the user in the SAP system.
type: str
required: false
password:
description:
- The password for the user in the SAP system.
type: str
required: false
useralias:
description:
- The alias for the user in the SAP system.
type: str
required: false
user_type:
description:
- The type for the user in the SAP system.
- C('A') Dialog user, C('B') System User, C('C') Communication User,
C('S') Service User, C('L') Reference User.
- Must be in uppercase.
type: str
required: false
default: 'A'
choices: ['A', 'B', 'C', 'S', 'L']
company:
description:
- The specific company the user belongs to.
- The company name must be available in the SAP system.
type: str
required: false
profiles:
description:
- Assign profiles to the user.
- Should be in uppercase, for example C('SAP_NEW') or C('SAP_ALL').
type: list
elements: str
default: ['']
required: false
roles:
description:
- Assign roles to the user.
type: list
elements: str
default: ['']
required: false
requirements:
- pyrfc >= 2.4.0
author:
- Rainer Leber (@rainerleber)
notes:
- Does not support C(check_mode).
'''
EXAMPLES = r'''
- name: Create SAP User
community.sap.sap_user:
conn_username: 'DDIC'
conn_password: 'Test123'
host: 192.168.1.150
sysnr: '01'
client: '000'
state: present
username: ADMIN
firstname: first_admin
lastname: last_admin
email: admin@test.de
password: Test123456
useralias: ADMIN
company: DEFAULT_COMPANY
roles:
- "SAP_ALL"
- name: Force change SAP User
community.sap.sap_user:
conn_username: 'DDIC'
conn_password: 'Test123'
host: 192.168.1.150
sysnr: '01'
client: '000'
state: present
force: true
username: ADMIN
firstname: first_admin
lastname: last_admin
email: admin@test.de
password: Test123456
useralias: ADMIN
company: DEFAULT_COMPANY
roles:
- "SAP_ALL"
- name: Delete SAP User
community.sap.sap_user:
conn_username: 'DDIC'
conn_password: 'Test123'
host: 192.168.1.150
sysnr: '01'
client: '000'
state: absent
force: true
username: ADMIN
- name: Unlock SAP User
community.sap.sap_user:
conn_username: 'DDIC'
conn_password: 'Test123'
host: 192.168.1.150
sysnr: '01'
client: '000'
state: unlock
force: true
username: ADMIN
'''
RETURN = r'''
msg:
description: A small execution description about the user action.
type: str
returned: always
sample: 'User ADMIN created'
out:
description: A detailed description about the user action.
type: list
elements: dict
returned: on success
sample: [...,{
"RETURN": [
{
"FIELD": "BNAME",
"ID": "01",
"LOG_MSG_NO": "000000",
"LOG_NO": "",
"MESSAGE": "User ADMIN created",
"MESSAGE_V1": "ADMIN",
"MESSAGE_V2": "",
"MESSAGE_V3": "",
"MESSAGE_V4": "",
"NUMBER": "102",
"PARAMETER": "",
"ROW": 0,
"SYSTEM": "",
"TYPE": "S"
}
],
"SAPUSER_UUID_HIST": []}]
'''
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
import traceback
import datetime
try:
from pyrfc import Connection
except ImportError:
HAS_PYRFC_LIBRARY = False
PYRFC_LIBRARY_IMPORT_ERROR = traceback.format_exc()
else:
HAS_PYRFC_LIBRARY = True
def add_to_dict(target_dict, target_key, value):
# Adds the given value to a dict as the key
# check if the given key is in the given dict yet
if target_key in target_dict:
return False
target_dict[target_key] = value
return True
def call_rfc_method(connection, method_name, kwargs):
# PyRFC call function
return connection.call(method_name, **kwargs)
def build_rfc_user_params(username, firstname, lastname, email, raw_password,
useralias, user_type, raw_company, user_change, force):
"""Creates RFC parameters for Creating users"""
# define dicts in batch
params = dict()
address = dict()
password = dict()
alias = dict()
logondata = dict()
company = dict()
# for change parameters
addressx = dict()
passwordx = dict()
logondatax = dict()
companyx = dict()
# define username
add_to_dict(params, 'USERNAME', username)
# define Address
add_to_dict(address, 'FIRSTNAME', firstname)
add_to_dict(address, 'LASTNAME', lastname)
add_to_dict(address, 'E_MAIL', email)
# define Password
add_to_dict(password, 'BAPIPWD', raw_password)
# define Alias
add_to_dict(alias, 'USERALIAS', useralias)
# define LogonData
add_to_dict(logondata, 'GLTGV', datetime.date.today())
add_to_dict(logondata, 'GLTGB', '20991231')
add_to_dict(logondata, 'USTYP', user_type)
# define company
add_to_dict(company, 'COMPANY', raw_company)
params['LOGONDATA'] = logondata
params['ADDRESS'] = address
params['COMPANY'] = company
params['ALIAS'] = alias
params['PASSWORD'] = password
# add change if user exists
if user_change and force:
add_to_dict(addressx, 'FIRSTNAME', 'X')
add_to_dict(addressx, 'LASTNAME', 'X')
add_to_dict(addressx, 'E_MAIL', 'X')
# define Password
add_to_dict(passwordx, 'BAPIPWD', 'X')
# define LogonData
add_to_dict(logondatax, 'USTYP', 'X')
# define company
add_to_dict(companyx, 'COMPANY', 'X')
params['LOGONDATAX'] = logondatax
params['ADDRESSX'] = addressx
params['COMPANYX'] = companyx
params['PASSWORDX'] = passwordx
return params
def user_role_assignment_build_rfc_params(roles, username):
rfc_table = []
for role_name in roles:
table_row = {'AGR_NAME': role_name}
add_to_dict(table_row, 'FROM_DAT', datetime.date.today())
add_to_dict(table_row, 'TO_DAT', '20991231')
rfc_table.append(table_row)
return {
'USERNAME': username,
'ACTIVITYGROUPS': rfc_table
}
def user_profile_assignment_build_rfc_params(profiles, username):
rfc_table = []
for profile_name in profiles:
table_row = {'BAPIPROF': profile_name}
rfc_table.append(table_row)
return {
'USERNAME': username,
'PROFILES': rfc_table
}
def check_user(user_detail):
if len(user_detail['RETURN']) > 0:
for sub in user_detail['RETURN']:
if sub['NUMBER'] == '124':
return False
return True
def return_analysis(raw):
change = False
failed = False
for state in raw['RETURN']:
if state['TYPE'] == "E":
if state['NUMBER'] == '224' or state['NUMBER'] == '124':
change = False
else:
failed = True
if state['TYPE'] == "S":
if state['NUMBER'] != '029':
change = True
if state['TYPE'] == "W":
if state['NUMBER'] == '049' or state['NUMBER'] == '047':
change = True
if state['NUMBER'] == '255':
change = True
return [{"change": change}, {"failed": failed}]
def run_module():
module = AnsibleModule(
argument_spec=dict(
# logical values
state=dict(default='present', choices=[
'absent', 'present', 'lock', 'unlock']),
force=dict(type='bool', default=False),
# values for connection
conn_username=dict(type='str', required=True),
conn_password=dict(type='str', required=True, no_log=True),
host=dict(type='str', required=True),
sysnr=dict(type='str', default="00"),
client=dict(type='str', default="000"),
# values for the new or existing user
username=dict(type='str', required=True),
firstname=dict(type='str', required=False),
lastname=dict(type='str', required=False),
email=dict(type='str', required=False),
password=dict(type='str', required=False, no_log=True),
useralias=dict(type='str', required=False),
user_type=dict(default="A",
choices=['A', 'B', 'C', 'S', 'L']),
company=dict(type='str', required=False),
# values for profile must a list
# Example ["SAP_NEW", "SAP_ALL"]
profiles=dict(type='list', elements='str', default=[""]),
# values for roles must a list
roles=dict(type='list', elements='str', default=[""]),
),
supports_check_mode=False,
required_if=[('state', 'present', ['useralias', 'company'])]
)
result = dict(changed=False, msg='', out='')
count = 0
raw = ""
params = module.params
state = params['state']
conn_username = (params['conn_username']).upper()
conn_password = params['conn_password']
host = params['host']
sysnr = params['sysnr']
client = params['client']
username = (params['username']).upper()
firstname = params['firstname']
lastname = params['lastname']
email = params['email']
password = params['password']
force = params['force']
if not params['useralias'] is None:
useralias = (params['useralias']).upper()
user_type = (params['user_type']).upper()
company = params['company']
profiles = params['profiles']
roles = params['roles']
if not HAS_PYRFC_LIBRARY:
module.fail_json(
msg=missing_required_lib('pyrfc'),
exception=PYRFC_LIBRARY_IMPORT_ERROR)
# basic RFC connection with pyrfc
try:
conn = Connection(user=conn_username, passwd=conn_password, ashost=host, sysnr=sysnr, client=client)
except Exception as err:
result['error'] = str(err)
result['msg'] = 'Something went wrong connecting to the SAP system.'
module.fail_json(**result)
# user details
user_detail = call_rfc_method(conn, 'BAPI_USER_GET_DETAIL', {'USERNAME': username})
user_exists = check_user(user_detail)
if state == "absent":
if user_exists:
raw = call_rfc_method(conn, 'BAPI_USER_DELETE', {'USERNAME': username})
if state == "present":
user_params = build_rfc_user_params(username, firstname, lastname, email, password, useralias, user_type, company, user_exists, force)
if not user_exists:
raw = call_rfc_method(conn, 'BAPI_USER_CREATE1', user_params)
if user_exists:
# check for address changes when user exists
user_no_changes = all((user_detail.get('ADDRESS')).get(k) == v for k, v in (user_params.get('ADDRESS')).items())
if not user_no_changes or force:
raw = call_rfc_method(conn, 'BAPI_USER_CHANGE', user_params)
call_rfc_method(conn, 'BAPI_USER_ACTGROUPS_ASSIGN', user_role_assignment_build_rfc_params(roles, username))
call_rfc_method(conn, 'BAPI_USER_PROFILES_ASSIGN', user_profile_assignment_build_rfc_params(profiles, username))
if state == "unlock":
if user_exists:
raw = call_rfc_method(conn, 'BAPI_USER_UNLOCK', {'USERNAME': username})
if state == "lock":
if user_exists:
raw = call_rfc_method(conn, 'BAPI_USER_LOCK', {'USERNAME': username})
# analyse return value
if raw != '':
analysed = return_analysis(raw)
result['out'] = raw
result['changed'] = analysed[0]['change']
for msgs in raw['RETURN']:
if count > 0:
result['msg'] = result['msg'] + '\n'
result['msg'] = result['msg'] + msgs['MESSAGE']
count = count + 1
if analysed[1]['failed']:
module.fail_json(**result)
else:
result['msg'] = "No changes where made."
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,220 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: sapcar_extract
short_description: Manages SAP SAPCAR archives
version_added: "0.1.0"
description:
- Provides support for unpacking C(sar)/C(car) files with the SAPCAR binary from SAP and pulling
information back into Ansible.
options:
path:
description: The path to the SAR/CAR file.
type: path
required: true
dest:
description:
- The destination where SAPCAR extracts the SAR file. Missing folders will be created.
If this parameter is not provided, it will unpack in the same folder as the SAR file.
type: path
binary_path:
description:
- The path to the SAPCAR binary, for example, C(/home/dummy/sapcar) or C(https://myserver/SAPCAR).
If this parameter is not provided, the module will look in C(PATH).
type: path
signature:
description:
- If C(true), the signature will be extracted.
default: false
type: bool
security_library:
description:
- The path to the security library, for example, C(/usr/sap/hostctrl/exe/libsapcrytp.so), for signature operations.
type: path
manifest:
description:
- The name of the manifest.
default: "SIGNATURE.SMF"
type: str
remove:
description:
- If C(true), the SAR/CAR file will be removed. B(This should be used with caution!)
default: false
type: bool
author:
- Rainer Leber (@RainerLeber)
notes:
- Always returns C(changed=true) in C(check_mode).
'''
EXAMPLES = r"""
- name: Extract SAR file
community.sap.sapcar_extract:
path: "~/source/hana.sar"
- name: Extract SAR file with destination
community.sap.sapcar_extract:
path: "~/source/hana.sar"
dest: "~/test/"
- name: Extract SAR file with destination and download from webserver can be a fileshare as well
community.sap.sapcar_extract:
path: "~/source/hana.sar"
dest: "~/dest/"
binary_path: "https://myserver/SAPCAR"
- name: Extract SAR file and delete SAR after extract
community.sap.sapcar_extract:
path: "~/source/hana.sar"
remove: true
- name: Extract SAR file with manifest
community.sap.sapcar_extract:
path: "~/source/hana.sar"
signature: true
- name: Extract SAR file with manifest and rename it
community.sap.sapcar_extract:
path: "~/source/hana.sar"
manifest: "MyNewSignature.SMF"
signature: true
"""
import os
from tempfile import NamedTemporaryFile
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.urls import open_url
from ansible.module_utils.common.text.converters import to_native
def get_list_of_files(dir_name):
# create a list of file and directories
# names in the given directory
list_of_file = os.listdir(dir_name)
allFiles = list()
# Iterate over all the entries
for entry in list_of_file:
# Create full path
fullPath = os.path.join(dir_name, entry)
# If entry is a directory then get the list of files in this directory
if os.path.isdir(fullPath):
allFiles = allFiles + [fullPath]
allFiles = allFiles + get_list_of_files(fullPath)
else:
allFiles.append(fullPath)
return allFiles
def download_SAPCAR(binary_path, module):
bin_path = None
# download sapcar binary if url is provided otherwise path is returned
if binary_path is not None:
if binary_path.startswith('https://') or binary_path.startswith('http://'):
random_file = NamedTemporaryFile(delete=False)
with open_url(binary_path) as response:
with random_file as out_file:
data = response.read()
out_file.write(data)
os.chmod(out_file.name, 0o700)
bin_path = out_file.name
module.add_cleanup_file(bin_path)
else:
bin_path = binary_path
return bin_path
def check_if_present(command, path, dest, signature, manifest, module):
# manipulating output from SAR file for compare with already extracted files
iter_command = [command, '-tvf', path]
sar_out = module.run_command(iter_command)[1]
sar_raw = sar_out.split("\n")[1:]
if dest[-1] != "/":
dest = dest + "/"
sar_files = [dest + x.split(" ")[-1] for x in sar_raw if x]
# remove any SIGNATURE.SMF from list because it will not unpacked if signature is false
if not signature:
sar_files = [item for item in sar_files if not item.endswith('.SMF')]
# if signature is renamed manipulate files in list of sar file for compare.
if manifest != "SIGNATURE.SMF":
sar_files = [item for item in sar_files if not item.endswith('.SMF')]
sar_files = sar_files + [manifest]
# get extracted files if present
files_extracted = get_list_of_files(dest)
# compare extracted files with files in sar file
present = all(elem in files_extracted for elem in sar_files)
return present
def main():
module = AnsibleModule(
argument_spec=dict(
path=dict(type='path', required=True),
dest=dict(type='path'),
binary_path=dict(type='path'),
signature=dict(type='bool', default=False),
security_library=dict(type='path'),
manifest=dict(type='str', default="SIGNATURE.SMF"),
remove=dict(type='bool', default=False),
),
supports_check_mode=True,
)
rc, out, err = [0, "", ""]
params = module.params
check_mode = module.check_mode
path = params['path']
dest = params['dest']
signature = params['signature']
security_library = params['security_library']
manifest = params['manifest']
remove = params['remove']
bin_path = download_SAPCAR(params['binary_path'], module)
if dest is None:
dest_head_tail = os.path.split(path)
dest = dest_head_tail[0] + '/'
else:
if not os.path.exists(dest):
os.makedirs(dest, 0o755)
if bin_path is not None:
command = [module.get_bin_path(bin_path, required=True)]
else:
try:
command = [module.get_bin_path('sapcar', required=True)]
except Exception as e:
module.fail_json(msg='Failed to find SAPCAR at the expected path or URL "{0}". Please check whether it is available: {1}'
.format(bin_path, to_native(e)))
present = check_if_present(command[0], path, dest, signature, manifest, module)
if not present:
command.extend(['-xvf', path, '-R', dest])
if security_library:
command.extend(['-L', security_library])
if signature:
command.extend(['-manifest', manifest])
if not check_mode:
(rc, out, err) = module.run_command(command, check_rc=True)
changed = True
else:
changed = False
out = "already unpacked"
if remove:
os.remove(path)
module.exit_json(changed=changed, message=rc, stdout=out,
stderr=err, command=' '.join(command))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,258 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com> <rainer.leber@sva.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: sap_snote
short_description: This module will upload and (de)implements C(SNOTES) in a SAP S4HANA environment.
version_added: "1.0.0"
description:
- The C(sap_snote) module depends on C(pyrfc) Python library (version 2.4.0 and upwards).
Depending on distribution you are using, you may need to install additional packages to
have these available.
- This module will use the Function Group C(SCWB_API).
- The C(TMS) must be configured at first.
- Integrating SNOTES cannot be done via C(DDIC)- or C(SAP*)-User.
options:
state:
description:
- The decision what to do with the SNOTE.
- Could be C('present'), C('absent')
default: 'present'
choices:
- 'present'
- 'absent'
required: false
type: str
conn_username:
description: The required username for the SAP system.
required: true
type: str
conn_password:
description: The required password for the SAP system.
required: true
type: str
host:
description: The required host for the SAP system. Can be either an FQDN or IP Address.
required: true
type: str
sysnr:
description:
- The system number of the SAP system.
- You must quote the value to ensure retaining the leading zeros.
required: false
default: '01'
type: str
client:
description:
- The client number to connect to.
- You must quote the value to ensure retaining the leading zeros.
required: false
default: '000'
type: str
snote_path:
description:
- The path to the extracted SNOTE txt file.
- The File could be extracted from SAR package.
- If C(snote_path) is not provided, the C(snote) parameter must be defined.
- The SNOTE txt file must be at a place where the SAP System is authorized for. For example C(/usr/sap/trans/files).
required: false
type: str
snote:
description:
- With the C(snote) paramter only implementation and deimplementation will work.
- Upload SNOTES to the System is only available if C(snote_path) is provided.
required: false
type: str
requirements:
- pyrfc >= 2.4.0
author:
- Rainer Leber (@rainerleber)
'''
EXAMPLES = r'''
- name: test snote module
hosts: localhost
tasks:
- name: implement SNOTE
community.sap.sap_snote:
conn_username: 'DDIC'
conn_password: 'Passwd1234'
host: 192.168.1.100
sysnr: '01'
client: '000'
state: present
snote_path: /usr/sap/trans/tmp/0002949148.txt
- name: test snote module without path
hosts: localhost
tasks:
- name: deimplement SNOTE
community.sap.sap_snote:
conn_username: 'DDIC'
conn_password: 'Passwd1234'
host: 192.168.1.100
sysnr: '01'
client: '000'
state: absent
snote: 0002949148
'''
RETURN = r'''
msg:
description: A small execution description.
type: str
returned: always
sample: 'SNOTE 000298026 implemented.'
out:
description: A complete description of the SNOTE implementation. If this is available.
type: list
elements: dict
returned: always
sample: '{
"RETURN": [{"ES_MSG": { "MSGNO": "000", "MSGTY": "", "MSGTXT": "", "MSGV1": "" },
"ET_MSG": [],
"EV_RC": 0,
"ET_MISSING_NOTES": [],
"IT_FILENAME": [{"FILENAME": "/usr/sap/trans/tmp/0002980265.txt"}],
"IT_NOTES": [{"NUMM": "0002980265", "VERSNO": "0000"}]
}]}'
'''
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from os import path as os_path
import traceback
try:
from pyrfc import Connection
except ImportError:
HAS_PYRFC_LIBRARY = False
ANOTHER_LIBRARY_IMPORT_ERROR = traceback.format_exc()
else:
HAS_PYRFC_LIBRARY = True
def call_rfc_method(connection, method_name, kwargs):
# PyRFC call function
return connection.call(method_name, **kwargs)
def check_implementation(conn, snote):
check_implemented = call_rfc_method(conn, 'SCWB_API_GET_NOTES_IMPLEMENTED', {})
for snote_list in check_implemented['ET_NOTES_IMPL']:
if snote in snote_list['NUMM']:
return True
return False
def run_module():
module = AnsibleModule(
argument_spec=dict(
state=dict(default='present', choices=['absent', 'present']),
conn_username=dict(type='str', required=True),
conn_password=dict(type='str', required=True, no_log=True),
host=dict(type='str', required=True),
sysnr=dict(type='str', default="01"),
client=dict(type='str', default="000"),
snote_path=dict(type='str', required=False),
snote=dict(type='str', required=False),
),
required_one_of=[('snote_path', 'snote')],
supports_check_mode=False,
)
result = dict(changed=False, msg='', out={}, error='')
raw = ""
post_check = False
params = module.params
state = params['state']
conn_username = (params['conn_username']).upper()
conn_password = params['conn_password']
host = params['host']
sysnr = (params['sysnr']).zfill(2)
client = params['client']
path = params['snote_path']
snote = params['snote']
if not HAS_PYRFC_LIBRARY:
module.fail_json(
msg=missing_required_lib('pyrfc'),
exception=ANOTHER_LIBRARY_IMPORT_ERROR)
if conn_username == "DDIC" or conn_username == "SAP*":
result['msg'] = 'User C(DDIC) or C(SAP*) not allowed for this operation.'
module.fail_json(**result)
# basic RFC connection with pyrfc
try:
conn = Connection(user=conn_username, passwd=conn_password, ashost=host, sysnr=sysnr, client=client)
except Exception as err:
result['error'] = str(err)
result['msg'] = 'Something went wrong connecting to the SAP system.'
module.fail_json(**result)
# pre evaluation of parameters
if path is not None:
if path.endswith('.txt'):
# splits snote number from path and txt extension
snote = os_path.basename(os_path.normpath(path)).split('.')[0]
else:
result['msg'] = 'The path must include the extracted snote file and ends with txt.'
module.fail_json(**result)
pre_check = check_implementation(conn, snote)
if state == "absent" and pre_check:
raw = call_rfc_method(conn, 'SCWB_API_NOTES_DEIMPLEMENT', {'IT_NOTES': [snote]})
if state == "present" and not pre_check:
if path:
raw_upload = call_rfc_method(conn, 'SCWB_API_UPLOAD_NOTES', {'IT_FILENAME': [path], 'IT_NOTES': [snote]})
if raw_upload['EV_RC'] != 0:
result['out'] = raw_upload
result['msg'] = raw_upload['ES_MSG']['MSGTXT']
module.fail_json(**result)
raw = call_rfc_method(conn, 'SCWB_API_NOTES_IMPLEMENT', {'IT_NOTES': [snote]})
queued = call_rfc_method(conn, 'SCWB_API_CINST_QUEUE_GET', {})
if queued['ET_MANUAL_ACTIVITIES']:
raw = call_rfc_method(conn, 'SCWB_API_CONFIRM_MAN_ACTIVITY', {})
if raw:
if raw['EV_RC'] == 0:
post_check = check_implementation(conn, snote)
if post_check and state == "present":
result['changed'] = True
result['msg'] = 'SNOTE "{0}" implemented.'.format(snote)
if not post_check and state == "absent":
result['changed'] = True
result['msg'] = 'SNOTE "{0}" deimplemented.'.format(snote)
else:
result['msg'] = "Something went wrong."
module.fail_json(**result)
result['out'] = raw
else:
result['msg'] = "Nothing to do."
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,206 @@
#!/usr/bin/python
# Copyright: (c) 2022, Rainer Leber rainerleber@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: sap_system_facts
short_description: Gathers SAP facts in a host
version_added: "1.0.0"
description:
- This facts module gathers SAP system facts about the running instance.
author:
- Rainer Leber (@rainerleber)
notes:
- Supports C(check_mode).
'''
EXAMPLES = r'''
- name: Return SAP system ansible_facts
community.sap.sap_system_fact:
'''
RETURN = r'''
# These are examples of possible return values,
# and in general should use other names for return values.
ansible_facts:
description: Facts to add to ansible_facts.
returned: always
type: list
elements: dict
contains:
sap:
description: Facts about the running SAP system.
type: list
elements: dict
returned: When SAP system fact is present
sample: [
{
"InstanceType": "NW",
"NR": "00",
"SID": "ABC",
"TYPE": "ASCS"
},
{
"InstanceType": "NW",
"NR": "01",
"SID": "ABC",
"TYPE": "PAS"
},
{
"InstanceType": "HANA",
"NR": "02",
"SID": "HDB",
"TYPE": "HDB"
},
{
"InstanceType": "NW",
"NR": "80",
"SID": "WEB",
"TYPE": "WebDisp"
}
]
'''
from ansible.module_utils.basic import AnsibleModule
import os
import re
def get_all_hana_sid():
hana_sid = list()
if os.path.isdir("/hana/shared"):
# /hana/shared directory exists
for sid in os.listdir('/hana/shared'):
if os.path.isdir("/usr/sap/" + sid):
hana_sid = hana_sid + [sid]
if hana_sid:
return hana_sid
def get_all_nw_sid():
nw_sid = list()
if os.path.isdir("/sapmnt"):
# /sapmnt directory exists
for sid in os.listdir('/sapmnt'):
if os.path.isdir("/usr/sap/" + sid):
nw_sid = nw_sid + [sid]
else:
# Check to see if /sapmnt/SID/sap_bobj exists
if os.path.isdir("/sapmnt/" + sid + "/sap_bobj"):
# is a bobj system
nw_sid = nw_sid + [sid]
if nw_sid:
return nw_sid
def get_hana_nr(sids, module):
hana_list = list()
for sid in sids:
for instance in os.listdir('/usr/sap/' + sid):
if 'HDB' in instance:
instance_nr = instance[-2:]
# check if instance number exists
command = [module.get_bin_path('/usr/sap/hostctrl/exe/sapcontrol', required=True)]
command.extend(['-nr', instance_nr, '-function', 'GetProcessList'])
check_instance = module.run_command(command, check_rc=False)
# sapcontrol returns c(0 - 5) exit codes only c(1) is unavailable
if check_instance[0] != 1:
hana_list.append({'NR': instance_nr, 'SID': sid, 'TYPE': 'HDB', 'InstanceType': 'HANA'})
return hana_list
def get_nw_nr(sids, module):
nw_list = list()
type = ""
for sid in sids:
for instance in os.listdir('/usr/sap/' + sid):
instance_nr = instance[-2:]
command = [module.get_bin_path('/usr/sap/hostctrl/exe/sapcontrol', required=True)]
# check if returned instance_nr is a number because sapcontrol returns all if a random string is provided
if instance_nr.isdigit():
command.extend(['-nr', instance_nr, '-function', 'GetInstanceProperties'])
check_instance = module.run_command(command, check_rc=False)
if check_instance[0] != 1:
for line in check_instance[1].splitlines():
if re.search('INSTANCE_NAME', line):
# convert to list and extract last
type_raw = (line.strip('][').split(', '))[-1]
# split instance number
type = type_raw[:-2]
nw_list.append({'NR': instance_nr, 'SID': sid, 'TYPE': get_instance_type(type), 'InstanceType': 'NW'})
return nw_list
def get_instance_type(raw_type):
if raw_type[0] == "D":
# It's a PAS
type = "PAS"
elif raw_type[0] == "A":
# It's an ASCS
type = "ASCS"
elif raw_type[0] == "W":
# It's a Webdisp
type = "WebDisp"
elif raw_type[0] == "J":
# It's a Java
type = "Java"
elif raw_type[0] == "S":
# It's an SCS
type = "SCS"
elif raw_type[0] == "E":
# It's an ERS
type = "ERS"
else:
# Unknown instance type
type = "XXX"
return type
def run_module():
module_args = dict()
system_result = list()
result = dict(
changed=False,
ansible_facts=dict(),
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True,
)
hana_sid = get_all_hana_sid()
if hana_sid:
system_result = system_result + get_hana_nr(hana_sid, module)
nw_sid = get_all_nw_sid()
if nw_sid:
system_result = system_result + get_nw_nr(nw_sid, module)
if system_result:
result['ansible_facts'] = {'sap': system_result}
else:
result['ansible_facts']
if module.check_mode:
module.exit_json(**result)
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,340 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Rainer Leber <rainerleber@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: sap_task_list_execute
short_description: Perform SAP Task list execution
version_added: "0.1.0"
description:
- The M(community.sap.sap_task_list_execute) module depends on C(pyrfc) Python library (version 2.4.0 and upwards).
Depending on distribution you are using, you may need to install additional packages to
have these available.
- Tasks in the task list which requires manual activities will be confirmed automatically.
- This module will use the RFC package C(STC_TM_API).
requirements:
- pyrfc >= 2.4.0
- xmltodict
options:
conn_username:
description: The required username for the SAP system.
required: true
type: str
conn_password:
description: The required password for the SAP system.
required: true
type: str
host:
description: The required host for the SAP system. Can be either an FQDN or IP Address.
required: true
type: str
sysnr:
description:
- The system number of the SAP system.
- You must quote the value to ensure retaining the leading zeros.
default: '00'
type: str
client:
description:
- The client number to connect to.
- You must quote the value to ensure retaining the leading zeros.
default: '000'
type: str
task_to_execute:
description: The task list which will be executed.
required: true
type: str
task_parameters:
description:
- The tasks and the parameters for execution.
- If the task list does not need any parameters, this could be empty.
- If only specific tasks from the task list should be executed,
the tasks even when no parameter is needed must be provided
alongside with the module parameter I(task_skip=true).
type: list
elements: dict
suboptions:
TASKNAME:
description: The name of the task in the task list.
type: str
required: true
FIELDNAME:
description: The name of the field of the task.
type: str
VALUE:
description: The value which have to be set.
type: raw
task_settings:
description:
- Setting for the execution of the task list. This can be the following as in TCODE SE80 described.
Check Mode C(CHECKRUN), Background Processing Active C(BATCH) (this is the default value),
Asynchronous Execution C(ASYNC), Trace Mode C(TRACE), Server Name C(BATCH_TARGET).
default: ['BATCH']
type: list
elements: str
task_skip:
description:
- If this parameter is C(true), not defined tasks in I(task_parameters) are skipped.
- This could be the case when only certain tasks should run from the task list.
default: false
type: bool
notes:
- Does not support C(check_mode). Always returns that the state has changed.
author:
- Rainer Leber (@rainerleber)
'''
EXAMPLES = r'''
# Pass in a message
- name: Test task execution
community.sap.sap_task_list_execute:
conn_username: DDIC
conn_password: Passwd1234
host: 10.1.8.10
sysnr: '01'
client: '000'
task_to_execute: SAP_BASIS_SSL_CHECK
task_settings: batch
- name: Pass in input parameters
community.sap.sap_task_list_execute:
conn_username: DDIC
conn_password: Passwd1234
host: 10.1.8.10
sysnr: '00'
client: '000'
task_to_execute: SAP_BASIS_SSL_CHECK
task_parameters :
- { 'TASKNAME': 'CL_STCT_CHECK_SEC_CRYPTO', 'FIELDNAME': 'P_OPT2', 'VALUE': 'X' }
- TASKNAME: CL_STCT_CHECK_SEC_CRYPTO
FIELDNAME: P_OPT3
VALUE: X
task_settings: batch
# Exported environment variables
- name: Hint if module will fail with error message like ImportError libsapnwrfc.so...
community.sap.sap_task_list_execute:
conn_username: DDIC
conn_password: Passwd1234
host: 10.1.8.10
sysnr: '00'
client: '000'
task_to_execute: SAP_BASIS_SSL_CHECK
task_settings: batch
environment:
SAPNWRFC_HOME: /usr/local/sap/nwrfcsdk
LD_LIBRARY_PATH: /usr/local/sap/nwrfcsdk/lib
'''
RETURN = r'''
msg:
description: A small execution description.
type: str
returned: always
sample: 'Successful'
out:
description: A complete description of the executed tasks. If this is available.
type: list
elements: dict
returned: on success
sample: [...,{
"LOG": {
"STCTM_S_LOG": [
{
"ACTIVITY": "U_CONFIG",
"ACTIVITY_DESCR": "Configuration changed",
"DETAILS": null,
"EXEC_ID": "20210728184903.815739",
"FIELD": null,
"ID": "STC_TASK",
"LOG_MSG_NO": "000000",
"LOG_NO": null,
"MESSAGE": "For radiobutton group ICM too many options are set; choose only one option",
"MESSAGE_V1": "ICM",
"MESSAGE_V2": null,
"MESSAGE_V3": null,
"MESSAGE_V4": null,
"NUMBER": "048",
"PARAMETER": null,
"PERIOD": "M",
"PERIOD_DESCR": "Maintenance",
"ROW": "0",
"SRC_LINE": "170",
"SRC_OBJECT": "CL_STCTM_REPORT_UI IF_STCTM_UI_TASK~SET_PARAMETERS",
"SYSTEM": null,
"TIMESTMP": "20210728184903",
"TSTPNM": "DDIC",
"TYPE": "E"
},...
]}}]
'''
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
import traceback
try:
from pyrfc import Connection
except ImportError:
HAS_PYRFC_LIBRARY = False
PYRFC_LIBRARY_IMPORT_ERROR = traceback.format_exc()
else:
HAS_PYRFC_LIBRARY = True
try:
import xmltodict
except ImportError:
HAS_XMLTODICT_LIBRARY = False
XMLTODICT_LIBRARY_IMPORT_ERROR = traceback.format_exc()
else:
HAS_XMLTODICT_LIBRARY = True
def call_rfc_method(connection, method_name, kwargs):
# PyRFC call function
return connection.call(method_name, **kwargs)
def process_exec_settings(task_settings):
# processes task settings to objects
exec_settings = {}
for settings in task_settings:
temp_dict = {settings.upper(): 'X'}
for key, value in temp_dict.items():
exec_settings[key] = value
return exec_settings
def xml_to_dict(xml_raw):
try:
xml_parsed = xmltodict.parse(xml_raw, dict_constructor=dict)
xml_dict = xml_parsed['asx:abap']['asx:values']['SESSION']['TASKLIST']
except KeyError:
xml_dict = "No logs available."
return xml_dict
def run_module():
params_spec = dict(
TASKNAME=dict(type='str', required=True),
FIELDNAME=dict(type='str'),
VALUE=dict(type='raw'),
)
# define available arguments/parameters a user can pass to the module
module = AnsibleModule(
argument_spec=dict(
# values for connection
conn_username=dict(type='str', required=True),
conn_password=dict(type='str', required=True, no_log=True),
host=dict(type='str', required=True),
sysnr=dict(type='str', default="00"),
client=dict(type='str', default="000"),
# values for execution tasks
task_to_execute=dict(type='str', required=True),
task_parameters=dict(type='list', elements='dict', options=params_spec),
task_settings=dict(type='list', elements='str', default=['BATCH']),
task_skip=dict(type='bool', default=False),
),
supports_check_mode=False,
)
result = dict(changed=False, msg='', out={})
params = module.params
username = params['conn_username'].upper()
password = params['conn_password']
host = params['host']
sysnr = params['sysnr']
client = params['client']
task_parameters = params['task_parameters']
task_to_execute = params['task_to_execute']
task_settings = params['task_settings']
task_skip = params['task_skip']
if not HAS_PYRFC_LIBRARY:
module.fail_json(
msg=missing_required_lib('pyrfc'),
exception=PYRFC_LIBRARY_IMPORT_ERROR)
if not HAS_XMLTODICT_LIBRARY:
module.fail_json(
msg=missing_required_lib('xmltodict'),
exception=XMLTODICT_LIBRARY_IMPORT_ERROR)
# basic RFC connection with pyrfc
try:
conn = Connection(user=username, passwd=password, ashost=host, sysnr=sysnr, client=client)
except Exception as err:
result['error'] = str(err)
result['msg'] = 'Something went wrong connecting to the SAP system.'
module.fail_json(**result)
try:
raw_params = call_rfc_method(conn, 'STC_TM_SCENARIO_GET_PARAMETERS',
{'I_SCENARIO_ID': task_to_execute})
except Exception as err:
result['error'] = str(err)
result['msg'] = 'The task list does not exist.'
module.fail_json(**result)
exec_settings = process_exec_settings(task_settings)
# initialize session task
session_init = call_rfc_method(conn, 'STC_TM_SESSION_BEGIN',
{'I_SCENARIO_ID': task_to_execute,
'I_INIT_ONLY': 'X'})
# Confirm Tasks which requires manual activities from Task List Run
for task in raw_params['ET_PARAMETER']:
call_rfc_method(conn, 'STC_TM_TASK_CONFIRM',
{'I_SESSION_ID': session_init['E_SESSION_ID'],
'I_TASKNAME': task['TASKNAME']})
if task_skip:
for task in raw_params['ET_PARAMETER']:
call_rfc_method(conn, 'STC_TM_TASK_SKIP',
{'I_SESSION_ID': session_init['E_SESSION_ID'],
'I_TASKNAME': task['TASKNAME'], 'I_SKIP_DEP_TASKS': 'X'})
# unskip defined tasks and set parameters
if task_parameters is not None:
for task in task_parameters:
call_rfc_method(conn, 'STC_TM_TASK_UNSKIP',
{'I_SESSION_ID': session_init['E_SESSION_ID'],
'I_TASKNAME': task['TASKNAME'], 'I_UNSKIP_DEP_TASKS': 'X'})
call_rfc_method(conn, 'STC_TM_SESSION_SET_PARAMETERS',
{'I_SESSION_ID': session_init['E_SESSION_ID'],
'IT_PARAMETER': task_parameters})
# start the task
try:
session_start = call_rfc_method(conn, 'STC_TM_SESSION_RESUME',
{'I_SESSION_ID': session_init['E_SESSION_ID'],
'IS_EXEC_SETTINGS': exec_settings})
except Exception as err:
result['error'] = str(err)
result['msg'] = 'Something went wrong. See error.'
module.fail_json(**result)
# get task logs because the execution may successfully but the tasks shows errors or warnings
# returned value is ABAPXML https://help.sap.com/doc/abapdocu_755_index_htm/7.55/en-US/abenabap_xslt_asxml_general.htm
session_log = call_rfc_method(conn, 'STC_TM_SESSION_GET_LOG',
{'I_SESSION_ID': session_init['E_SESSION_ID']})
task_list = xml_to_dict(session_log['E_LOG'])
result['changed'] = True
result['msg'] = session_start['E_STATUS_DESCR']
result['out'] = task_list
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()