Source code for saltext.sap_nwabap._states.sap_nwabap

"""
SaltStack extension for SAP NetWeaver
Copyright (C) 2022 SAP UCC Magdeburg

SAP NetWeaver AS ABAP state module
==================================
SaltStack module that implements SAP NetWeaver states based on the SAP NetWeaver RFC SDK.

:codeauthor:    Benjamin Wegener, Alexander Wilke
:maturity:      new
:depends:       pyrfc
:platform:      All

This module implements states for SAP NetWeaver utilizing the SAP NetWeaver RFC SDK and the
python wrapper ``pyrfc``. The states uses SAP function modules to read the current state
of the system and write new information back.

.. warning::
    Not all function modules are supported by SAP, meaning that the can be removed by SAP at any
    time or in case of errors, they might not be fixed.
"""
import logging
from datetime import date
from datetime import datetime

import salt.utils.dictdiffer
import salt.utils.dictupdate

# Third Party libs
PYRFCLIB = True
try:
    from pyrfc import Connection
    from pyrfc._exception import ABAPApplicationError
except ImportError:
    PYRFCLIB = False

# Globals
log = logging.getLogger(__name__)

__virtualname__ = "sap_nwabap"

# USER_MAPPING = {<human_readable_name>: <sap_key>}
USER_MAPPING = {
    "ACCOUNT_ID": "ACCNT",
    "ADDRESS_NUMBER": "ADDR_NO",
    "ADDRESS_DATA": "ADDRESS",
    "ADDRESS_NOTES": "ADR_NOTES",
    "USERNAME_ALIAS": "ALIAS",
    "CUA_REDISTRIBUTION": "BACK_DISTRIBUTION",
    "USERNAME": "BAPIBNAME",
    "CUA_REDISTRIBUTION_FLAG": "BAPIFLAG",
    "PWD_HASH_KEY": "BCODE",
    "BIRTH_NAME": "BIRTH_NAME",
    "CHARGABLE_USER": "BNAME_CHARGEABLE",
    "BUILDING_CODE_LONG": "BUILD_LONG",
    "BUILDING_CODE": "BUILDING",
    "BUILDING_CODE_P": "BUILDING_P",
    "CO_NAME": "C_O_NAME",
    "CATT_TEST_STATUS": "CATTKENNZ",
    "CITY_FILE_STATUS": "CHCKSTATUS",
    "CITY": "CITY",
    "CITY_CODE": "CITY_NO",
    "USER_GROUP": "CLASS",
    "CLIENT": "CLIENT",
    "PWD_HASH_CODE_VERSION_C": "CODVC",
    "PWD_HASH_VERSION": "CODVN",
    "PWD_HASH_CODE_VERSION_S": "CODVS",
    "COMMUNICATION_METHOD": "COMM_TYPE",
    "COMPANY": "COMPANY",
    "COMPANY_ADDRESS": "COMPANY",
    "COUNTRY_KEY": "COUNTRY",
    "COUNTRY_SURCHARGE": "COUNTRY_SURCHARGE",
    "COUNTRY_KEY_ISO": "COUNTRYISO",
    "COUNTY": "COUNTY",
    "COUNTY_CODE": "COUNTY_CODE",
    "DATE_FORMAT": "DATFM",
    "DECIMAL_FORMAT": "DCPFM",
    "USER_DEFAULTS": "DEFAULTS",
    "DELIVERY_SERVICE_NUMBER": "DELI_SERV_NUMBER",
    "DELIVERY_SERVICE_TYPE": "DELI_SERV_TYPE",
    "POST_DELIVERY_DISTRICT": "DELIV_DIS",
    "DEPARTMENT": "DEPARTMENT",
    "DESCRIPTION": "DESCRIPTION",
    "DISTRICT_CODE": "DISTRCT_NO",
    "DISTRUCT": "DISTRICT",
    "PO_BOX_ADDRESS": "DONT_USE_P",
    "STREET_ADDRESS": "DONT_USE_S",
    "EMAIL": "E_MAIL",
    "EXTERNAL_ID": "EXTID",
    "EXTERNAL_ID_CHANGE_INDICATOR": "EXTIDSX",
    "FAX_NUMBER_EXTENSION": "FAX_EXTENS",
    "FAX_NUMBER": "FAX_NUMBER",
    "FIRST_NAME": "FIRSTNAME",
    "BUILDING_FLOOR": "FLOOR",
    "BUILDING_FLOOR_P": "FLOOR_P",
    "FULL_NAME": "FULLNAME",
    "FULL_NAME_STATUS": "FULLNAME_X",
    "FUNCTION": "FUNCTION",
    "USER_VALID_TO": "GLTGB",
    "USER_VALID_FROM": "GLTGV",
    "SNC_ALLOW_PW_LOGON": "GUIFLAG",
    "HOME_CITY": "HOME_CITY",
    "HOME_CITY_CODE": "HOMECITYNO",
    "HOUSE_NUMBER": "HOUSE_NO",
    "HOUSE_NUMBER_SUPPLEMENT": "HOUSE_NO2",
    "HOUSE_NUMER_RANGE": "HOUSE_NO3",
    "POSTAL_CODE_INTERNAL": "INHOUSE_ML",
    "INITIALS": "INITIALS",
    "SHORT_NAME": "INITS_SIG",
    "COST_CENTER": "KOSTL",
    "LOGON_LANGUAGE": "LANGU",
    "LANGUAGE_KEY": "LANGU",
    "LANGUAGE_RECORD_CREATION": "LANGU_CR_P",
    "LANGUAGE_KEY_SAP": "LANGU_ISO",
    "LANGUAGE_KEY_P": "LANGU_P",
    "LANGUAGE_KEY_SAP_CP": "LANGUCPISO",
    "LANGUAGE_KEY_SAP_P": "LANGUP_ISO",
    "LAST_NAME": "LASTNAME",
    "LICENSE_TYPE": "LIC_TYPE",
    "LOCATION": "LOCATION",
    "LOGON_DATA": "LOGONDATA",
    "LAST_LOGON_TIME": "LTIME",
    "MIDDLE_NAME": "MIDDLENAME",
    "NAME_COUNTRY_FORMAT_RULE": "NAMCOUNTRY",
    "NAME_1": "NAME",
    "NAME_2": "NAME_2",
    "NAME_3": "NAME_3",
    "NAME_4": "NAME_4",
    "NAME_FORMAT": "NAMEFORMAT",
    "NICKNAME": "NICKNAME",
    "PWD_HASH_VALUE_SHA1": "PASSCODE",
    "PO_BOX_CITY_CODE": "PBOXCIT_NO",
    "POSTAL_CODE_EXTENSION_1": "PCODE1_EXT",
    "POSTAL_CODE_EXTENSION_2": "PCODE2_EXT",
    "POSTAL_CODE_EXTENSION_3": "PCODE3_EXT",
    "PERSON_NUMBER": "PERS_NO",
    "SNC_PRINTABLE_NAME": "PNAME",
    "PO_BOX": "PO_BOX",
    "PO_BOX_CITY": "PO_BOX_CIT",
    "PO_BOX_LOBBY": "PO_BOX_LOBBY",
    "PO_BOX_REGION": "PO_BOX_REG",
    "PO_COUNTRY_ISO": "PO_CTRYISO",
    "PO_BOX_NO_NUMBER_FLAG": "PO_W_O_NO",
    "PO_BOX_COUNTRY": "POBOX_CTRY",
    "POSTAL_CODE": "POSTL_COD1",
    "PO_POSTAL_CODE": "POSTL_COD2",
    "COMPANY_POSTAL_CODE": "POSTL_COD3",
    "NAME_PREFIX_1": "PREFIX1",
    "NAME_PREFIX_2": "PREFIX2",
    "PWD_HASH_VALUE": "PWDSALTEDHASH",
    "REFERENCE_USER": "REF_USER",
    "REFERENCE_USERNAME": "REF_USER",
    "REGIONAL_STRUCTURE_GROUPING": "REGIOGROUP",
    "REGION": "REGION",
    "TECH_USER_ACCOUNT_RESPONSIBLE": "RESPONSIBLE",
    "APARTMENT_NUMBER": "ROOM_NO",
    "APARTMENT_NUMBER_P": "ROOM_NO_P",
    "SECOND_NAME": "SECONDNAME",
    "SECURITY_POLICY": "SECURITY_POLICY",
    "SNC": "SNC",
    "SEARCH_TERM_1": "SORT1",
    "SEARCH_TERM_1_P": "SORT1_P",
    "SEARCH_TERM_2": "SORT2",
    "SEARCH_TERM_2_P": "SORT2_P",
    "PRINT_PARAM_3": "SPDA",
    "PRINT_PARAM_2": "SPDB",
    "USER_CLASS_SPECIAL_VERSION": "SPEC_VERS",
    "SPOOL_OUTPUT_DEVICE": "SPLD",
    "PRINT_PARAM_1": "SPLG",
    "START_MENU": "START_MENU",
    "START_MENU_OLD": "STCOD",
    "STREET_ABBREVIATION": "STR_ABBR",
    "STREET_SUPPLEMENT_1": "STR_SUPPL1",
    "STREET_SUPPLEMENT_2": "STR_SUPPL2",
    "STREET_SUPPLEMENT_3": "STR_SUPPL3",
    "STREET": "STREET",
    "STREET_NUMBER": "STREET_NO",
    "USER_CLASSIFICATION_VALID_FROM": "SUBSTITUTE_FROM",
    "USER_CLASSIFICATION_VALID_TO": "SUBSTITUTE_UNTIL",
    "SYSTEM_ID": "SYSID",
    "TAX_JURISDICTION": "TAXJURCODE",
    "TECH_USER_DESCRIPTION": "TECHDESC",
    "TEL_NUMBER_EXTENSION": "TEL1_EXT",
    "TEL_NUMBER": "TEL1_NUMBR",
    "ADDRESS_TIME_ZONE": "TIME_ZONE",
    "TIME_FORMAT": "TIMEFM",
    "TITLE_TEXT": "TITLE",
    "ACADEMIC_TITLE_1": "TITLE_ACA1",
    "ACADEMIC_TITLE_2": "TITLE_ACA2",
    "TITLE_P": "TITLE_P",
    "NAME_SUPPLEMENT": "TITLE_SPPL",
    "TOWNSHIP": "TOWNSHIP",
    "TOWNSHIP_CODE": "TOWNSHIP_CODE",
    "TRANSPORTATION_ZONE": "TRANSPZONE",
    "TIME_ZONE": "TZONE",
    "USER_CLASSIFICATION": "UCLASS",
    "USER_INTERNET_ALIAS": "USERALIAS",
    "USER_NAME": "USERNAME",
    "USER_TYPE": "USTYP",
    "BUSINESS_PURPOSE_FLAG": "XPCPT",
}

# this is a 1:1 mapping because the parameter names are already pretty good
RFC_MAPPING = {
    "ACCEPT_COOKIE": "ACCEPT_COOKIE",
    "ARFC_ACTIVE": "ARFC_ACTIVE",
    "ARFC_CYCLE": "ARFC_CYCLE",
    "ARFC_METHOD": "ARFC_METHOD",
    "ASSERTION_TICKET": "ASSERTION_TICKET",
    "ASSERTION_TICKET_CLIENT": "ASSERTION_TICKET_CLIENT",
    "ASSERTION_TICKET_SYSID": "ASSERTION_TICKET_SYSID",
    "AUTHORIZATION_PARAMETER": "AUTHORIZATION_PARAMETER",
    "BASXML_ACTIVE": "BASXML_ACTIVE",
    "CALLBACK_WHITELIST": "CALLBACK_WHITELIST",
    "CALLBACK_WHITELIST_ACTIVE": "CALLBACK_WHITELIST_ACTIVE",
    "CATEGORY": "CATEGORY",
    "CLIENT_CODEPAGE_ACTIVE": "CLIENT_CODEPAGE_ACTIVE",
    "COMPRESS_REPLY": "COMPRESS_REPLY",
    "CONVERSION_BYTES": "CONVERSION_BYTES",
    "CONVERSION_MODE": "CONVERSION_MODE",
    "CPIC_TIMEOUT": "CPIC_TIMEOUT",
    "DESCRIPTION": "DESCRIPTION",
    "ENABLE_TRACE": "ENABLE_TRACE",
    "EXPLICIT_CODEPAGE": "EXPLICIT_CODEPAGE",
    "EXPLICIT_CODEPAGE_ACTIVE": "EXPLICIT_CODEPAGE_ACTIVE",
    "EXPORT_TRACE": "EXPORT_TRACE",
    "GATEWAY_HOST": "GATEWAY_HOST",
    "GATEWAY_SERVICE": "GATEWAY_SERVICE",
    "GROUP_NAME": "GROUP_NAME",
    "HTTP_COMPRESS": "HTTP_COMPRESS",
    "HTTP_TIMEOUT": "HTTP_TIMEOUT",
    "HTTP_VERSION": "HTTP_VERSION",
    "KEEP_PASSWORD": "KEEP_PASSWORD",
    "KEEP_PROXY_PASSWORD": "KEEP_PROXY_PASSWORD",
    "KEEPALIVE_TIMEOUT": "KEEPALIVE_TIMEOUT",
    "LANGUAGE_CODEPAGE_ACTIVE": "LANGUAGE_CODEPAGE_ACTIVE",
    "LOAD_BALANCING": "LOAD_BALANCING",
    "LOGON_CLIENT": "LOGON_CLIENT",
    "LOGON_LANGUAGE": "LOGON_LANGUAGE",
    "LOGON_METHOD": "LOGON_METHOD",
    "LOGON_USER": "LOGON_USER",
    "LOGON_USER_254": "LOGON_USER_254",
    "MDMP_LIST": "MDMP_LIST",
    "MDMP_SETTINGS_ACTIVE": "MDMP_SETTINGS_ACTIVE",
    "METHOD": "METHOD",
    "NAME": "NAME",
    "PATH_PREFIX": "PATH_PREFIX",
    "PROGRAM": "PROGRAM",
    "PROXY_SERVER": "PROXY_SERVER",
    "PROXY_SERVICE_NUMBER": "PROXY_SERVICE_NUMBER",
    "PROXY_USER": "PROXY_USER",
    "QRFC_VERSION": "QRFC_VERSION",
    "REFERENCE": "REFERENCE",
    "RFC_BITMAP": "RFC_BITMAP",
    "RFC_WAN": "RFC_WAN",
    "RFCLOGON_GUI": "RFCLOGON_GUI",
    "SAME_USER": "SAME_USER",
    "SAVE_AS_HOSTNAME": "SAVE_AS_HOSTNAME",
    "SERVER_NAME": "SERVER_NAME",
    "SERVICE_NUMBER": "SERVICE_NUMBER",
    "SNC_ACTIVE": "SNC_ACTIVE",
    "SNC_PARAMETER": "SNC_PARAMETER",
    "SSL_ACTIVE": "SSL_ACTIVE",
    "SSL_APPLICATION": "SSL_APPLICATION",
    "SSO_TICKET": "SSO_TICKET",
    "START_TYPE": "START_TYPE",
    "SYSTEM_IDENTIFIER": "SYSTEM_IDENTIFIER",
    "SYSTEM_NUMBER": "SYSTEM_NUMBER",
    "TRACE_SETTINGS": "TRACE_SETTINGS",
    "TRFC_BG_DELAY": "TRFC_BG_DELAY",
    "TRFC_BG_REPETITIONS": "TRFC_BG_REPETITIONS",
    "TRFC_BG_SUPRESS": "TRFC_BG_SUPRESS",
    "TRUSTED_SYSTEM": "TRUSTED_SYSTEM",
    "UI_LOCK": "UI_LOCK",
    "UNICODE_BYTES": "UNICODE_BYTES",
    "UPDATE_ALL": "UPDATE_ALL",
    "UPDATE_FIELDS": "UPDATE_FIELDS",
}

# JOB_HEADER_MAPPING = {<HUMAN_READABLE_NAME>: <SAP_KEY>}
JOB_HEADER_MAPPING = {
    "PLANNED_START_DATE": "SDLSTRTDT",  # PLANNED START DATE FOR BACKGROUND JOB
    "PLANNED_START_TIME": "SDLSTRTTM",  # PLANNED START TIME FOR BACKGROUND JOB
    "LAST_START_DATE": "LASTSTRTDT",  # LATEST EXECUTION DATE FOR A BACKGROUND JOB
    "LAST_START_TIME": "LASTSTRTTM",  # LATEST EXECUTION TIME FOR BACKGROUND JOB
    "PREDECESSOR_JOB_NAME": "PREDJOB",  # NAME OF PREVIOUS JOB
    "PREDECESSOR_JOB_ID": "PREDJOBCNT",  # JOB ID
    "JOB_STATUS_CHECK": "CHECKSTAT",  # JOB STATUS CHECK INDICATOR FOR SUBSEQUENT JOB START
    "EVENT_ID": "EVENTID",  # BACKGROUND PROCESSING EVENT
    "EVENT_PARAM": "EVENTPARM",  # BACKGROUND EVENT PARAMETERS (SUCH AS, JOBNAME/JOBCOUNT)
    "DURATION_MIN": "PRDMINS",  # DURATION PERIOD (IN MINUTES) FOR A BATCH JOB
    "DURATION_HOURS": "PRDHOURS",  # DURATION PERIOD (IN HOURS) FOR A BATCH JOB
    "DURATION_DAYS": "PRDDAYS",  # DURATION (IN DAYS) OF DBA ACTION
    "DURATION_WEEKS": "PRDWEEKS",  # DURATION PERIOD (IN WEEKS) FOR A BATCH JOB
    "DURATION_MONTHS": "PRDMONTHS",  # DURATION PERIOD (IN MONTHS) FOR A BATCH JOB
    "PERIODIC": "PERIODIC",  # PERIODIC JOBS INDICATOR
    "CALENDAR_ID": "CALENDARID",  # FACTORY CALENDAR ID FOR BACKGROUND PROCESSING
    "CAN_START_IMMEDIATELY": "IMSTRTPOS",  # FLAG INDICATING WHETHER JOB CAN BE STARTED IMMEDIATELY
    "PERIODIC_BEHAVIOR": "PRDBEHAV",  # PERIOD BEHAVIOR OF JOBS ON NON-WORKDAYS
    "WORKDAY_NUMBER": "WDAYNO",  # NO. OF WORKDAY ON WHICH A JOB IS TO START
    "WORKDAY_COUNT_DIRECTION": "WDAYCDIR",  # COUNT DIRECTION FOR 'ON WORKDAY' START DATE OF A JOB
    "NOT_RUN_BEFORE": "NOTBEFORE",  # PLANNED START DATE FOR BACKGROUND JOB
    "OPERATION_MODE": "OPMODE",  # NAME OF OPERATION MODE
    "LOGICAL_SYSTEM": "LOGSYS",  # LOGICAL SYSTEM
    "OBJECT_TYPE": "OBJTYPE",  # OBJECT TYPE
    "OBJECT_KEY": "OBJKEY",  # OBJECT KEY
    "DESCRIBE_FLAG": "DESCRIBE",  # DESCRIBE FLAG
    "TARGET_SERVER": "TSERVER",  # SERVER NAME
    "TARGET_HOST": "THOST",  # TARGET SYSTEM TO RUN BACKGROUND JOB
    "TARGET_SERVER_GROUP": "TSRVGRP",  # SERVER GROUP NAME BACKGROUND PROCESSING
}

# JOB_STEPS_MAPPING = {<HUMAN_READABLE_NAME>: <SAP_KEY>}
JOB_STEPS_MAPPING = {
    "PROGRAM_NAME": "ABAP_PROGRAM_NAME",  # ABAP PROGRAM NAME
    "PROGRAM_VARIANT": "ABAP_VARIANT_NAME",  # ABAP VARIANT NAME
    "USERNAME": "SAP_USER_NAME",  # SAP USER NAME FOR AUTHORIZATION CHECK
    "LANGUAGE": "LANGUAGE",  # LANGUAGE FOR LIST OUTPUT
}

# job header fields that are mapped to the STARTCOND mask
JOB_MASK_STARTCOND = [
    "WDAYNO",
    "PRDMINS",
    "PRDHOURS",
    "PRDDAYS",
    "PRDWEEKS",
    "PRDMONTHS",
    "SDLSTRTDT",
    "IMSTRTPOS",
    "EVENTID",
    "PREDJOB",
    "OPMODE",
]

# job header fields that are mapped to the RECIPLNT mask
JOB_MASK_RECIPLNT = ["REC_TYPE"]

# job modify step mapping {"<modify param>": "<read param>"}
JOB_STEP_MODIFY_MAPPING = {
    "PROGRAM": "ABAP_PROGRAM_NAME",
    "PARAMETER": "ABAP_VARIANT_NAME",
    "AUTHCKNAM": "SAP_USER_NAME",
    "LANGUAGE": "LANGUAGE",
}


[docs]def __virtual__(): """ Only load this module if all libraries are available. """ if not PYRFCLIB: return False, "Could not load state module, pyrfc unavailable" return __virtualname__
def _replace_human_readable(dic, mapping, remove_unknown=True): """ Takes a dictionary and replaces all keys with the SAP names based on a given mapping (recursively). Unknown keys will be removed. """ if not isinstance(dic, dict): return dic else: new_d = {} for key, value in dic.items(): new_k = None if key.upper() in mapping.keys(): new_k = mapping[key.upper()] elif key.upper() in mapping.values(): new_k = key.upper() elif not remove_unknown: log.debug("The key '{}' is not present in the mapping table, but adding anyway") new_k = key.upper() else: continue if isinstance(value, dict): new_d[new_k] = _replace_human_readable(value, mapping, remove_unknown) else: new_d[new_k] = value return new_d def _convert_date(datestring): """ Takes a string and converts it into a date. """ log.debug(f"Converting '{datestring}' to date") new_d = False if isinstance(datestring, date): return datestring elif isinstance(datestring, int): datestring = str(datestring) formats = [ "%Y-%m-%d", # '2014-12-04' "%d-%m-%Y", # '04-12-2014' "%Y%m%d", # '20141204' "%d%m%Y", # '04122014' ] for form in formats: try: new_d = datetime.strptime(datestring, form).date() except ValueError: continue log.debug(f"Used format '{form}' to convert '{datestring}'") return new_d log.warning(f"Could not convert date '{datestring}' to a python date object") return False def _generate_change_flag_dict(dic): """ Generates a dictionary with flags for each key present. """ if not isinstance(dic, dict): return dic new_d = {} for key, value in dic.items(): if isinstance(value, dict): new_d[key] = _generate_change_flag_dict(value) else: new_d[key] = "X" return new_d def _clear_empty_dict(dic, remove_none=True): """ Recursively remove empty sub dictionaries. """ if not isinstance(dic, dict): return dic new_d = {} for key, value in dic.items(): if isinstance(value, dict): value = _clear_empty_dict(value, remove_none) if value: new_d[key] = value else: if not remove_none: if value is not None: new_d[key] = value else: new_d[key] = value return new_d def _extract_changed_from_dict(changed_dict, dic): """ Will extract all data from ``dic`` if corresponding keys exist in ``changed_dict``. """ new_d = {} for key, value in changed_dict.items(): if key in dic and isinstance(value, dict): new_d[key] = _extract_changed_from_dict(value, dic[key]) elif key in dic: new_d[dic] = dic[key] return new_d def _get_bapiret2_messages(ret): """ Retrieve all MESSAGE fields from the result and return them as a list. """ if isinstance(ret, dict): ret = [ret] return [message.get("MESSAGE", "") for message in ret]
[docs]def icm_notified( name, sid, client, message_server_host, message_server_port, logon_group, username, password, invalidate_cache=True, reset_ni_buffer=True, ): """ Notify the ICM that a PSE has changed and refresh caches if required name: Name of the PSE file, e.g. ``SAPSSLS.pse``. sid SID of the SAP system. message_server_host Host of the message server. message_server_port Port of the message server. client Client to connect to. logon_group Logon group to use. username Username to use for the connection. password Password to use for the connection. invalidate_cache Boolean if the ICM cache should be invalidated, default is ``True``. reset_ni_buffer Boolean if the network interface buffer should be reset, default is ``True``. .. note:: If the ``SAP_BASIS`` release of the system is <= 701, you need to restart the ICM. Example: .. code-block:: jinja ICM on S4H is notified on changes to SSLS PSEs: sap_nwabap.icm_notified: - name: SAPSSLS.pse - invalidate_cache: True - reset_ni_buffer: True - sid: S4H - client: "000" - message_server_host: s4h - message_server_port: 3600 - logon_group: SPACE - username: SALT - password: __slot__:salt:vault.read_secret(path="nwabap/S4H/000", key="SALT") """ log.debug("Running function") ret = {"name": name, "changes": {"old": [], "new": []}, "comment": "", "result": False} if isinstance(client, int): client = f"{client:03d}" abap_connection = { "mshost": message_server_host, "msserv": str(message_server_port), "sysid": sid, "group": logon_group, "client": client, "user": username, "passwd": password, "lang": "EN", } with Connection(**abap_connection) as conn: log.debug(f"Notifying ICM of system {sid}") if __opts__["test"]: ret["changes"]["new"].append("Would have notified ICM about PSE changes") else: function_modules = {"ICM_SSL_PSE_CHANGED": {"GLOBAL": 1, "CRED_NAME": name}} success, _ = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not notify the ICM of system {sid}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret ret["changes"]["new"].append("Notified ICM about PSE changes") if invalidate_cache: log.debug("Invalidating cache") if __opts__["test"]: ret["changes"]["new"].append("Would have invalidated ICM cache") else: function_modules = { "ICM_CACHE_INVALIDATE_ALL": { "GLOBAL": 1, } } success, _ = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not invalidate the ICM cache of system {sid}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret ret["changes"]["new"].append("Invalidated ICM cache") if reset_ni_buffer: log.debug("Resetting ICM network interfaces buffers") if __opts__["test"]: ret["changes"]["new"].append("Would have reset ICM network interface buffer") else: function_modules = {"ICM_RESET_NIBUFFER": {"GLOBAL": 1}} success, _ = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not reset the ICM network interface buffer of system {sid}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret ret["changes"]["new"].append("Reset ICM network interface buffer") if __opts__["test"]: ret["comment"] = "Would have notified ICM" else: ret["comment"] = "Notified ICM" ret["result"] = True if (not __opts__["test"] or not ret["changes"]) else None return ret
[docs]def icm_restarted( name, sid, client, message_server_host, message_server_port, logon_group, username, password, restart_mode="soft", ): """ Ensure that the ICM is restarted name: An arbitrary string. sid SID of the SAP system. message_server_host Host of the message server. message_server_port Port of the message server. client Client to connect to. logon_group Logon group to use. username Username to use for the connection. password Password to use for the connection. restart_mode Restart mode, either ``soft`` or ``hard``, default is ``soft``. Example: .. code-block:: jinja ICM on S4H is restarted on changes to SSLS PSEs: sap_nwabap.icm_restarted: - name: ICM restarted - sid: S4H - client: "000" - message_server_host: s4h - message_server_port: 3600 - logon_group: SPACE - username: SALT - password: __slot__:salt:vault.read_secret(path="nwabap/S4H/000", key="SALT") - restart_mode: hard """ log.debug("Running function") ret = {"name": name, "changes": {}, "comment": "", "result": False} if restart_mode.lower() == "soft": restart_mode = 15 elif restart_mode.lower() == "hard": restart_mode = 16 else: msg = f"Unknown restart mode '{restart_mode}'" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if isinstance(client, int): client = f"{client:03d}" abap_connection = { "mshost": message_server_host, "msserv": str(message_server_port), "sysid": sid, "group": logon_group, "client": client, "user": username, "passwd": password, "lang": "EN", } with Connection(**abap_connection) as conn: log.debug(f"Restarting the ICM of system {sid}") if __opts__["test"]: ret["comment"] = "Would have restarted the ICM" ret["changes"] = {"old": "Running", "new": "Would have been restarted"} else: function_modules = {"ICM_SHUTDOWN_ICM": {"GLOBAL": 1, "HOW": restart_mode}} success, _ = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not restart the ICM of system {sid}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret ret["comment"] = "Restarted the ICM" ret["changes"] = {"old": "Running", "new": "Restarted"} ret["result"] = True if (not __opts__["test"] or not ret["changes"]) else None return ret
# pylint: disable=unused-argument
[docs]def user_present( name, sid, client, message_server_host, message_server_port, logon_group, username, password, user_password=None, attributes=None, roles=None, profiles=None, unlock_user=True, **kwargs, ): """ Ensures that a user is present in the SAP system. name Username. sid SID of the SAP system. message_server_host Host of the message server. message_server_port Port of the message server. client Client to connect to. logon_group Logon group to use. username Username to use for the connection. password Password to use for the connection. user_password Password for the user. If None, no one will be set. attributes All attributes of the user object as a dictionary (see below). roles List of roles to assign to the user; list of dictionaries: .. code-block:: yaml - name: <role name> valid_from: <date string> # default is today valid_to: <date string> # default 31-12-9999 - name: Z_MY_ROLE_1 - name: Z_MY_ROLE_2 valid_from: 30-11-2000 valid_to: 99991231 profiles List of profiles to assign to the user (list of strings). unlock_user ``True|False`` if the user should be unlocked, default is ``True``. The dictionary provided over ``attributes`` must look like the following. Alternativly, the SAP struct names (see constant ``USER_MAPPING``) can be used. Both upper- and lowercase are supported. .. code-block:: yaml address_data: address_number: <value> address_notes: <value> birth_name: <value> building_code_long: <value> building_code: <value> building_code_p: <value> co_name: <value> city_file_status: <value> city: <value> city_code: <value> communication_method: <value> country_key: <value> country_key_iso: <value> county: <value> county_code: <value> delivery_service_number: <value> delivery_service_type: <value> post_delivery_district: <value> department: <value> district_code: <value> district: <value> po_box_address: <value> street_address: <value> email: <value> fax_number_extension: <value> fax_number: <value> first_name: <value> building_floor: <value> building_floor_p: <value> full_name: <value> full_name_status: <value> function: <value> home_city: <value> home_city_code: <value> house_number: <value> house_number_supplement: <value> house_numer_range: <value> postal_code_internal: <value> initials: <value> short_name: <value> language_key: <value> language_record_creation: <value> language_key_sap: <value> language_key_p: <value> language_key_sap_cp: <value> language_key_sap_p: <value> last_name: <value> location: <value> middle_name: <value> name_country_format_rule: <value> name: <value> name_2: <value> name_3: <value> name_4: <value> name_format: <value> nickname: <value> po_box_city_code: <value> postal_code_extension_1: <value> postal_code_extension_2: <value> postal_code_extension_3: <value> person_number: <value> po_box: <value> po_box_city: <value> po_box_lobby: <value> po_box_region: <value> po_country_iso: <value> po_box_no_number_flag: <value> po_box_country: <value> postal_code: <value> po_postal_code: <value> company_postal_code: <value> name_prefix_1: <value> name_prefix_2: <value> regional_structure_grouping: <value> region: <value> apartment_number: <value> apartment_number_p: <value> second_name: <value> search_term_1: <value> search_term_1_p: <value> search_term_2: <value> search_term_2_p: <value> street_abbreviation: <value> street_supplement_1: <value> street_supplement_2: <value> street_supplement_3: <value> street: <value> street_number: <value> tax_jurisdiction: <value> tel_number_extension: <value> tel_number: <value> address_time_zone: <value> title_text: <value> academic_title_1: <value> academic_title_2: <value> title_p: <value> name_supplement: <value> township: <value> township_code: <value> transpzone: <value> business_purpose_flag: <value> username_alias: useralias: <value> cua_redistribution: <value> company: company_address: <value> user_defaults: catt_test_status: <value> date_format: <value> decimal_format: <value> user_defaults: <value> cost_center: <value> logon_language: <value> print_param_3: <value> print_param_2: <value> spool_output_device: <value> print_param_1: <value> start_menu: <value> start_menu_old: <value> time_format: <value> description: tech_user_account_responsible: <value> techdesc: <value> external_id_change_indicator: external_id: <value> logon_data: account_id: <value> pwd_hash_key: <value> user_group: <value> pwd_hash_code_version_c: <value> pwd_hash_version: <value> pwd_hash_code_version_s: <value> user_valid_to: <value> user_valid_from: <value> last_logon_time: <value> pwd_hash_value_sha1: <value> pwd_hash_value: <value> security_policy: <value> time_zone: <value> user_type: <value> reference_user: reference_username: <value> snc: snc_allow_pw_logon: <value> snc_printable_name: <value> user_classification: chargable_user: <value> client: <value> country_surcharge: <value> license_type: <value> user_class_special_version: <value> substitute_from: <value> substitute_until: <value> system_id: <value> user_classification: <value> .. warning:: This state will not check if the inputs in terms of user data are valid! Example: .. code-block:: jinja Technical user SALT for SAP system S4H / client 000 is present: sap_nwabap.user_present: - name: SALT - sid: S4H - client: "000" - message_server_host: s4h - message_server_port: 3600 - logon_group: SPACE - username: DDIC - password: __slot__:salt:vault.read_secret(path="nwabap/S4H/000", key="DDIC") - user_password: __slot__:salt:vault.read_secret(path="nwabap/S4H/000", key="SALT") - attributes: logon_data: user_type: B user_valid_to: "99991231" address_data: first_name: SALT_SERVICE_USER last_name: SALT_SERVICE_USER - roles: - name: Z_SALT_ROLE valid_to: 99991231 - profiles: - SAP_ALL - unlock_user: True """ def _compare_role_lists(old, new): """ Takes two lists of role dictionaries and compares them. """ dict_representation_old = { e["AGR_NAME"]: {"FROM_DAT": e["FROM_DAT"], "TO_DAT": e["TO_DAT"]} for e in old } dict_representation_new = { e["AGR_NAME"]: {"FROM_DAT": e["FROM_DAT"], "TO_DAT": e["TO_DAT"]} for e in new } # compare elements if dict_representation_old.keys() != dict_representation_new.keys(): return False # compare validity dates for k, v_old in dict_representation_old.items(): if v_old.get("FROM_DAT", date.today()) > dict_representation_new[k].get( "FROM_DAT", date.today() ): return False if v_old.get("TO_DAT", date(9999, 12, 31)) != dict_representation_new[k].get( "TO_DAT", date(9999, 12, 31) ): return False return True def _convert_date_fields(dic): """ Convert the date fields from string to dict """ log.debug(f"Converting date fields of {dic}") if dic.get("LOGONDATA", {}).get("GLTGB"): log.debug("Converting LOGONDATA:GLTGB to date object") converted_date = _convert_date(dic["LOGONDATA"]["GLTGB"]) if not converted_date: return False dic["LOGONDATA"]["GLTGB"] = converted_date if dic.get("LOGONDATA", {}).get("GLTGV"): log.debug("Converting LOGONDATA:GLTGB to date object") converted_date = _convert_date(dic["LOGONDATA"]["GLTGV"]) if not converted_date: return False dic["LOGONDATA"]["GLTGV"] = converted_date if dic.get("UCLASS", {}).get("SUBSTITUTE_FROM"): log.debug("Converting UCLASS:SUBSTITUTE_FROM to date object") converted_date = _convert_date(dic["UCLASS"]["SUBSTITUTE_FROM"]) if not converted_date: return False dic["UCLASS"]["SUBSTITUTE_FROM"] = converted_date if dic.get("UCLASS", {}).get("SUBSTITUTE_UNTIL"): log.debug("Converting UCLASS:SUBSTITUTE_UNTIL to date object") converted_date = _convert_date(dic["UCLASS"]["SUBSTITUTE_UNTIL"]) if not converted_date: return False dic["UCLASS"]["SUBSTITUTE_UNTIL"] = converted_date return dic log.debug("Running function") ret = {"name": name, "changes": {"old": {}, "new": {}}, "comment": "", "result": False} user = name.upper() if not roles: roles = [] if not profiles: profiles = [] log.debug(f"Roles to set: {roles}") log.debug(f"Profiles to set: {profiles}") log.debug("Parsing attributes and replacing human-readable ") if not attributes: attributes = {} else: attributes = _replace_human_readable(attributes, mapping=USER_MAPPING, remove_unknown=True) log.debug("Converting date field inputs") attributes = _convert_date_fields(attributes) if isinstance(attributes, bool) and not attributes: msg = "Invalid value for a date field" log.error(msg) ret["comment"] = msg ret["result"] = False return ret log.debug("Get usertype from input") user_type = attributes.get("LOGONDATA", {}).get("USTYP", None) log.debug("Creating one connection for the lifetime of this state") if isinstance(client, int): client = f"{client:03d}" abap_connection = { "mshost": message_server_host, "msserv": str(message_server_port), "sysid": sid, "group": logon_group, "client": client, "user": username, "passwd": password, "lang": "EN", } with Connection(**abap_connection) as conn: log.debug(f"Retrieving information about {user} from SAP system {sid}") function_modules = {"BAPI_USER_GET_DETAIL": {"USERNAME": user}} success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not retrieve information about {user} from SAP system {sid}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret log.trace(f"Got user:\n{result}") user_exists = __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_USER_GET_DETAIL"]["RETURN"] ) if user_exists: log.debug("User already exists, checking for required updates") profiles_in_sys = [ x["BAPIPROF"] for x in result["BAPI_USER_GET_DETAIL"].pop("PROFILES", []) ] roles_in_sys = result["BAPI_USER_GET_DETAIL"].pop("ACTIVITYGROUPS", []) log.debug(f"Existing profiles: {profiles_in_sys}") log.debug(f"Existing roles: {roles_in_sys}") log.debug("Converting date field inputs") user_details = _convert_date_fields(result["BAPI_USER_GET_DETAIL"]) if isinstance(user_details, bool) and not user_details: msg = "Invalid value for a date field" log.error(msg) ret["comment"] = msg ret["result"] = False return ret # check if we need to unlock as long as we have the results lock_status = user_details["ISLOCKED"] locked = False for lock_type in ["WRNG_LOGON", "LOCAL_LOCK", "GLOB_LOCK"]: if lock_status[lock_type] == "L": locked = True break if unlock_user and locked: log.debug(f"User {user} is locked and needs to be unlocked") if not __opts__["test"]: function_modules = { "BAPI_USER_UNLOCK": {"USERNAME": user}, "BAPI_TRANSACTION_COMMIT": {"WAIT": "X"}, } success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not unlock {user}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_USER_UNLOCK"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages( result["BAPI_USER_UNLOCK"]["RETURN"] ) ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_TRANSACTION_COMMIT"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages( result["BAPI_TRANSACTION_COMMIT"]["RETURN"] ) ret["result"] = False return ret if not user_type: log.debug("Retrieve user type") user_type = user_details["LOGONDATA"]["USTYP"] log.debug(f"Calculating diffs for update of user {user}") diff = salt.utils.dictdiffer.deep_diff(user_details, attributes) log.debug("Removing empty dictionaries") diff = _clear_empty_dict( diff, remove_none=False ) # this will also remove empty "new" and "old" dicts new = diff.get("new", {}) # because we're only interested in the changed fields, # we remove unchanged stuff from "old" old = _extract_changed_from_dict(new, diff.get("old", {})) if new: log.debug(f"Data for {user} has changed, we need to update. Changes:\n{new}") if not __opts__["test"]: function_modules = { "BAPI_USER_CHANGE": {"USERNAME": user}, "BAPI_TRANSACTION_COMMIT": {"WAIT": "X"}, } log.debug("Adding change flags") for k in [ "LOGONDATA", "DEFAULTS", "ADDRESS", "COMPANY", "SNC", "REF_USER", "ALIAS", "UCLASS", "DESCRIPTION", ]: if k in attributes: attributes[k + "X"] = _generate_change_flag_dict(attributes[k]) function_modules = salt.utils.dictupdate.merge( function_modules, {"BAPI_USER_CHANGE": attributes}, merge_lists=True, strategy="smart", ) success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not change {user}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_USER_CHANGE"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages( result["BAPI_USER_CHANGE"]["RETURN"] ) ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_TRANSACTION_COMMIT"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages( result["BAPI_TRANSACTION_COMMIT"]["RETURN"] ) ret["result"] = False return ret ret["changes"] = {"new": new, "old": old} # only process the password if it is given if user_password: log.debug("Checking if password needs to be updated") log.debug("Checking if logon is possible") function_modules = { "SUSR_LOGIN_CHECK_RFC": {"BNAME": user, "PASSWORD": user_password} } password_correct = True try: success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn, raise_on_error=True ) except ABAPApplicationError as aae: # unsuccessful logons are output as exceptions """ EXCEPTIONS WAIT = 1 " USER_LOCKED = 2 " User is locked USER_NOT_ACTIVE = 3 " PASSWORD_EXPIRED = 4 " WRONG_PASSWORD = 5 " NO_CHECK_FOR_THIS_USER = 6 " PASSWORD_ATTEMPTS_LIMITED = 7 " """ # pylint: disable=pointless-string-statement if aae.key == "WRONG_PASSWORD": log.debug("User password is wrong") password_correct = False else: msg = ( f"User {user} is in state {aae.key} which " f"cannot be handled by the state" ) log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not password_correct: log.debug("Password is not correct, setting") log.debug("Setting initial password") if not __opts__["test"]: function_modules = { "BAPI_USER_CHANGE": { "USERNAME": user, "PASSWORD": {"BAPIPWD": user_password}, "PASSWORDX": {"BAPIPWD": "X"}, }, "BAPI_TRANSACTION_COMMIT": {"WAIT": "X"}, } success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not set initial password for {user}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_USER_CHANGE"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages( result["BAPI_USER_CHANGE"]["RETURN"] ) ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_TRANSACTION_COMMIT"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages( result["BAPI_TRANSACTION_COMMIT"]["RETURN"] ) ret["result"] = False return ret ret["changes"]["new"]["PASSWORD"] = "XXX-REDACTED-XXX" ret["changes"]["old"]["PASSWORD"] = "XXX-REDACTED-XXX" else: log.debug("User does not exist, creating") if not user_password: msg = f"User password is required to create {user}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __opts__["test"]: function_modules = { "BAPI_USER_CREATE1": { "USERNAME": user, "PASSWORD": {"BAPIPWD": user_password}, }, "BAPI_TRANSACTION_COMMIT": {"WAIT": "X"}, } function_modules = salt.utils.dictupdate.merge( function_modules, {"BAPI_USER_CREATE1": attributes}, merge_lists=True, strategy="smart", ) success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not create user {user}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_USER_CREATE1"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages(result["BAPI_USER_CREATE1"]["RETURN"]) ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_TRANSACTION_COMMIT"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages( result["BAPI_TRANSACTION_COMMIT"]["RETURN"] ) ret["result"] = False return ret ret["changes"]["new"] = {"USERNAME": user, **attributes} profiles_in_sys = [] roles_in_sys = [] log.debug("Checking for role changes") # converting roles to correct format for comparison for i in range(0, len(roles)): # pylint: disable=consider-using-enumerate role_from_dat = _convert_date(roles[i].get("valid_from", date.today())) role_to_dat = _convert_date(roles[i].get("valid_to", date(9999, 12, 31))) roles[i] = { "AGR_NAME": roles[i]["name"], "FROM_DAT": role_from_dat, "TO_DAT": role_to_dat, } log.debug(f"New roles: {roles}") # remove keys to NOT compare for i in range(0, len(roles_in_sys)): # pylint: disable=consider-using-enumerate keys = list( roles_in_sys[i].keys() ) # otherwise the dictionary size would change in the next loop for k in keys: if k not in ["AGR_NAME", "FROM_DAT", "TO_DAT"]: del roles_in_sys[i][k] elif k in ["FROM_DAT", "TO_DAT"]: # convert dates for comparison roles_in_sys[i][k] = _convert_date(roles_in_sys[i][k]) log.debug(f"Exising roles: {roles_in_sys}") if not _compare_role_lists(roles_in_sys, roles): log.debug("Roles need to be updated") if not __opts__["test"]: # this FM will remove all roles that are not listed here function_modules = { "BAPI_USER_ACTGROUPS_ASSIGN": {"USERNAME": user, "ACTIVITYGROUPS": roles} } success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not update roles for {user}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_USER_ACTGROUPS_ASSIGN"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages( result["BAPI_USER_ACTGROUPS_ASSIGN"]["RETURN"] ) ret["result"] = False return ret ret["changes"]["new"]["ROLES"] = roles ret["changes"]["old"]["ROLES"] = roles_in_sys log.debug("Checking for profile changes") # remove duplicates and sort lists for comparison profiles = sorted(list(set(profiles))) log.debug(f"New profiles: {profiles}") # remove duplicates and sort lists for comparison profiles_in_sys = sorted(list(set(profiles_in_sys))) # remove profiles that come from roles for role in roles: function_modules = {"PRGN_GET_PROFILES_OF_ROLE_RFC": {"AGR_NAME": role["AGR_NAME"]}} success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not retrieve profile of role {role['AGR_NAME']}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret log.debug(f"Role profiles in the system: {result['PRGN_GET_PROFILES_OF_ROLE_RFC']}") if result["PRGN_GET_PROFILES_OF_ROLE_RFC"]["PROFILE"]: role_profile = result["PRGN_GET_PROFILES_OF_ROLE_RFC"]["PROFILE"][0]["PROFILE"] if role_profile in profiles_in_sys: profiles_in_sys.remove(role_profile) log.debug(f"Exising profiles: {profiles_in_sys}") if profiles != profiles_in_sys: log.debug("Profiles need to be updated") # this FM will remove all profiles that are not listed here if not __opts__["test"]: function_modules = { "BAPI_USER_PROFILES_ASSIGN": {"USERNAME": user, "PROFILES": profiles} } success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not update roles for {user}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_USER_PROFILES_ASSIGN"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages( result["BAPI_USER_PROFILES_ASSIGN"]["RETURN"] ) ret["result"] = False return ret ret["changes"]["new"]["PROFILES"] = profiles ret["changes"]["old"]["PROFILES"] = profiles_in_sys if not ret["changes"].get("new", None) and not ret["changes"].get("old", None): ret["changes"] = {} ret["comment"] = "No changes required" elif __opts__["test"]: ret["comment"] = f"Would have maintained user {user}" else: ret["comment"] = f"Maintained user {user}" ret["result"] = True if (not __opts__["test"] or not ret["changes"]) else None return ret
# pylint: disable=unused-argument
[docs]def user_absent( name, sid, client, message_server_host, message_server_port, logon_group, username, password, **kwargs, ): """ Ensure that a user is absent in the system. name Username. sid SID of the SAP system. message_server_host Host of the message server. message_server_port Port of the message server. client Client to connect to. logon_group Logon group to use. username Username to use for the connection. password Password to use for the connection. Example: .. code-block:: jinja Technical user SALT for SAP system S4H / client 000 is absent: sap_nwabap.user_absent: - name: SALT - sid: S4H - client: "000" - message_server_host: s4h - message_server_port: 3600 - logon_group: SPACE - username: DDIC - password: __slot__:salt:vault.read_secret(path="nwabap/S4H/000", key="DDIC") """ log.debug("Running function") ret = {"name": name, "changes": {}, "comment": "", "result": False} user = name.upper() log.debug("Creating one connection for the lifetime of this state") if isinstance(client, int): client = f"{client:03d}" abap_connection = { "mshost": message_server_host, "msserv": str(message_server_port), "sysid": sid, "group": logon_group, "client": client, "user": username, "passwd": password, "lang": "EN", } with Connection(**abap_connection) as conn: log.debug(f"Retrieving information about {user} from SAP system {sid}") function_modules = {"BAPI_USER_GET_DETAIL": {"USERNAME": user}} success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not retrieve information about {user} from SAP system {sid}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret log.trace(f"Got user:\n{result}") user_exists = __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_USER_GET_DETAIL"]["RETURN"] ) if user_exists: log.debug("User exists, deleting") if not __opts__["test"]: function_modules = { "BAPI_USER_DELETE": {"USERNAME": user}, "BAPI_TRANSACTION_COMMIT": {"WAIT": "X"}, } success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not execute BAPI_USER_DELETE for {sid}, please check logs" log.error(msg) ret["comment"] = msg ret["result"] = False return ret log.trace(f"Got result:\n{result}") if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_USER_DELETE"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages(result["BAPI_USER_DELETE"]["RETURN"]) ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_TRANSACTION_COMMIT"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages( result["BAPI_TRANSACTION_COMMIT"]["RETURN"] ) ret["result"] = False return ret ret["comment"] = f"User {user} deleted" ret["changes"] = {"old": f"User {user} exists", "new": None} else: ret["comment"] = f"User {user} would be deleted" ret["changes"] = {"old": f"User {user} exists", "new": None} else: ret["comment"] = "No changes required" ret["changes"] = {} ret["result"] = True if (not __opts__["test"] or not ret["changes"]) else None return ret
# pylint: disable=unused-argument
[docs]def user_password_productive( name, sid, client, message_server_host, message_server_port, logon_group, username, password, user_password, **kwargs, ): """ Ensure that the given password is set as productive for the user name Username. sid SID of the SAP system. message_server_host Host of the message server. message_server_port Port of the message server. client Client to connect to. logon_group Logon group to use. username Username to use for the connection. password Password to use for the connection. user_password Password for the user. .. note: If the user is a DIALOG user, the state will read the password history size and set a sufficient number of random passwords to reset the password history. Last, the given password will be set as productive password. Example: .. code-block:: jinja Password for user MMUSTERMANN is productive: sap_nwabap.user_absent: - name: MMUSTERMANN - sid: S4H - client: "000" - message_server_host: s4h - message_server_port: 3600 - logon_group: SPACE - username: SALT - password: __slot__:salt:vault.read_secret(path="nwabap/S4H/000", key="SALT") - user_password: Abcd1234! """ log.debug("Running function") ret = {"name": name, "changes": {}, "comment": "", "result": False} user = name.upper() if isinstance(client, int): client = f"{client:03d}" abap_connection = { "mshost": message_server_host, "msserv": str(message_server_port), "sysid": sid, "group": logon_group, "client": client, "user": username, "passwd": password, "lang": "EN", } with Connection(**abap_connection) as conn: log.debug("Checking if logon is possible") function_modules = {"SUSR_LOGIN_CHECK_RFC": {"BNAME": user, "PASSWORD": user_password}} password_correct = True try: success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn, raise_on_error=True ) except ABAPApplicationError as aae: # unsuccessful logons are output as exceptions """ EXCEPTIONS WAIT = 1 " USER_LOCKED = 2 " User is locked USER_NOT_ACTIVE = 3 " PASSWORD_EXPIRED = 4 " WRONG_PASSWORD = 5 " NO_CHECK_FOR_THIS_USER = 6 " PASSWORD_ATTEMPTS_LIMITED = 7 " """ # pylint: disable=pointless-string-statement if aae.key in ["WRONG_PASSWORD", "PASSWORD_EXPIRED"]: password_correct = False else: msg = f"User {user} is in state {aae.key} which cannot be handled by the state" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if password_correct: ret["changes"] = {} ret["comment"] = "No changes required" ret["result"] = True else: log.debug(f"Password needs to be updated for {user}") log.debug(f"Retrieving user type for {user}") function_modules = {"BAPI_USER_GET_DETAIL": {"USERNAME": user}} success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not retrieve user type of {user} from SAP system {sid}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if result["LOGONDATA"]["USTYP"] == "A": log.debug( f"User {user} is a dialog user, " "setting multiple initial passwords to clear password history" ) log.debug("Retrieving password history size") function_modules = { "SBUF_PARAMETER_GET": {"PARAMETER_NAME": "login/password_history_size"} } success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = "Could not retrieve profile parameter login/password_history_size" log.error(msg) ret["comment"] = msg ret["result"] = False return ret history_size = int(result["SBUF_PARAMETER_GET"]["PARAMETER_VALUE"]) log.debug( f"Setting {history_size} temporary generated passwords to reset password history size" ) initial_passwd = None for i in range(0, history_size): log.debug(f"Generating initial password #{i}") function_modules = {"SUSR_GENERATE_PASSWORD": {}} success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not generate a random password #{i}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if "PASSWORD" not in result["SUSR_GENERATE_PASSWORD"]: ret["comment"] = "Execution of SUSR_GENERATE_PASSWORD failed" ret["result"] = False return ret initial_passwd = result["SUSR_GENERATE_PASSWORD"]["PASSWORD"] log.debug(f"Setting initial password #{i}") if not __opts__["test"]: function_modules = { "BAPI_USER_CHANGE": { "USERNAME": user, "PASSWORD": {"BAPIPWD": initial_passwd}, "PASSWORDX": {"BAPIPWD": "X"}, }, "BAPI_TRANSACTION_COMMIT": {"WAIT": "X"}, } success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not set initial password #{i} for {user}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_USER_CHANGE"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages( result["BAPI_USER_CHANGE"]["RETURN"] ) ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_TRANSACTION_COMMIT"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages( result["BAPI_TRANSACTION_COMMIT"]["RETURN"] ) ret["result"] = False return ret log.debug(f"Setting productive password for {user}") if not __opts__["test"]: function_modules = { "SUSR_USER_CHANGE_PASSWORD_RFC": { "BNAME": user, "PASSWORD": initial_passwd, "NEW_PASSWORD": user_password, } } success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could set productive password for {user}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["SUSR_USER_CHANGE_PASSWORD_RFC"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages( __salt__["sap_nwabap.process_bapiret2"]( result["SUSR_USER_CHANGE_PASSWORD_RFC"]["RETURN"] ) ) ret["result"] = False return ret ret["changes"] = {"new": {"PASSWORD": "XXX-REDACTED-XXX"}} if __opts__["test"]: ret["comment"] = f"Would have set productive password set for {user}" else: ret["comment"] = f"Productive password set for {user}" ret["result"] = True if (not __opts__["test"] or not ret["changes"]) else None return ret
# pylint: disable=unused-argument
[docs]def pse_uploaded( name, sid, client, message_server_host, message_server_port, logon_group, username, password, pse_owner, pin=None, pse_type=None, context=None, applic=None, pse_name=None, **kwargs, ): """ Ensures that a PSE is uploaded to the SAP system. Before the upload takes place, the PSE on the filesystem and the PSE in STRUST will be compared. name Filepath of the PSE Filepath pin PIN of the PSE file, default is ``None``. pse_owner Owner of the PSE file. pse_type PSE type, either ``SSLS``, ``SSLC``, ``SSLA`` or ``None``. If ``None`` (default), the arguments ``context`` and ``applic`` must be set. context See function module ``SSFPSE_FILENAME`` for possible values. applic See function module ``SSFPSE_FILENAME`` for possible values. pse_name Name for the PSE resulting from ``context`` and ``applic``. sid SID of the SAP system. message_server_host Host of the message server. message_server_port Port of the message server. client Client to connect to. logon_group Logon group to use. username Username to use for the connection. password Password to use for the connection. .. warning:: This function module does not correctly set the PIN for the ASCS instance PSE after upload, leaving the system in an inconsistent state. SAP will not fix this issue (function module is not released to customer), so DO NOT use this function module if you have PIN-protected PSE files. Note that there are currently no remote-enabled function modules to set PSE PINs in STRUST. Example: .. code-block:: jinja SAP NetWeaver AS ABAP S4H SSLS PSE is uploaded: sap_nwabap.pse_uploaded: - name: /usr/sap/S4H/SYS/sec/SAPSSLS.pse - pse_owner: s4hadm - pse_type: SSLS - sid: S4H - client: "000" - message_server_host: s4h - message_server_port: 3600 - logon_group: SPACE - username: SALT - password: __slot__:salt:vault.read_secret(path="nwabap/S4H/000", key="SALT") - user_password: Abcd1234! """ log.debug("Running function") ret = { "name": name, "changes": {"new": [], "old": []}, "result": False, "comment": "", } if not pse_type: if not context and not applic and not pse_name: msg = "Either pse_type or context and applic and pse_name must be specified" log.error(f"{msg}") ret["comment"] = msg ret["result"] = False return ret elif pse_type == "SSLS": context = "SSLS" applic = "DFAULT" pse_name = "SAPSSLS.pse" elif pse_type == "SSLC": context = "SSLC" applic = "DFAULT" pse_name = "SAPSSLC.pse" elif pse_type == "SSLA": context = "SSLC" applic = "ANONYM" pse_name = "SAPSSLA.pse" else: msg = f"Unknown PSE type '{pse_type}'" log.error(f"{msg}") ret["comment"] = msg ret["result"] = False return ret if isinstance(client, int): client = f"{client:03d}" abap_connection = { "mshost": message_server_host, "msserv": str(message_server_port), "sysid": sid, "group": logon_group, "client": client, "user": username, "passwd": password, "lang": "EN", } with Connection(**abap_connection) as conn: log.debug(f"Checking validity of PSE {pse_name} on the file") function_modules = {"SSFPSE_CHECK": {"PSENAME": pse_name}} success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not check PSE {name}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if result["SSFPSE_CHECK"]["CRC"] != 0: log.debug( f"PSE {pse_name} on the system is either invalid or does not exist and must be replaced" ) certs_are_equal = False else: log.debug(f"Retrieving information about {name} in SAP system {sid}") function_modules = {"SSFP_GET_PSEINFO": {"CONTEXT": context, "APPLIC": applic}} success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) cert_data = result.get("SSFP_GET_PSEINFO", {}).get("CERTIFICATE", None) if not success or not cert_data: msg = f"Could not retrieve information about {name} in SAP system {sid}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret log.debug(f"Parsing certificate of {name} in SAP system {sid}") function_modules = {"SSFP_PARSECERTIFICATE": {"CERTXSTRING": cert_data}} success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not parse certificate of {name} in SAP system {sid}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret validity = result["SSFP_PARSECERTIFICATE"]["CERTATTRIBUTES"]["VALIDITY"] pse_sap = { "alg": result["SSFP_PARSECERTIFICATE"]["ALGID"], "subject": result["SSFP_PARSECERTIFICATE"]["CERTATTRIBUTES"]["SUBJECT"], "issuer": result["SSFP_PARSECERTIFICATE"]["CERTATTRIBUTES"]["ISSUER"], "serial": result["SSFP_PARSECERTIFICATE"]["CERTATTRIBUTES"]["SNUMBER"], "valid_from": "".join(validity.split(" ")[:2]), "valid_to": "".join(validity.split(" ")[2:]), "fingerprint": result["SSFP_PARSECERTIFICATE"]["CERTATTRIBUTES"]["FINGERPR"], } log.debug("Reading PSE information from file") result = __salt__["sap_pse.get_my_name"]( pse_file=name, pse_pwd=pin, runas=pse_owner, ) if not isinstance(result, dict): msg = f"Cannot read PSE file {name}" log.error(f"{msg}") ret["result"] = False ret["comment"] = msg return ret pse_file = result["MY Certificate"] log.debug("Converting datetime strings for comparison") not_after_sap = datetime.strptime(pse_sap["valid_to"], "%Y%m%d%H%M%S") pse_not_after = pse_file["Validity not after"].split("(", 1)[0].strip() not_after_file = datetime.strptime(pse_not_after, "%a %b %d %H:%M:%S %Y") not_before_sap = datetime.strptime(pse_sap["valid_from"], "%Y%m%d%H%M%S") pse_not_before = pse_file["Validity not before"].split("(", 1)[0].strip() not_before_pse = datetime.strptime(pse_not_before, "%a %b %d %H:%M:%S %Y") log.debug("Comparing certificate attributes") certs_are_equal = True if pse_sap["serial"] != pse_file["Serial Number"].replace(":", ""): msg = ( f"Serial numbers of File PSE ({pse_file['Serial Number'].replace(':', '')}) " f"and SAP PSE ({pse_sap['serial']}) do not match" ) log.debug(msg) certs_are_equal = False elif pse_sap["fingerprint"] != pse_file["Certificate fingerprint (MD5)"]: msg = ( f"MD5 finger prints of File PSE ({pse_file['Certificate fingerprint (MD5)']}) " f"and SAP PSE ({pse_sap['fingerprint']}) do not match" ) log.debug(msg) certs_are_equal = False elif abs((not_after_sap - not_after_file).total_seconds()) > 0: msg = ( f"Not after datetimes of File PSE ({not_after_file}) " f"and SAP PSE ({not_after_sap}) do not match" ) log.debug(msg) msg = ( "Difference between File PSE <> SAP PSE: " f"{abs((not_after_sap - not_after_file).total_seconds())} seconds" ) log.debug(msg) certs_are_equal = False elif abs((not_before_sap - not_before_pse).total_seconds()) > 0: msg = ( f"Not before datetimes of File PSE ({not_before_pse}) and " f"SAP PSE ({not_before_sap}) do not match" ) log.debug(msg) msg = ( f"Difference between File PSE <> SAP PSE: " f"{abs((not_before_sap - not_before_pse).total_seconds())} seconds" ) log.debug(msg) certs_are_equal = False if certs_are_equal: log.debug("No changes required") ret["result"] = True ret["changes"] = {} ret["comment"] = "No changes required" return ret log.debug("Certificates do not match") log.debug(f"Uploading PSE {name} for {context} / {applic}") if not __opts__["test"]: function_modules = { "SSFR_PSE_UPLOAD": { "IS_STRUST_IDENTITY": {"PSE_CONTEXT": context, "PSE_APPLIC": applic}, "IV_FILENAME": name, "IV_REPLACE_EXISTING_PSE": "X", } } if pin is not None: function_modules["SSFR_PSE_UPLOAD"]["IV_PSEPIN"] = pin success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not upload PSE {name} for {context} / {applic}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["SSFR_PSE_UPLOAD"]["ET_BAPIRET2"] ): ret["comment"] = _get_bapiret2_messages(result["SSFR_PSE_UPLOAD"]["ET_BAPIRET2"]) ret["result"] = False return ret log.debug(f"Uploaded PSE {name} for {context} / {applic} to {sid}") ret["changes"]["new"].append(msg) if pse_name: log.debug(f"Checking validity of PSE {pse_name} on the file") if not __opts__["test"]: function_modules = {"SSFPSE_CHECK": {"PSENAME": pse_name}} success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not check PSE {name}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if result["SSFPSE_CHECK"]["CRC"] != 0: msg = f"Check for PSE {name} failed, please check the system" log.error(msg) ret["comment"] = msg ret["result"] = False return ret log.debug(f"Checked PSE {pse_name} on {sid} successfully") if not ret["changes"].get("new", None): ret["changes"] = {} ret["comment"] = "No changes required" elif __opts__["test"]: ret["comment"] = f"Would have maintained PSE for {context} / {applic}" else: ret["comment"] = f"Maintained PSE for {context} / {applic}" ret["result"] = True if (not __opts__["test"] or not ret["changes"]) else None return ret
[docs]def rfc_dest_present( name, sid, client, message_server_host, message_server_port, logon_group, username, password, dest_type=None, dest_password=None, keep_password=True, keep_proxy_password=True, **kwargs, ): """ Ensures that an RFC destination is present in the SAP system. name Name of the RFC destination. sid SID of the SAP system. message_server_host Host of the message server. message_server_port Port of the message server. client Client to connect to. logon_group Logon group to use. username Username to use for the connection. password Password to use for the connection. dest_type Type of the destination, required for creation. Can be one of: ``H``, ``G``, ``L``, ``3``, ``T`` dest_password Password to set for the connection. keep_password ``<True|False>`` if the password should be kept on update (if no explicit password is given). keep_proxy_password ``<True|False>`` if the proxy password should be kept on update (if no explicit password is given). Next to these arguments, additional kwargs can be used to set attributes in the RFC destination. The following kwargs are recognized and can be used in upper- and lowercase, depending on the RFC destination type: .. code-block:: yaml accept_cookie: <value> arfc_active: <value> arfc_cycle: <value> arfc_method: <value> assertion_ticket: <value> assertion_ticket_client: <value> assertion_ticket_sysid: <value> authorization_parameter: <value> basxml_active: <value> callback_whitelist: <value> callback_whitelist_active: <value> category: <value> client_codepage_active: <value> compress_reply: <value> conversion_bytes: <value> conversion_mode: <value> cpic_timeout: <value> description: <value> enable_trace: <value> explicit_codepage: <value> explicit_codepage_active: <value> export_trace: <value> gateway_host: <value> gateway_service: <value> group_name: <value> http_compress: <value> http_timeout: <value> http_version: <value> keep_password: <value> keep_proxy_password: <value> keepalive_timeout: <value> language_codepage_active: <value> load_balancing: <value> logon_client: <value> logon_language: <value> logon_method: <value> logon_user: <value> logon_user_254: logon_user_254, mdmp_list: <value> mdmp_settings_active: <value> method: <value> name: <value> path_prefix: <value> program: <value> proxy_server: <value> proxy_service_number: <value> proxy_user: <value> qrfc_version: <value> reference: <value> rfc_bitmap: <value> rfc_wan: <value> rfclogon_gui: <value> same_user: <value> save_as_hostname: <value> server_name: <value> service_number: <value> snc_active: <value> snc_parameter: <value> ssl_active: <value> ssl_application: <value> sso_ticket: <value> start_type: <value> system_identifier: <value> system_number: <value> trace_settings: <value> trfc_bg_delay: <value> trfc_bg_repetitions: <value> trfc_bg_supress: <value> trusted_system: <value> ui_lock: <value> unicode_bytes: <value> update_all: <value> update_fields: <value> Example: .. code-block:: jinja SM_SOLCLNT100_BACK is adapted for SAP NetWeaver AS ABAP system S4H: sap_nwabap.rfc_dest_present: - name: SM_SOLCLNT100_BACK - sid: S4H - client: "000" - message_server_host: s4h - message_server_port: 3600 - logon_group: SPACE - username: SALT - password: __slot__:salt:vault.read_secret(path="nwabap/S4H/000", key="SALT") - dest_type: 3 - server_name: /H/saprouter.my.domain.de/S/3299/H/sol """ log.debug("Running function") name = name.upper() ret = {"name": name, "changes": {"old": {}, "new": {}}, "comment": "", "result": False} dest_type = str(dest_type) # if type = 3 -> salt states like to interpret numbers as int log.debug("Parsing attributes and replacing human-readable ") attributes = _replace_human_readable(kwargs, mapping=RFC_MAPPING, remove_unknown=True) # some values need to be strings for key in ["SERVICE_NUMBER", "EXPORT_TRACE", "UNICODE_BYTES", "RFC_BITMAP", "BASXML_ACTIVE"]: if key in attributes: attributes[key] = str(attributes[key]) log.debug("Creating one connection for the lifetime of this state") if isinstance(client, int): client = f"{client:03d}" abap_connection = { "mshost": message_server_host, "msserv": str(message_server_port), "sysid": sid, "group": logon_group, "client": client, "user": username, "passwd": password, "lang": "EN", } with Connection(**abap_connection) as conn: log.debug(f"Checking if RFC destination {name} exists") function_modules = {"DEST_EXISTS": {"NAME": name}} success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success or "EXISTS" not in result["DEST_EXISTS"]: msg = f"Could not check existance of {name}" log.error(msg) ret["comment"] = msg ret["changes"] = {} ret["result"] = False return ret update_password = False if result["DEST_EXISTS"]["EXISTS"] == "X": log.debug(f"RFC destination {name} already exists") log.debug(f"Determining type for {name}") function_modules = {"DEST_GET_TYPE": {"NAME": name}} success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success or "DEST_TYPE" not in result["DEST_GET_TYPE"]: msg = f"Could not determine type of {name}" log.error(msg) ret["comment"] = msg ret["changes"] = {} ret["result"] = False return ret dest_type_existing = result["DEST_GET_TYPE"]["DEST_TYPE"]["RFCTYPE"] if not dest_type: dest_type = dest_type_existing log.debug(f"RFC destination {name} is of type {dest_type}") elif dest_type != dest_type_existing: msg = ( f"RFC destination {name} already exists as type '{dest_type_existing}' with " f"type '{dest_type}' being requested. Switching types is not supported" ) log.error(msg) ret["comment"] = msg ret["changes"] = {} ret["result"] = False return ret log.debug(f"Reading existing data for destination {name}") if dest_type == "H": read_fm = "DEST_HTTP_ABAP_READ" elif dest_type == "G": read_fm = "DEST_HTTP_EXT_READ" elif dest_type == "L": read_fm = "DEST_LOGICAL_READ" elif dest_type == "3": read_fm = "DEST_RFC_ABAP_READ" elif dest_type == "T": read_fm = "DEST_RFC_TCPIP_READ" else: msg = f"RFC destination {name} is of the unsupported type {dest_type}" log.error(msg) ret["changes"] = {} ret["comment"] = msg ret["result"] = False return ret success, result = __salt__["sap_nwabap.call_fms"]( function_modules={read_fm: {"NAME": name}}, conn=conn ) if not success: msg = f"Could not read {name}" log.error(msg) ret["changes"] = {} ret["comment"] = msg ret["result"] = False return ret data = result[read_fm] log.debug(f"Calculating diffs for update of RFC destination {name}") diff = salt.utils.dictdiffer.deep_diff(data, attributes) log.debug("Removing empty dictionaries") diff = _clear_empty_dict( diff, remove_none=False ) # this will also remove empty "new" and "old" dicts new = diff.get("new", {}) # because we're only interested in the changed fields, we remove unchanged stuff from "old" old = _extract_changed_from_dict(new, diff.get("old", {})) if new: log.debug(f"Data for {name} has changed, we need to update. Changes:\n{new}") log.debug("Generating list of fields to update") update_data = new update_data["NAME"] = name update_data["UPDATE_ALL"] = " " # required since default value is "X" update_data["UPDATE_FIELDS"] = {} for field in new: if field not in ["NAME", "UPDATE_FIELDS", "UPDATE_ALL"]: update_data["UPDATE_FIELDS"][field] = "X" if "LOGON_USER" not in new and keep_password: update_data["KEEP_PASSWORD"] = "X" else: update_password = True if "PROXY_USER" not in new and dest_type in ["H", "G"] and keep_proxy_password: # only supported for HTTP connections update_data["KEEP_PROXY_PASSWORD"] = "X" if dest_type == "H": update_fm = "DEST_HTTP_ABAP_UPDATE" elif dest_type == "G": update_fm = "DEST_HTTP_EXT_UPDATE" elif dest_type == "L": update_fm = "DEST_LOGICAL_UPDATE" elif dest_type == "3": update_fm = "DEST_RFC_ABAP_UPDATE" elif dest_type == "T": update_fm = "DEST_RFC_TCPIP_UPDATE" else: msg = f"RFC destination {name} is of the unsupported type {dest_type}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __opts__["test"]: success, result = __salt__["sap_nwabap.call_fms"]( function_modules={update_fm: update_data}, conn=conn ) if not success: msg = f"Could not update {name}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret ret["changes"] = {"new": new, "old": old} else: update_password = True if not dest_type: msg = "RFC destination type 'dest_type' was not provided as argument but is required for creation" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if dest_type == "H": create_fm = "DEST_HTTP_ABAP_CREATE" elif dest_type == "G": create_fm = "DEST_HTTP_EXT_CREATE" elif dest_type == "L": create_fm = "DEST_LOGICAL_CREATE" elif dest_type == "3": create_fm = "DEST_RFC_ABAP_CREATE" elif dest_type == "T": create_fm = "DEST_RFC_TCPIP_CREATE" else: msg = f"RFC destination {name} is of the unsupported type {dest_type}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret attributes["NAME"] = name if not __opts__["test"]: success, result = __salt__["sap_nwabap.call_fms"]( function_modules={create_fm: attributes}, conn=conn ) if not success: msg = f"Could not create RFC destination {name}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret ret["changes"]["new"] = {**attributes} if dest_password and update_password: log.debug(f"Setting password for RFC destination {name}") if not __opts__["test"]: function_modules = {"DEST_SET_PASSWORD": {"NAME": name, "PASSWORD": dest_password}} success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not set password for RFC destination {name}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret ret["changes"]["new"]["PASSWORD"] = "XXX-REDACTED-XXX" if not ret["changes"].get("new", None) and not ret["changes"].get("old", None): ret["changes"] = {} ret["comment"] = "No changes required" elif __opts__["test"]: ret["comment"] = f"Would have maintained RFC destination {name}" else: ret["comment"] = f"Maintained RFC destination {name}" ret["result"] = True if (not __opts__["test"] or not ret["changes"]) else None return ret
# pylint: disable=unused-argument
[docs]def rfc_dest_absent( name, sid, client, message_server_host, message_server_port, logon_group, username, password, **kwargs, ): """ Ensures that an RFC destination is absent in the SAP system. name Name of the RFC destination. sid SID of the SAP system. message_server_host Host of the message server. message_server_port Port of the message server. client Client to connect to. logon_group Logon group to use. username Username to use for the connection. password Password to use for the connection. Example: .. code-block:: jinja SM_SOLCLNT100_BACK is absent from SAP NetWeaver AS ABAP system S4H: sap_nwabap.rfc_dest_absent: - name: SM_SOLCLNT100_BACK - sid: S4H - client: "000" - message_server_host: s4h - message_server_port: 3600 - logon_group: SPACE - username: SALT - password: __slot__:salt:vault.read_secret(path="nwabap/S4H/000", key="SALT") """ log.debug("Running function") name = name.upper() ret = {"name": name, "changes": {"old": {}, "new": {}}, "comment": "", "result": False} log.debug("Creating one connection for the lifetime of this state") if isinstance(client, int): client = f"{client:03d}" abap_connection = { "mshost": message_server_host, "msserv": str(message_server_port), "sysid": sid, "group": logon_group, "client": client, "user": username, "passwd": password, "lang": "EN", } with Connection(**abap_connection) as conn: log.debug(f"Checking if RFC destination {name} exists") function_modules = {"DEST_EXISTS": {"NAME": name}} success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success or "EXISTS" not in result["DEST_EXISTS"]: msg = f"Could not check existance of {name}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if result["DEST_EXISTS"]["EXISTS"] == "X": log.debug(f"RFC destination {name} exists, removing") if not __opts__["test"]: function_modules = {"DEST_DELETE": {"NAME": name}} success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not delete {name}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret ret["changes"] = {"old": name, "new": None} if not ret["changes"].get("new", None) and not ret["changes"].get("old", None): ret["changes"] = {} ret["comment"] = "No changes required" elif __opts__["test"]: ret["comment"] = f"Would have removed RFC destination {name}" else: ret["comment"] = f"Removed RFC destination {name}" ret["result"] = True if (not __opts__["test"] or not ret["changes"]) else None return ret
# pylint: disable=unused-argument
[docs]def sld_config_present( name, sid, client, message_server_host, message_server_port, logon_group, username, password, **kwargs, ): """ Ensure that an SLD configuration is present in the system. name Name of the SLD RFC destination. sid SID of the SAP system. message_server_host Host of the message server. message_server_port Port of the message server. client Client to connect to. logon_group Logon group to use. username Username to use for the connection. password Password to use for the connection. Example: .. code-block:: jinja SLD config is present on S4H: sap_nwabap.sld_config_present: - name: SLD_DS_TARGET - sid: S4H - client: "000" - message_server_host: s4h - message_server_port: 3600 - logon_group: SPACE - username: SALT - password: __slot__:salt:vault.read_secret(path="nwabap/S4H/000", key="SALT") """ log.debug("Running function") name = name.upper() ret = {"name": name, "changes": {"old": {}, "new": {}}, "comment": "", "result": False} log.debug("Creating one connection for the lifetime of this state") if isinstance(client, int): client = f"{client:03d}" abap_connection = { "mshost": message_server_host, "msserv": str(message_server_port), "sysid": sid, "group": logon_group, "client": client, "user": username, "passwd": password, "lang": "EN", } with Connection(**abap_connection) as conn: log.debug("Retrieving current configuration") current_config = __salt__["sap_nwabap.read_table"]( table_name="SLDAGADM", fields=["PROGNAME", "ACTIVE", "SEQNR", "RFCDEST", "DORFC", "DOBTC", "BTCMIN"], conn=conn, ) update_http_dest = True update_bg_job = True for line in current_config: if line["PROGNAME"] == "HTTP_SLD_DS_TARGET": if line["ACTIVE"] == "X" and line["SEQNR"] == "0000" and line["RFCDEST"] == name: update_http_dest = False else: ret["changes"]["old"]["HTTP_SLD_DS_TARGET"] = { "ACTIVE": line["ACTIVE"], "SEQNR": line["SEQNR"], "RFCDEST": line["RFCDEST"], } elif line["PROGNAME"] == "RSLDAGDS": if ( line["ACTIVE"] == "" and line["SEQNR"] == "0000" and line["DORFC"] == "X" and line["DOBTC"] == "X" and line["BTCMIN"] == 720 ): update_bg_job = False else: ret["changes"]["old"]["RSLDAGDS"] = { "ACTIVE": line["ACTIVE"], "SEQNR": line["SEQNR"], "DORFC": line["DORFC"], "DOBTC": line["DOBTC"], "BTCMIN": line["BTCMIN"], } payload = [] if update_http_dest: log.debug("Setting HTTP_SLD_DS_TARGET") payload.append( {"PROGNAME": "HTTP_SLD_DS_TARGET", "ACTIVE": "X", "SEQNR": "0000", "RFCDEST": name} ) ret["changes"]["new"]["HTTP_SLD_DS_TARGET"] = { "ACTIVE": "X", "SEQNR": "0000", "RFCDEST": name, } if update_bg_job: log.debug("Setting RSLDAGDS") payload.append( { "PROGNAME": "RSLDAGDS", "ACTIVE": "", "SEQNR": "0000", "DORFC": "X", "DOBTC": "X", "BTCMIN": 720, } ) ret["changes"]["new"]["RSLDAGDS"] = { "ACTIVE": "", "SEQNR": "0000", "DORFC": "X", "DOBTC": "X", "BTCMIN": 720, } if payload: log.debug("Updating SLD configuration") if not __opts__["test"]: function_modules = {"SLDAG_SET_CONFIG": {"SLDCFG": payload}} success, _ = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = "Could not update SLD configuration" log.error(msg) ret["changes"]["new"] = {} ret["comment"] = msg ret["result"] = False return ret if not ret["changes"].get("new", None) and not ret["changes"].get("old", None): ret["changes"] = {} ret["comment"] = "No changes required" elif __opts__["test"]: ret["comment"] = "Would have updated SLD registration" else: ret["comment"] = "Updated SLD registration" ret["result"] = True if (not __opts__["test"] or not ret["changes"]) else None return ret
# pylint: disable=unused-argument
[docs]def sld_data_transfered( name, sid, client, message_server_host, message_server_port, logon_group, username, password, **kwargs, ): """ Runs the report RSLDAGDS that triggers the SLD data transfer. name Name of the SLD RFC destination. sid SID of the SAP system. message_server_host Host of the message server. message_server_port Port of the message server. client Client to connect to. logon_group Logon group to use. username Username to use for the connection. password Password to use for the connection. .. note:: This state will **always** produce changes. Example: .. code-block:: jinja SLD data is transfered for S4H: sap_nwabap.sld_data_transfered: - name: SLD_DS_TARGET - sid: S4H - client: "000" - message_server_host: s4h - message_server_port: 3600 - logon_group: SPACE - username: SALT - password: __slot__:salt:vault.read_secret(path="nwabap/S4H/000", key="SALT") """ log.debug("Running function") name = name.upper() ret = {"name": name, "changes": {"old": {}, "new": {}}, "comment": "", "result": False} log.debug("Creating one connection for the lifetime of this state") if isinstance(client, int): client = f"{client:03d}" abap_connection = { "mshost": message_server_host, "msserv": str(message_server_port), "sysid": sid, "group": logon_group, "client": client, "user": username, "passwd": password, "lang": "EN", } with Connection(**abap_connection) as conn: log.debug("Executing INST_EXECUTE_REPORT") if not __opts__["test"]: success, result = __salt__["sap_nwabap.call_fms"]( function_modules={"INST_EXECUTE_REPORT": {"PROGRAM": "RSLDAGDS"}}, conn=conn ) if not success: msg = "Could not execute INST_EXECUTE_REPORT with program RSLDAGDS" log.error(msg) ret["comment"] = msg ret["result"] = False return ret success_messages = 0 for line in result["INST_EXECUTE_REPORT"]["OUTPUT_TAB"]: # Note: INST_EXECUTE_REPORT only outputs a maximum of 85 chars per line, # therefore we need to check for the cut off content if "Used HTTP destination: SLD_DS_TAR" in line["ZEILE"]: success_messages += 1 elif "Data sent with destination SLD_DS" in line["ZEILE"]: success_messages += 1 if success_messages < 2: msg = ( "Error during execution of RSLDAGDS: " f"{result['INST_EXECUTE_REPORT']['OUTPUT_TAB']}" ) log.error(msg) ret["comment"] = "SLD data was not sent successfully" ret["result"] = False else: ret["comment"] = "SLD data was sent successfully" ret["result"] = True ret["changes"]["new"] = f"SLD data sent to {name}" else: ret["comment"] = "SLD data would have been sent successfully" ret["result"] = True ret["changes"]["new"] = f"SLD data would have been sent to {name}" return ret
# pylint: disable=unused-argument
[docs]def job_present( name, jobclass, header, steps, sid, client, message_server_host, message_server_port, logon_group, username, password, **kwargs, ): """ Ensure that a job is present in the SAP system. name Name of the job. jobclass Class of the job, on of: ``A``, ``B``, ``C`` header Header of the job. steps List of job steps. sid SID of the SAP system. message_server_host Host of the message server. message_server_port Port of the message server. client Client to connect to. logon_group Logon group to use. username Username to use for the connection. password Password to use for the connection. The dictionary provided over ``header`` must look like the SAP struct names or human readable names for job headers (see constant ``JOB_HEADER_MAPPING``): .. list-table:: JOB_HEADER_MAPPING :widths: 25 25 50 :header-rows: 1 * - Attribute Name - SAP Field - Description * - planned_start_date - SDLSTRTDT - Planned start date for background job * - planned_start_time - SDLSTRTTM - Planned start time for background job * - last_start_date - LASTSTRTDT - Latest execution date for a background job * - last_start_time - LASTSTRTTM - Latest execution time for background job * - predecessor_job_name - PREDJOB - Name of previous job * - predecessor_job_id - PREDJOBCNT - Job ID * - job_status_check - CHECKSTAT - Job status check indicator for subsequent job start * - event_id - EVENTID - Background processing event * - event_param - EVENTPARM - Background event parameters (such as jobname/jobcount) * - duration_min - PRDMINS - Duration period (in minutes) for a batch job * - duration_hours - PRDHOURS - Duration period (in hours) for a batch job * - duration_days - PRDDAYS - Duration (in days) of dba action * - duration_weeks - PRDWEEKS - Duration period (in weeks) for a batch job * - duration_months - PRDMONTHS - Duration period (in months) for a batch job * - periodic - PERIODIC - Periodic jobs indicator * - calendar_id - CALENDARID - Factory calendar id for background processing * - can_start_immediately - IMSTRTPOS - Flag indicating whether job can be started immediately * - periodic_behavior - PRDBEHAV - Period behavior of jobs on non-workdays * - workday_number - WDAYNO - No. of workday on which a job is to start * - workday_count_direction - WDAYCDIR - Count direction for 'on workday' start date of a job * - not_run_before - NOTBEFORE - Planned start date for background job * - operation_mode - OPMODE - Name of operation mode * - logical_system - LOGSYS - Logical system * - object_type - OBJTYPE - Object type * - object_key - OBJKEY - Object key * - describe_flag - DESCRIBE - Describe flag * - target_server - TSERVER - Server name * - target_host - THOST - Target system to run background job * - target_server_group - TSRVGRP - Server group name background processing The element of the list provided over ``steps`` must look like the SAP struct names or human readable names for job steps (see constant ``JOB_STEPS_MAPPING``): .. list-table:: JOB_STEPS_MAPPING :widths: 25 25 50 :header-rows: 1 * - Attribute Name - SAP Field - Description * - program_name - ABAP_PROGRAM_NAME - ABAP program name * - program_variant - ABAP_VARIANT_NAME - ABAP variant name * - username - SAP_USER_NAME - SAP user name for authorization check * - language - LANGUAGE - Language for list output .. note:: This currently only supports ABAP job steps. Example: .. code-block:: jinja SLD job SAP_SLD_DATA_COLLECT is present on S4H: sap_nwabap.job_present: - name: SAP_SLD_DATA_COLLECT - jobclass: C - header: EVENTID: SAP_SYSTEM_START - steps: - ABAP_PROGRAM_NAME: RSLDAGDS - sid: S4H - client: "000" - message_server_host: s4h - message_server_port: 3600 - logon_group: SPACE - username: SALT - password: __slot__:salt:vault.read_secret(path="nwabap/S4H/000", key="SALT") """ log.debug("Running function") name = name.upper() jobclass = jobclass.upper() ret = {"name": name, "changes": {"old": {}, "new": {}}, "comment": "", "result": False} log.debug("Parsing attributes and replacing human-readable") header = _replace_human_readable(header, mapping=JOB_HEADER_MAPPING, remove_unknown=True) steps = [ _replace_human_readable(x, mapping=JOB_STEPS_MAPPING, remove_unknown=True) for x in steps ] log.debug("Creating one connection for the lifetime of this state") if isinstance(client, int): client = f"{client:03d}" abap_connection = { "mshost": message_server_host, "msserv": str(message_server_port), "sysid": sid, "group": logon_group, "client": client, "user": username, "passwd": password, "lang": "EN", } with Connection(**abap_connection) as conn: log.debug("Logging in and retrieving session id") function_modules = { "BAPI_XMI_LOGON": { "EXTCOMPANY": "SAPUCC", "EXTPRODUCT": "PYTHON", "INTERFACE": "XBP", "VERSION": "2.0", } } success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = "Could not logon" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"](result["BAPI_XMI_LOGON"]["RETURN"]): ret["comment"] = _get_bapiret2_messages(result["BAPI_XMI_LOGON"]["RETURN"]) ret["result"] = False return ret session_id = result["BAPI_XMI_LOGON"]["SESSIONID"] log.debug("Opening a try-catch block to ensure logoff") try: log.debug(f"Checking if job {name} already exists") function_modules = { "BAPI_XBP_JOB_SELECT": { "JOB_SELECT_PARAM": { "JOBNAME": name, "USERNAME": "*", # required for BAPI_XBP_JOB_SELECT to work "PRELIM": "X", "SCHEDUL": "X", }, "EXTERNAL_USER_NAME": session_id, } } success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not lookup job {name}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"](result["BAPI_XBP_JOB_SELECT"]["RETURN"]): ret["comment"] = _get_bapiret2_messages(result["BAPI_XBP_JOB_SELECT"]["RETURN"]) ret["result"] = False return ret if result["BAPI_XBP_JOB_SELECT"]["SELECTED_JOBS"]: job = result["BAPI_XBP_JOB_SELECT"]["SELECTED_JOBS"][0] log.debug( f"Job {job['JOBNAME']} already exists with the id {job['JOBCOUNT']}, retrieving data" ) function_modules = { "BAPI_XBP_JOB_READ": { "JOBNAME": job["JOBNAME"], "JOBCOUNT": job["JOBCOUNT"], "EXTERNAL_USER_NAME": session_id, } } success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not read job {name}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_XBP_JOB_READ"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages(result["BAPI_XBP_JOB_READ"]["RETURN"]) ret["result"] = False return ret job_name = job["JOBNAME"] job_count = job["JOBCOUNT"] job_header = result["BAPI_XBP_JOB_READ"]["JOBHEAD"] job_steps = result["BAPI_XBP_JOB_READ"]["STEPS"] log.debug(f"Calculating diffs for update of header of {name}") diff = salt.utils.dictdiffer.deep_diff(job_header, header) log.debug("Removing empty dictionaries") diff = _clear_empty_dict( diff, remove_none=False ) # this will also remove empty "new" and "old" dicts new = diff.get("new", {}) # because we're only interested in the changed fields, we remove unchanged stuff from "old" old = _extract_changed_from_dict(new, diff.get("old", {})) if new: log.debug(f"Changing the following fields for {name}:\n{new}") mask = {} # because SAP doesn't like documentation, you have to check the code of FM BP_JOB_HEADER_MODIFY # to find out which MASK flags correspond to which header fields. # in case any of these fields are set, the corresponding mask flag must be set for cond in JOB_MASK_STARTCOND: if new.get(cond, None): mask["STARTCOND"] = "X" break for cond in JOB_MASK_RECIPLNT: if new.get(cond, None): mask["RECIPLNT"] = "X" break # all other fields are 1:1 mappings for field in ["THOST", "TSERVER", "TSRVGRP"]: if new.get(field, None): mask[field] = "X" if not __opts__["test"]: function_modules = { "BAPI_XBP_JOB_HEADER_MODIFY": { "JOBNAME": job_name, "JOBCOUNT": job_count, "EXTERNAL_USER_NAME": session_id, "JOB_HEADER": new, # we only need the fields the have actually changed "MASK": mask, } } # the job class must be handled separately (thx SAP!) if job_header["JOBCLASS"] != jobclass: function_modules["BAPI_XBP_JOB_HEADER_MODIFY"]["JOBCLASS"] = jobclass success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not modify header for {name}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_XBP_JOB_HEADER_MODIFY"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages( result["BAPI_XBP_JOB_HEADER_MODIFY"]["RETURN"] ) ret["result"] = False return ret # update changes ret["changes"] = {"old": old, "new": new} if job_header["JOBCLASS"] != jobclass: ret["changes"]["old"]["JOBCLASS"] = job_header["JOBCLASS"] ret["changes"]["new"]["JOBCLASS"] = jobclass log.debug(f"Checking for changes in the steps of job {name}") for i in range(0, len(steps)): # pylint: disable=consider-using-enumerate if len(job_steps) <= i: log.debug(f"The job step #{i} is missing, adding to {name}") steps[i]["JOBNAME"] = name steps[i]["JOBCOUNT"] = job_count steps[i]["EXTERNAL_USER_NAME"] = session_id if not __opts__["test"]: function_modules = {"BAPI_XBP_JOB_ADD_ABAP_STEP": steps[i]} success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not add ABAP step {steps[i]['ABAP_PROGRAM_NAME']} to {name}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_XBP_JOB_ADD_ABAP_STEP"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages( result["BAPI_XBP_JOB_ADD_ABAP_STEP"]["RETURN"] ) ret["result"] = False return ret # update changes if "STEPS" not in ret["changes"]["new"]: ret["changes"]["new"]["STEPS"] = [] del steps[i]["JOBNAME"] del steps[i]["JOBCOUNT"] del steps[i]["EXTERNAL_USER_NAME"] ret["changes"]["new"]["STEPS"].append(steps[i]) else: log.debug(f"Calculating diffs for step #{i} of {name}") # set correct names for existing job steps (field names differ between read <> modify) job_steps[i] = _replace_human_readable( job_steps[i], mapping=JOB_STEP_MODIFY_MAPPING, remove_unknown=True ) diff = salt.utils.dictdiffer.deep_diff(job_steps[i], steps[i]) log.debug("Removing empty dictionaries") diff = _clear_empty_dict( diff, remove_none=False ) # this will also remove empty "new" and "old" dicts new = diff.get("new", {}) # because we're only interested in the changed fields, we remove unchanged stuff from "old" old = _extract_changed_from_dict(new, diff.get("old", {})) if new: new["JOBNAME"] = name new["JOBCOUNT"] = job_count new["EXTERNAL_USER_NAME"] = session_id new["STEP_NUMBER"] = i + 1 # we ALWAYS need the program name, even if it doesn't change if "ABAP_PROGRAM_NAME" not in new: rm_prog = False new["ABAP_PROGRAM_NAME"] = steps[i]["ABAP_PROGRAM_NAME"] else: rm_prog = True if not __opts__["test"]: function_modules = {"BAPI_XBP_JOB_ABAP_STEP_MODIFY": new} success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not modify ABAP step #{i} of {name}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_XBP_JOB_ABAP_STEP_MODIFY"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages( result["BAPI_XBP_JOB_ABAP_STEP_MODIFY"]["RETURN"] ) ret["result"] = False return ret # update changes del new["JOBNAME"] del new["JOBCOUNT"] del new["EXTERNAL_USER_NAME"] if rm_prog: del new["ABAP_PROGRAM_NAME"] if "STEPS" not in ret["changes"]["old"]: ret["changes"]["old"]["STEPS"] = [] if "STEPS" not in ret["changes"]["new"]: ret["changes"]["new"]["STEPS"] = [] ret["changes"]["old"]["STEPS"].append(old) ret["changes"]["new"]["STEPS"].append(new) log.debug(f"Checking if there are more then {i} steps defined for {name}") if len(job_steps) > i + 1: log.debug(f"There are more steps defined on the NetWeaver server for {name}") for j in range(i + 1, len(job_steps)): log.debug(f"Deleting step #{j+1} of {name}") if not __opts__["test"]: function_modules = { "BAPI_XBP_MODIFY_JOB_STEP": { "JOBNAME": name, "JOBCOUNT": job_count, "EXTERNAL_USER_NAME": session_id, "STEP_NUM": j + 1, "DELETE": "X", } } success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not remove ABAP step {j} from {name}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_XBP_MODIFY_JOB_STEP"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages( result["BAPI_XBP_MODIFY_JOB_STEP"]["RETURN"] ) ret["result"] = False return ret # update changes if "STEPS" not in ret["changes"]["old"]: ret["changes"]["old"]["STEPS"] = [] ret["changes"]["old"]["STEPS"].append({"STEPNUMBER": j + 1}) else: log.debug(f"Job {name} does not exist, creating") # because the inital job_open and all subsequent steps are bound together by the returned JOBCOUNT, # this is one big section if not __opts__["test"]: function_modules = { "BAPI_XBP_JOB_OPEN": { "JOBNAME": name, "EXTERNAL_USER_NAME": session_id, } } success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not open job definition for {name}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_XBP_JOB_OPEN"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages( result["BAPI_XBP_JOB_OPEN"]["RETURN"] ) ret["result"] = False return ret job_count = result["BAPI_XBP_JOB_OPEN"]["JOBCOUNT"] # we need to add steps before we modify the header, otherwise we will get the error # "Job has no steps" log.debug(f"Adding steps to {name}") for step in steps: step["JOBNAME"] = name step["JOBCOUNT"] = job_count step["EXTERNAL_USER_NAME"] = session_id function_modules = {"BAPI_XBP_JOB_ADD_ABAP_STEP": step} success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not add ABAP step {step['ABAP_PROGRAM_NAME']} to {name}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_XBP_JOB_ADD_ABAP_STEP"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages( result["BAPI_XBP_JOB_ADD_ABAP_STEP"]["RETURN"] ) ret["result"] = False return ret log.debug("Modifying header") # because this is initial, we can just change everything mask = { "STARTCOND": "X", "RECIPLNT": "X", "THOST": "X", "TSERVER": "X", "TSRVGRP": "X", } function_modules = { "BAPI_XBP_JOB_HEADER_MODIFY": { "JOBNAME": name, "JOBCOUNT": job_count, "EXTERNAL_USER_NAME": session_id, "JOB_HEADER": header, "JOBCLASS": jobclass, "MASK": mask, } } success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not modify header for {name}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_XBP_JOB_HEADER_MODIFY"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages( result["BAPI_XBP_JOB_HEADER_MODIFY"]["RETURN"] ) ret["result"] = False return ret log.debug("Closing job definition") function_modules = { "BAPI_XBP_JOB_CLOSE": { "JOBNAME": name, "JOBCOUNT": job_count, "EXTERNAL_USER_NAME": session_id, } } success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not close job definition for {name}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_XBP_JOB_CLOSE"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages( result["BAPI_XBP_JOB_CLOSE"]["RETURN"] ) ret["result"] = False return ret else: job_count = "UNKNOWN" ret["changes"] = { "old": None, "new": { "JOBNAME": name, "JOBCOUNT": job_count, "HEADER": header, "STEPS": steps, }, } except Exception as exc: # pylint: disable=broad-except # handle exception log.exception(exc) raise finally: log.debug("Logging off") function_modules = { "BAPI_XMI_LOGOFF": { "INTERFACE": "XBP", } } success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = "Could not log off" log.error(msg) ret["comment"] = msg ret["result"] = False return ret # pylint: disable=lost-exception if not __salt__["sap_nwabap.process_bapiret2"](result["BAPI_XMI_LOGOFF"]["RETURN"]): ret["comment"] = _get_bapiret2_messages(result["BAPI_XMI_LOGOFF"]["RETURN"]) ret["result"] = False return ret # pylint: disable=lost-exception if not ret["changes"].get("new", None) and not ret["changes"].get("old", None): ret["changes"] = {} ret["comment"] = "No changes required" elif __opts__["test"]: ret["comment"] = f"Would have maintained job {name}" else: ret["comment"] = f"Maintained job {name}" ret["result"] = True if (not __opts__["test"] or not ret["changes"]) else None return ret
# pylint: disable=unused-argument
[docs]def job_absent( name, sid, client, message_server_host, message_server_port, logon_group, username, password, **kwargs, ): """ Ensure that a job is absent in the system. name Name of the job. sid SID of the SAP system. message_server_host Host of the message server. message_server_port Port of the message server. client Client to connect to. logon_group Logon group to use. username Username to use for the connection. password Password to use for the connection. Example: .. code-block:: jinja SLD job SAP_SLD_DATA_COLLECT is absent on S4H: sap_nwabap.job_present: - name: SAP_SLD_DATA_COLLECT - sid: S4H - client: "000" - message_server_host: s4h - message_server_port: 3600 - logon_group: SPACE - username: SALT - password: __slot__:salt:vault.read_secret(path="nwabap/S4H/000", key="SALT") """ log.debug("Running function") name = name.upper() ret = {"name": name, "changes": {"old": {}, "new": {}}, "comment": "", "result": False} log.debug("Creating one connection for the lifetime of this state") if isinstance(client, int): client = f"{client:03d}" abap_connection = { "mshost": message_server_host, "msserv": str(message_server_port), "sysid": sid, "group": logon_group, "client": client, "user": username, "passwd": password, "lang": "EN", } with Connection(**abap_connection) as conn: log.debug("Logging in and retrieving session id") function_modules = { "BAPI_XMI_LOGON": { "EXTCOMPANY": "SAPUCC", "EXTPRODUCT": "PYTHON", "INTERFACE": "XBP", "VERSION": "2.0", } } success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = "Could not logon" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"](result["BAPI_XMI_LOGON"]["RETURN"]): ret["comment"] = _get_bapiret2_messages(result["BAPI_XMI_LOGON"]["RETURN"]) ret["result"] = False return ret session_id = result["BAPI_XMI_LOGON"]["SESSIONID"] try: log.debug(f"Finding job {name}") function_modules = { "BAPI_XBP_JOB_SELECT": { "JOB_SELECT_PARAM": { "JOBNAME": name, "USERNAME": "*", # required for BAPI_XBP_JOB_SELECT to work "PRELIM": "X", "SCHEDUL": "X", }, "EXTERNAL_USER_NAME": session_id, } } success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not retrieve data for {name}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"](result["BAPI_XBP_JOB_SELECT"]["RETURN"]): ret["comment"] = _get_bapiret2_messages(result["BAPI_XBP_JOB_SELECT"]["RETURN"]) ret["result"] = False return ret if result["BAPI_XBP_JOB_SELECT"]["SELECTED_JOBS"]: if len(result["BAPI_XBP_JOB_SELECT"]["SELECTED_JOBS"]) > 1: msg = f"There is more then 1 job with the name {name}, only deleting 1" log.warning(msg) ret["warnings"] = [msg] job = result["BAPI_XBP_JOB_SELECT"]["SELECTED_JOBS"][0] log.debug(f"Job {job['JOBNAME']} exists with id {job['JOBCOUNT']}, removing") if not __opts__["test"]: function_modules = { "BAPI_XBP_JOB_DELETE": { "JOBNAME": job["JOBNAME"], "JOBCOUNT": job["JOBCOUNT"], "EXTERNAL_USER_NAME": session_id, } } success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = f"Could not delete {job['JOBNAME']}" log.error(msg) ret["comment"] = msg ret["result"] = False return ret if not __salt__["sap_nwabap.process_bapiret2"]( result["BAPI_XBP_JOB_DELETE"]["RETURN"] ): ret["comment"] = _get_bapiret2_messages( result["BAPI_XBP_JOB_DELETE"]["RETURN"] ) ret["result"] = False return ret ret["changes"] = { "old": f"{job['JOBNAME']} - {job['JOBCOUNT']} deleted", "new": None, } except Exception as exc: # pylint: disable=broad-except log.exception(exc) raise finally: log.debug("Logging off") function_modules = { "BAPI_XMI_LOGOFF": { "INTERFACE": "XBP", } } success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = "Could not log off" log.error(msg) ret["comment"] = msg ret["result"] = False return ret # pylint: disable=lost-exception if not __salt__["sap_nwabap.process_bapiret2"](result["BAPI_XMI_LOGOFF"]["RETURN"]): ret["comment"] = _get_bapiret2_messages(result["BAPI_XMI_LOGOFF"]["RETURN"]) ret["result"] = False return ret # pylint: disable=lost-exception if not ret["changes"].get("new", None) and not ret["changes"].get("old", None): ret["changes"] = {} ret["comment"] = "No changes required" elif __opts__["test"]: ret["comment"] = f"Would have removed job {name}" else: ret["comment"] = f"Removed job {name}" ret["result"] = True if (not __opts__["test"] or not ret["changes"]) else None return ret
# pylint: disable=unused-argument
[docs]def system_health_ok( name, check_from, client, message_server_host, message_server_port, logon_group, username, password, max_allowed_dumps=0, **kwargs, ): """ Check the system health by checking: - Transaction ``SICK`` - Short Dumps name SID of the SAP system. check_from Date from which on the system health should be checked (e.g. for log entries) in the format ``DDMMYYYY``, e.g. ``31129999`` or ``01012000``. max_allowed_dumps Maximum number of allowed short dumps (default: 0). message_server_host Host of the message server. message_server_port Port of the message server. client Client to connect to. logon_group Logon group to use. username Username to use for the connection. password Password to use for the connection. Example: .. code-block:: jinja System healh is OK for SAP NetWeaver AS ABAP system S4H (ST22 / SICK): sap_nwabap.system_health_ok: - name: S4H - check_from: {{ None | strftime("%d%m%Y") }} {# renders to current date, e.g. 31082022 #} - client: "000" - message_server_host: s4h - message_server_port: 3600 - logon_group: SPACE - username: SALT - password: __slot__:salt:vault.read_secret(path="nwabap/S4H/000", key="SALT") .. note:: This function does not implement ``__opts__["test"]`` since no data is changed. """ log.debug("Running function") ret = {"name": name, "changes": {}, "comment": [], "result": False} log.debug("Creating one connection for the lifetime of this state") if isinstance(client, int): client = f"{client:03d}" abap_connection = { "mshost": message_server_host, "msserv": str(message_server_port), "sysid": name, "group": logon_group, "client": client, "user": username, "passwd": password, "lang": "EN", } with Connection(**abap_connection) as conn: log.debug("Checking for ABAP dumps") function_modules = { "/SDF/EWA_GET_ABAP_DUMPS": { "BEDATUM": _convert_date(check_from), } } success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = "Could not retrieve ABAP dumps" log.error(msg) ret["comment"] = msg ret["changes"] = {} ret["result"] = False return ret num_dumps = len(result["/SDF/EWA_GET_ABAP_DUMPS"]["I_SNAP_ERROR_DAY"]) if num_dumps > max_allowed_dumps: msg = f"ST22: System contains {num_dumps} short dumps" ret["comment"].append(msg) log.debug("Checking for SICK") function_modules = { "INST_EXECUTE_REPORT": {"PROGRAM": "RSICC000"} # report behind transaction SICK } success, result = __salt__["sap_nwabap.call_fms"]( function_modules=function_modules, conn=conn ) if not success: msg = "Could not run transaction SICK" log.error(msg) ret["comment"] = msg ret["changes"] = {} ret["result"] = False return ret output = [e["ZEILE"].strip("|") for e in result["INST_EXECUTE_REPORT"]["OUTPUT_TAB"]] errors = True for line in output: if "no errors reported" in line: errors = False break if errors: log.error("SICK reported errors:") for line in output: log.error(line) ret["comment"].append(f"SICK: {output}") if ret["comment"]: ret["result"] = False else: ret["comment"] = "System health OK" ret["result"] = True return ret