Source code for saltext.sap_hostctrl._states.sap_hostctrl

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

SAP Host Agent state module
===========================
SaltStack module that implements states based on SAP Host Agent functionality.

:codeauthor:    Benjamin Wegener, Alexander Wilke
:maturity:      new
:depends:       requests
:platform:      Linux

This module implements states that utilize SAP Host Agent functionality.

.. note::
    Because functions are called over SOAP, only authenticated requests are accepted.

Currently, only basic authentication (username/password) is implemented.

.. note::
    This module can only run on linux platforms.
"""
import glob
import logging
import os
import re

import requests
import salt.utils.files
import salt.utils.http
from packaging import version
from requests.auth import HTTPBasicAuth

# Globals
log = logging.getLogger(__name__)

__virtualname__ = "sap_hostctrl"

HOST_AGENT_DIR = "/usr/sap/hostctrl"


# pylint: disable=unused-argument
[docs]def system_installed(name, password, username="sapadm", **kwargs): """ Checks if an SAP system is installed over the SAP Host Agent function ListSystems. name SID of the SAP system. password Password for the user that executes SAP Host Agent commands (see ``username``). username User that executes SAP Host Agent commands, default is ``sapadm``. This state can be used to initially check if an SAP system is installed before running many other states against it. Example: .. code-block:: jinja SAP System S4H is installed: sap_hostctrl.system_installed: - name: S4H - username: sapadm - password: __slot__:salt:vault.read_secret(path="os", key="sapadm") """ log.debug("Running function") ret = { "name": name, "changes": {}, "result": False, "comment": "", } if name in __salt__["sap_hostctrl.list_systems"](username=username, password=password): ret["comment"] = f"SAP system {name} is installed" ret["result"] = True else: ret["comment"] = f"SAP system {name} is not installed" ret["result"] = False return ret
# pylint: disable=unused-argument
[docs]def outside_discovery_executed( name, sld_port, sld_user, sld_password, password, username="sapadm", overwrite=False, keep_other_config=False, **kwargs, ): """ Ensure that SAP Host Agent is configured to use outside discovery and is executed. This state can manage multiple SLD / LMDB configurations, see parameter ``keep_other_config`` for more information. name SLD / LMDB fully qualified domain name. sld_port Port of the SLD / LMDB. sld_username Username used for authentication against the SLD / LMDB. sld_password Password used for authentication against the SLD / LMDB. password Password for the user that executes SAP Host Agent commands (see ``username``). username User that executes SAP Host Agent commands, default is ``sapadm``. overwrite Overwrite the SLD configuration even if the correct configuration is already set, default is ``False``. keep_other_config If set to True, other SLD configurations are not removed. Default is ``False``. .. warning: You need to set the parameter ``service/trace = 2`` in ``host_profile`` in order for the SAP Host Agent to write logs that contain information about the result of the last executed outside discovery. This behavior differs between CLI and SOAP exection. If you do not set the parameter, the outside discovery will always be executed. Example: .. code-block:: jinja Outside Discovery is executed: sap_hostctrl.outside_discovery_executed: - name: sol.my.domain - sld_port: 50000 - sld_username: SLD_DS_USER - sld_password: __slot__:salt:vault.read_secret(path="SAP", key="SLD_DS_USER") - username: sapadm - password: __slot__:salt:vault.read_secret(path="os", key="sapadm") """ log.debug("Running function") ret = { "name": name, "changes": { "new": [], "old": [], }, "result": False, "comment": "", } sldreg_dir = f"{HOST_AGENT_DIR}/exe" sldreg_bin = f"{sldreg_dir}/sldreg" conf = f"{HOST_AGENT_DIR}/exe/config.d/slddest_{name}_{sld_port}.cfg" log_file = f"{HOST_AGENT_DIR}/work/outsidediscovery.log" if not keep_other_config: log.debug( f"Checking and removing other SLD configurations in {HOST_AGENT_DIR}/exe/config.d/" ) sld_configs = glob.glob(f"{HOST_AGENT_DIR}/exe/config.d/slddest_*.cfg") if conf in sld_configs: sld_configs.remove(conf) for sld_cfg in sld_configs: if __opts__["test"]: ret["changes"]["old"].append(f"Would remove {sld_cfg}") else: os.remove(sld_cfg) ret["changes"]["old"].append(f"Removed {sld_cfg}") log.debug("Check if SAP Host Agent is configured to use outside discovery") update_cfg = True if __salt__["file.file_exists"](conf): log.debug("Getting existing config") cmd = " ".join([sldreg_bin, "-showconnect", conf]) result = __salt__["cmd.run_all"]( cmd=cmd, runas=username, env={"LD_LIBRARY_PATH": sldreg_dir} ) if result["retcode"] != 0: return False log.debug("Parsing output") existing_config = {} log.debug(f"Existing config: {result['stdout']}") for line in result["stdout"].split("\n"): log.debug(f"Line: {line}") for param in ["host_param", "https_param", "port_param", "user_param"]: log.debug(f"Checking {param}") if param in line: line_num = line.find(param) key, value = line[line_num:].split("=", 1) existing_config[key] = value.strip("'") log.debug(f"Existing config: {existing_config}") if ( sld_user == existing_config.get("user_param", None) and name == existing_config.get("host_param", None) and str(sld_port) == str(existing_config.get("port_param", "")) and "y" == existing_config.get("https_param", None) ): update_cfg = False if update_cfg: ret["changes"]["old"].append("Outside Discovery is not configured correctly") else: ret["changes"]["old"].append("Outside Discovery is configured correctly") if overwrite or update_cfg: log.debug("Configure outside discovery") if __opts__["test"]: ret["changes"]["new"].append("Outside Discovery would be configured") else: result = __salt__["sap_hostctrl.configure_outside_discovery"]( name, sld_port, sld_user, sld_password, username, password ) if not isinstance(result, bool) or not result: msg = "Cannot configure SAP Host Agent to use outside discovery" log.error(msg) ret["comment"] = msg ret["result"] = False return ret ret["changes"]["new"].append("Outside Discovery is configured") else: # Note: the log file is only written if service/trace = 2 is set when running the outside # discovery over SOAP! log.debug(f"Checking {log_file} for existing outside discovery success") re_rc = re.compile(r"Return code: ([0-9]{3})") success = True log.debug(f"Checking {log_file}") try: log_file_data = __salt__["file.read"](log_file) except FileNotFoundError: log.debug(f"{log_file} does not exist") success = False else: return_codes = re_rc.findall(log_file_data) log.debug(f"Got result from checkup: {return_codes}") if not return_codes or int(return_codes[-1]) != 200: log.debug("Could not find any return codes or last return code is not 200") success = False else: log.debug( f"Last return code is {return_codes[-1]}, outside discovery was already successful" ) if success: ret["comment"] = "No changes required" ret["changes"] = {} ret["result"] = None return ret else: log.debug("Outside was not executed successfully, running again") ret["changes"]["old"].append("Outside Discovery was not yet executed sucessfully") log.debug("Remove old logfile") if __opts__["test"]: ret["changes"]["old"].append(f"Would remove {log_file}") else: result = __salt__["file.remove"](log_file) if result: ret["changes"]["old"].append(f"Removed {log_file}") log.debug("Executing outside discovery") if __opts__["test"]: ret["comment"] = "Outside discovery would be maintained and executed" ret["changes"]["new"].append("Outside discovery would be executed") ret["result"] = None else: result = __salt__["sap_hostctrl.execute_outside_discovery"](username, password) if not isinstance(result, bool) or not result: log.error("Cannot execute outside discovery") ret["comment"] = "Outside discovery configuration is maintained but execution failed" ret["result"] = False else: ret[ "comment" ] = "Outside discovery configuration is maintained and was executed successfully" ret["changes"]["new"].append("Outside discovery was executed succesfully") ret["result"] = True return ret
# pylint: disable=unused-argument
[docs]def sda_installed( name, jvm_arch, password, username="sapadm", verify=True, overwrite=False, **kwargs ): """ Ensures that a Simple Diagnostics Agent is installed on the SAP Host Agent. name Path to the SDA SAR archive. jvm_arch Path to the SAPJVM SAR archive. password Password for the user that executes SAP Host Agent commands (see "username"). username User that executes SAP Host Agent commands, default is "sapadm". verify If set to False, HTTPS connections will not be verified. overwrite If set to True, SDA will be installed, even if it was already installed. .. note:: This state will use the CA bundle of the OS to determine the validity of the HTTPS connection. Example: .. code-block:: jinja Simple Diagnostics Agent is installed: sap_hostctrl.sda_installed: - name: /mnt/nfs/SIMDIAGAGNT1SP60P_3-70002252.SAR - jvm_arch: /mnt/nfs/SAPJVM8_90-80000202.SAR - username: sapadm - password: __slot__:salt:vault.read_secret(path="os", key="sapadm") """ log.debug("Running function") ret = { "name": name, "changes": {}, "result": False, "comment": "", } fqdn = __grains__["fqdn"] session = requests.Session() if verify: session.verify = salt.utils.http.get_ca_bundle() else: session.verify = False session.auth = HTTPBasicAuth(username, password) sda_inst = False if not overwrite: log.debug("Checking if SDA is already installed") url = f"https://{fqdn}:1129/lmsl/sda/default/?service=ping" response = session.get(url) log.trace(f"Raw response:\n{response.text}") if response.ok: # if we can retrieve and parse the version info, then SDA is installed try: sda_info = response.json() version.parse(sda_info["software"]) except: # pylint: disable=bare-except pass else: sda_inst = True if not sda_inst: log.debug("SDA is not installed or overwrite set, installing") if __opts__["test"]: if overwrite: ret["changes"] = { "old": "SDA was perhaps installed", "new": "SDA would be installed", } else: ret["changes"] = {"old": "SDA was not installed", "new": "SDA would be installed"} else: url = f"https://{fqdn}:1129/SMDAgent/deploy" with salt.utils.files.fopen(name, "rb") as sda_archive_f, salt.utils.files.fopen( jvm_arch, "rb" ) as jvm_archive_f: files = { "sda-archive": sda_archive_f, "jvm-archive": jvm_archive_f, } response = session.post(url, files=files) log.trace(f"Raw response:\n{response.text}") if not response.ok: log.error(f"Could not upload SDA:\n{response.text}") ret["comment"] = "Could not install SDA" ret["changes"] = {} ret["result"] = False else: log.debug("Installed SDA") ret["comment"] = "Installed SDA" if overwrite: ret["changes"] = { "old": "SDA was perhaps installed", "new": "SDA is installed", } else: ret["changes"] = {"old": "SDA was not installed", "new": "SDA is installed"} else: log.debug("SDA is already installed") ret["comment"] = "No changes required" ret["result"] = True ret["changes"] = {} ret["result"] = True if (not __opts__["test"] or not ret["changes"]) else None return ret