diff --git a/CHANGELOG.rst b/CHANGELOG.rst index aef6fa1..57e2936 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,20 @@ Community SAP Release Notes .. contents:: Topics +v1.6.0 +====== + +Release Summary +--------------- + +This release adds new module `sap_hostctrl_exec` that allows executing `SAPHostcontrol` functions. +New contributors readme is added to show our appreciation to contributors. + +Minor Changes +------------- +- sap_control_exec - Add local socket support (https://github.com/sap-linuxlab/community.sap_libs/pull/66) +- sap_hostctrl_exec - Add new module and tests (https://github.com/sap-linuxlab/community.sap_libs/pull/67) + v1.5.0 ====== diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..cde516f --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,93 @@ +# Contributors to community.sap_libs + +Thank you to all contributors who have helped make this project possible! + +We welcome contributions! Please see our [contributing guidelines](https://sap-linuxlab.github.io/initiative_contributions/) to get started. + +## Maintainers + +The following people are the maintainers of this collection: + +### SUSE + +- [Marcel Mamula](https://github.com/marcelmamula) + + +For specific role maintainers, see the `README.md` file in the corresponding role's directory. + +## All Contributors + +| Name | Commits | Lines Changed | Last Commit | +| ---- | ------- | ------------- | ----------- | +| [Rainer Leber](https://github.com/rainerleber) | 23 | 11921 | 2025-09-22 | +| ydouvry | 8 | 892 | 2025-12-09 | +| Nicolas Bettembourg | 7 | 265 | 2025-10-27 | +| [Marcel Mamula](https://github.com/marcelmamula) | 4 | 1404 | 2025-10-16 | +| stm85 | 1 | 4 | 2024-04-29 | +| [Sean Freeman](https://github.com/sean-freeman) | 1 | 3 | 2022-11-11 | + +## Contributions by Module + +### Module: sap_control_exec.py + +| Name | Commits | Lines Changed | Last Commit | +| ---- | ------- | ------------- | ----------- | +| Nicolas Bettembourg | 4 | 221 | 2025-10-27 | +| [Rainer Leber](https://github.com/rainerleber) | 4 | 555 | 2025-09-22 | + +### Module: sap_pyrfc.py + +| Name | Commits | Lines Changed | Last Commit | +| ---- | ------- | ------------- | ----------- | +| [Rainer Leber](https://github.com/rainerleber) | 4 | 191 | 2023-03-08 | + +### Module: sap_snote.py + +| Name | Commits | Lines Changed | Last Commit | +| ---- | ------- | ------------- | ----------- | +| [Rainer Leber](https://github.com/rainerleber) | 3 | 269 | 2022-12-05 | + +### Module: sap_system_facts.py + +| Name | Commits | Lines Changed | Last Commit | +| ---- | ------- | ------------- | ----------- | +| [Rainer Leber](https://github.com/rainerleber) | 2 | 215 | 2022-09-09 | + +### Module: sap_task_list_execute.py + +| Name | Commits | Lines Changed | Last Commit | +| ---- | ------- | ------------- | ----------- | +| [Rainer Leber](https://github.com/rainerleber) | 3 | 352 | 2022-12-05 | + +### Module: sap_hostctrl_exec.py + +| Name | Commits | Lines Changed | Last Commit | +| ---- | ------- | ------------- | ----------- | +| ydouvry | 8 | 506 | 2025-12-09 | + +### Module: sapcar_extract.py + +| Name | Commits | Lines Changed | Last Commit | +| ---- | ------- | ------------- | ----------- | +| [Rainer Leber](https://github.com/rainerleber) | 2 | 230 | 2022-09-09 | + +### Module: sap_hdbsql.py + +| Name | Commits | Lines Changed | Last Commit | +| ---- | ------- | ------------- | ----------- | +| [Marcel Mamula](https://github.com/marcelmamula) | 1 | 2 | 2025-09-25 | +| stm85 | 1 | 2 | 2024-04-29 | +| [Rainer Leber](https://github.com/rainerleber) | 1 | 246 | 2022-09-09 | + +### Module: sap_user.py + +| Name | Commits | Lines Changed | Last Commit | +| ---- | ------- | ------------- | ----------- | +| [Rainer Leber](https://github.com/rainerleber) | 3 | 510 | 2022-12-05 | + +### Module: sap_company.py + +| Name | Commits | Lines Changed | Last Commit | +| ---- | ------- | ------------- | ----------- | +| [Rainer Leber](https://github.com/rainerleber) | 3 | 337 | 2022-12-05 | + diff --git a/README.md b/README.md index f27bf15..30dd23f 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ modules where we are able to execute integration test we decided to disable thes You can find more information about maintainers of this Ansible Collection at [MAINTAINERS.md](https://github.com/sap-linuxlab/community.sap_libs/blob/main/MAINTAINERS.md). ## Contributing -You can find more information about ways you can contribute at [sap-linuxlab website](https://sap-linuxlab.github.io/initiative_contributions/). +We welcome contributions to this collection. For a list of all contributors and information on how you can get involved, please see our [CONTRIBUTORS document](./CONTRIBUTORS.md). ## Support You can report any issues using [Issues](https://github.com/sap-linuxlab/community.sap_libs/issues) section. diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 2b8a93a..0218f03 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -136,3 +136,11 @@ releases: release_summary: This release removes `Python 2` support and updates `ansible-test` workflow to validate latest versions. Documentation was updated to reflect supported and tested versions. release_date: '2025-09-25' + 1.6.0: + changes: + minor_changes: + - sap_control_exec - Add local socket support (https://github.com/sap-linuxlab/community.sap_libs/pull/66) + - sap_hostctrl_exec - Add new module and tests (https://github.com/sap-linuxlab/community.sap_libs/pull/67) + release_summary: This release adds new module `sap_hostctrl_exec` that allows executing `SAPHostcontrol` functions. + New contributors readme is added to show our appreciation to contributors. + release_date: '2025-12-10' diff --git a/galaxy.yml b/galaxy.yml index 0f8c129..39808e1 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -2,7 +2,7 @@ namespace: community name: sap_libs -version: 1.5.0 +version: 1.6.0 readme: README.md authors: - Rainer Leber (github.com/rainerleber) diff --git a/plugins/modules/sap_control_exec.py b/plugins/modules/sap_control_exec.py index e761cef..ec510b9 100644 --- a/plugins/modules/sap_control_exec.py +++ b/plugins/modules/sap_control_exec.py @@ -26,6 +26,8 @@ - Provides support for sapstartsrv formaly known as sapcontrol - A complete information of all functions and the parameters can be found here U(https://www.sap.com/documents/2016/09/0a40e60d-8b7c-0010-82c7-eda71af511fa.html) + - When hostname is 'localhost', sysnr is set and no username/password are provided, the module will attempt + to use local Unix socket authentication (which works with 'become' privilege escalation). options: sysnr: @@ -36,6 +38,7 @@ port: description: - The port number of the sapstartsrv. + - If provided, the module will use always use http connection instead of local socket. required: false type: int username: @@ -157,14 +160,27 @@ function: GetProcessList port: 50113 -- name: ParameterValue +- name: ParameterValue with authentication community.sap_libs.sap_control_exec: hostname: 192.168.8.15 sysnr: "01" username: hdbadm - password: test1234# + password: test1234 function: ParameterValue parameter: ztta + +- name: GetVersionInfo using local Unix socket (requires become) + community.sap_libs.sap_control_exec: + sysnr: "00" + function: GetVersionInfo + become: true + +- name: GetProcessList using local Unix socket as SAP admin user + community.sap_libs.sap_control_exec: + sysnr: "00" + function: GetProcessList + become: true + become_user: "{{ sap_sid | lower }}adm" """ RETURN = r''' @@ -213,15 +229,73 @@ from ansible.module_utils.basic import AnsibleModule, missing_required_lib import traceback +import socket +import os + +try: + from urllib.request import HTTPHandler +except ImportError: + from ansible.module_utils.urls import ( + UnixHTTPHandler as HTTPHandler, + ) + +try: + from http.client import HTTPConnection +except ImportError: + from httplib import HTTPConnection + try: from suds.client import Client from suds.sudsobject import asdict + from suds.transport.http import HttpAuthenticated, HttpTransport + HAS_SUDS_LIBRARY = True + SUDS_LIBRARY_IMPORT_ERROR = None + + class LocalSocketHttpAuthenticated(HttpAuthenticated): + """Authenticated HTTP transport using Unix domain sockets.""" + def __init__(self, socketpath, **kwargs): + HttpAuthenticated.__init__(self, **kwargs) + self._socketpath = socketpath + + def u2handlers(self): + handlers = HttpTransport.u2handlers(self) + handlers.append(LocalSocketHandler(socketpath=self._socketpath)) + return handlers + except ImportError: HAS_SUDS_LIBRARY = False SUDS_LIBRARY_IMPORT_ERROR = traceback.format_exc() -else: - SUDS_LIBRARY_IMPORT_ERROR = None - HAS_SUDS_LIBRARY = True + + # Define dummy class when suds is not available + class LocalSocketHttpAuthenticated(object): + def __init__(self, socketpath, **kwargs): + pass + + def u2handlers(self): + return [] + + +class LocalSocketHttpConnection(HTTPConnection): + """HTTP connection class that uses Unix domain sockets.""" + def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, + source_address=None, socketpath=None): + super(LocalSocketHttpConnection, self).__init__(host, port, timeout, source_address) + self.socketpath = socketpath + + def connect(self): + """Connect to Unix domain socket.""" + self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.sock.connect(self.socketpath) + + +class LocalSocketHandler(HTTPHandler): + """HTTP handler for Unix domain sockets.""" + def __init__(self, debuglevel=0, socketpath=None): + self._debuglevel = debuglevel + self._socketpath = socketpath + + def http_open(self, req): + return self.do_open(LocalSocketHttpConnection, req, socketpath=self._socketpath) def choices(): @@ -260,9 +334,27 @@ def recursive_dict(suds_object): return out -def connection(hostname, port, username, password, function, parameter): - url = 'http://{0}:{1}/sapcontrol?wsdl'.format(hostname, port) - client = Client(url, username=username, password=password) +def connection(hostname, port, username, password, function, parameter, sysnr=None, use_local=False): + if use_local and sysnr is not None: + # Use Unix domain socket for local connection + unix_socket = "/tmp/.sapstream5{0}13".format(str(sysnr).zfill(2)) + + # Check if socket exists + if not os.path.exists(unix_socket): + raise Exception("SAP control Unix socket not found: {0}".format(unix_socket)) + + url = "http://localhost/sapcontrol?wsdl" + + try: + localsocket = LocalSocketHttpAuthenticated(unix_socket) + client = Client(url, transport=localsocket) + except Exception as e: + raise Exception("Failed to connect via Unix socket: {0}".format(str(e))) + else: + # Use HTTP connection (original behavior) + url = 'http://{0}:{1}/sapcontrol?wsdl'.format(hostname, port) + client = Client(url, username=username, password=password) + _function = getattr(client.service, function) if parameter is not None: result = _function(parameter) @@ -288,6 +380,7 @@ def main(): parameter=dict(type='str', required=False), force=dict(type='bool', default=False), ), + # Remove strict requirements to allow local mode required_one_of=[('sysnr', 'port')], mutually_exclusive=[('sysnr', 'port')], supports_check_mode=False, @@ -309,26 +402,46 @@ def main(): msg=missing_required_lib('suds'), exception=SUDS_LIBRARY_IMPORT_ERROR) + # Validate arguments + if sysnr is None and port is None: + module.fail_json(msg="Either 'sysnr' or 'port' must be provided") + + if sysnr is not None and port is not None: + module.fail_json(msg="'sysnr' and 'port' are mutually exclusive") + if function == "Stop": if force is False: module.fail_json(msg="Stop function requires force: True") + # Determine if we should use local Unix socket connection + # Use local if hostname is localhost and no username/password provided + use_local = (hostname == "localhost" and + username is None and + password is None and + sysnr is not None) + if port is None: try: - try: - conn = connection(hostname, "5{0}14".format((sysnr).zfill(2)), username, password, function, parameter) - except Exception: - conn = connection(hostname, "5{0}13".format((sysnr).zfill(2)), username, password, function, parameter) + if use_local: + # Try local connection first + conn = connection(hostname, None, username, password, function, parameter, sysnr, use_local=True) + else: + # Try HTTP ports + try: + conn = connection(hostname, "5{0}14".format((sysnr).zfill(2)), username, password, function, parameter, sysnr) + except Exception: + conn = connection(hostname, "5{0}13".format((sysnr).zfill(2)), username, password, function, parameter, sysnr) except Exception as err: result['error'] = str(err) else: try: - conn = connection(hostname, port, username, password, function, parameter) + conn = connection(hostname, port, username, password, function, parameter, sysnr, use_local=False) except Exception as err: result['error'] = str(err) if result['error'] != '': - result['msg'] = 'Something went wrong connecting to the SAPCONTROL SOAP API.' + connection_type = "Unix socket" if use_local else "SOAP API" + result['msg'] = 'Something went wrong connecting to the {0}.'.format(connection_type) module.fail_json(**result) if conn is not None: diff --git a/plugins/modules/sap_hostctrl_exec.py b/plugins/modules/sap_hostctrl_exec.py new file mode 100644 index 0000000..21a0cfd --- /dev/null +++ b/plugins/modules/sap_hostctrl_exec.py @@ -0,0 +1,466 @@ +#!/usr/bin/python + +# Copyright: (c) 2022, Rainer Leber rainerleber@gmail.com, rainer.leber@sva.de, +# Robert Kraemer @rkpobe, robert.kraemer@sva.de, +# Yannick Douvry, ydouvry@oxya.com +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: sap_hostctrl_exec + +short_description: Ansible Module to execute SAPHostControl + +version_added: "1.6.0" + +description: + - Provides support for SAP Host Agent + - More information of some functions can be found here + U(https://help.sap.com/docs/host-agent/saphostcontrol-web-service-interface) + - When hostname is 'localhost' and no username/password are provided, the module will attempt + to use local Unix socket authentication (which works with 'become' privilege escalation). + +options: + port: + description: + - The port number of the sapstartsrv (usually 1128 and 1129). + - If provided, the module will use always use http connection instead of local socket. + required: false + type: int + username: + description: + - The username to connect to the sapstartsrv. + required: false + type: str + password: + description: + - The password to connect to the sapstartsrv. + required: false + type: str + hostname: + description: + - The hostname to connect to the sapstartsrv. + - Could be an IP address, FQDN or hostname. + required: false + default: localhost + type: str + function: + description: + - The function to execute. + required: true + choices: + - ACOSPrepare + - AttachDatabase + - CallServiceOperation + - CancelOperation + - ConfigureOutsideDiscovery + - ConfigureOutsideDiscoveryDestination + - ConfigureOutsideDiscoveryPath + - DeployConfiguration + - DeployManagedObjectsFromSAR + - DetachDatabase + - DetectManagedObjects + - ExecuteDatabaseOperation + - ExecuteInstallationProcedure + - ExecuteOperation + - ExecuteOutsideDiscovery + - ExecuteUpgradeProcedure + - FinalizeDatabaseCopy + - GetCIMObject + - GetComputerSystem + - GetDatabaseProperties + - GetDatabaseStatus + - GetDatabaseSystemStatus + - GetIpAddressProperties + - GetOperationResults + - ListDatabases + - ListDatabaseSystems + - ListInstances + - LiveDatabaseUpdate + - PrepareDatabaseCopy + - RegisterInstanceService + - SetDatabaseProperty + - StartDatabase + - StartInstance + - StopDatabase + - StopInstance + - UnregisterInstanceService + type: str + parameters: + description: + - A dictionary containing all the parameters to pass to the function. + - This option is mandatory for most of the functions, only a few like ListInstances or ListDatabases can be run without option. + - Be careful, no validation is done by this module regarding the suboptions. + - An analysis of the WSDL file must be done to provide correct parameters. + - See also the examples section for more appreciation. + required: false + type: dict + force: + description: + - Forces the execution of the function C(Stop). + required: false + default: false + type: bool +author: + - Rainer Leber (@RainerLeber) + - Robert Kraemer (@rkpobe) + - Yannick Douvry (@ydouvry) +notes: + - Does not support C(check_mode). +''' + + +EXAMPLES = r""" +- name: ListDatabases with custom host and port + community.sap_libs.sap_hostctrl_exec: + hostname: 192.168.8.15 + function: ListDatabases + port: 1128 + +- name: ListInstances using local Unix socket (requires become) + community.sap_libs.sap_hostctrl_exec: + function: ListInstances + become: true + +- name: ListInstances using local Unix socket as SAP admin user and selector parameters + community.sap_libs.sap_hostctrl_exec: + function: ListInstances + parameters: + aSelector: + aInstanceStatus: S-INSTALLED # S-INSTALLED | S_RUNNING | S-STOPPED | S-LAST + become: true + become_user: "{{ sap_sid | lower }}adm" + +- name: StartInstance with authentication + community.sap_libs.sap_hostctrl_exec: + hostname: 192.168.8.15 + username: tstadm + password: test1234 + function: StartInstance + parameters: + aInstance: + mSid: 'TST' + mSystemNumber: '01' + aOptions: + mTimeout: -1 # -1=synchronous, 0=async, >0=wait timeout in seconds + mSoftTimeout: 0 + mOptions: + - O-INSTANCE + +- name: Synchronous StartDatabase using local Unix socket with arguments + community.sap_libs.sap_hostctrl_exec: + function: StartDatabase + parameters: + aArguments: + item: "{{ dict_arguments | dict2items(key_name='mKey', value_name='mValue') }}" + aOptions: + mTimeout: -1 + vars: + dict_arguments: + Database/Name: SYSTEMDB@XDH + Database/Type: hdb # hdb|ora|mss|db6|ada|sap|syb|ase|db2|max + # Database/InstanceName: HDB00 # the following parameters are optional + # Database/Host: mydbhost.example.com + # Database/Username: SYSTEM + # Database/Password: StarWarsFTW123! + become: true + +- name: Example of GetDatabaseStatus + community.sap_libs.sap_hostctrl_exec: + function: GetDatabaseStatus + parameters: + aArguments: + item: "{{ dict_arguments | dict2items(key_name='mKey', value_name='mValue') }}" + vars: + dict_arguments: + Database/Name: XDH + Database/Type: hdb + become: true + +# Example from https://help.sap.com/docs/host-agent/saphostcontrol-web-service-interface/executeoperation +- name: Asynchronous ExecuteOperation + community.sap_libs.sap_hostctrl_exec: + function: ExecuteOperation + parameters: + aOperation: "sayhello" + aArguments: + item: + mKey: "MY_NAME" + mValue: "Sally" + register: operation_say_hello + become: true + +- name: Check results of previous ExecuteOperation + community.sap_libs.sap_hostctrl_exec: + function: GetOperationResults + parameters: + aOperationID: "{{ operation_say_hello.out[0].mOperationID }}" + aOptions: + mTimeout: -1 + become: true + +# Output of GetOperationResults for the above ExecuteOperation : +# +# changed: true +# error: "" +# msg: Succesful execution of: GetOperationResults +# out: +# - mOperationID: "42010A3F050B1FD0B5A26EF66B9FA7B7" +# mOperationResults: +# item: +# - mMessageKey: description +# mMessageValue: Say hello +# - mMessageKey: null +# mMessageValue: "\"hello Sally\"" +# - mMessageKey: exitcode +# mMessageValue: 0 +""" + +RETURN = r''' +msg: + description: Success-message with functionname. + type: str + returned: always + sample: 'Succesful execution of: ListInstances' +out: + description: The full output of the required function. + type: list + elements: dict + returned: always + sample: [{ + "item": [ + { + "mHostname": "test-vm-001", + "mSapVersionInfo": "793, patch 200, commit ec1833e294d84a70c04c6a1b01fd1a493f5c72fb", + "mSid": "TST", + "mSystemNumber": "01" + }, + { + "mHostname": "test-vm-001", + "mSapVersionInfo": "793, patch 200, commit ec1833e294d84a70c04c6a1b01fd1a493f5c72fb", + "mSid": "TST", + "mSystemNumber": "00" + }] + }] +''' + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +import traceback +import socket +import os + +try: + from urllib.request import HTTPHandler +except ImportError: + from ansible.module_utils.urls import ( + UnixHTTPHandler as HTTPHandler, + ) + +try: + from http.client import HTTPConnection +except ImportError: + from httplib import HTTPConnection + +try: + from suds.client import Client + from suds.sudsobject import asdict + from suds.transport.http import HttpAuthenticated, HttpTransport + HAS_SUDS_LIBRARY = True + SUDS_LIBRARY_IMPORT_ERROR = None + + class LocalSocketHttpAuthenticated(HttpAuthenticated): + """Authenticated HTTP transport using Unix domain sockets.""" + def __init__(self, socketpath, **kwargs): + HttpAuthenticated.__init__(self, **kwargs) + self._socketpath = socketpath + + def u2handlers(self): + handlers = HttpTransport.u2handlers(self) + handlers.append(LocalSocketHandler(socketpath=self._socketpath)) + return handlers + +except ImportError: + HAS_SUDS_LIBRARY = False + SUDS_LIBRARY_IMPORT_ERROR = traceback.format_exc() + + # Define dummy class when suds is not available + class LocalSocketHttpAuthenticated(object): + def __init__(self, socketpath, **kwargs): + pass + + def u2handlers(self): + return [] + + +class LocalSocketHttpConnection(HTTPConnection): + """HTTP connection class that uses Unix domain sockets.""" + def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, + source_address=None, socketpath=None): + super(LocalSocketHttpConnection, self).__init__(host, port, timeout, source_address) + self.socketpath = socketpath + + def connect(self): + """Connect to Unix domain socket.""" + self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.sock.connect(self.socketpath) + + +class LocalSocketHandler(HTTPHandler): + """HTTP handler for Unix domain sockets.""" + def __init__(self, debuglevel=0, socketpath=None): + self._debuglevel = debuglevel + self._socketpath = socketpath + + def http_open(self, req): + return self.do_open(LocalSocketHttpConnection, req, socketpath=self._socketpath) + + +def choices(): + retlist = ["ACOSPrepare", "AttachDatabase", "CallServiceOperation", "CancelOperation", "ConfigureOutsideDiscovery", + "ConfigureOutsideDiscoveryDestination", "ConfigureOutsideDiscoveryPath", "DeployConfiguration", "DeployManagedObjectsFromSAR", + "DetachDatabase", "DetectManagedObjects", "ExecuteDatabaseOperation", "ExecuteInstallationProcedure", "ExecuteOperation", + "ExecuteOutsideDiscovery", "ExecuteUpgradeProcedure", "FinalizeDatabaseCopy", "GetCIMObject", "GetComputerSystem", + "GetDatabaseProperties", "GetDatabaseStatus", "GetDatabaseSystemStatus", "GetIpAddressProperties", "GetOperationResults", + "ListDatabases", "ListDatabaseSystems", "ListInstances", "LiveDatabaseUpdate", "PrepareDatabaseCopy", "RegisterInstanceService", + "SetDatabaseProperty", "StartDatabase", "StartInstance", "StopDatabase", "StopInstance", "UnregisterInstanceService"] + return retlist + + +# converts recursively the suds object to a dictionary e.g. {'item': [{'name': hdbdaemon, 'value': '1'}]} +def recursive_dict(suds_object): + out = {} + if isinstance(suds_object, str): + return suds_object + for k, v in asdict(suds_object).items(): + if hasattr(v, '__keylist__'): + out[k] = recursive_dict(v) + elif isinstance(v, list): + out[k] = [] + for item in v: + if hasattr(item, '__keylist__'): + out[k].append(recursive_dict(item)) + else: + out[k].append(item) + else: + out[k] = v + return out + + +def connection(hostname, port, username, password, function, parameters, use_local=False): + if use_local: + # Use Unix domain socket for local connection + unix_socket = "/tmp/.sapstream1128" + + # Check if socket exists + if not os.path.exists(unix_socket): + raise Exception("SAP control Unix socket not found: {0}".format(unix_socket)) + + url = "http://localhost/SAPHostControl/?wsdl" + + try: + localsocket = LocalSocketHttpAuthenticated(unix_socket) + client = Client(url, transport=localsocket) + except Exception as e: + raise Exception("Failed to connect via Unix socket: {0}".format(str(e))) + else: + # Use HTTP connection (original behavior) + url = 'http://{0}:{1}/SAPHostControl/?wsdl'.format(hostname, port) + client = Client(url, username=username, password=password) + + _function = getattr(client.service, function) + if parameters is not None: + result = _function(**parameters) + else: + result = _function() + + return result + + +def main(): + module = AnsibleModule( + argument_spec=dict( + port=dict(type='int', required=False), + username=dict(type='str', required=False), + password=dict(type='str', no_log=True, required=False), + hostname=dict(type='str', default="localhost"), + function=dict(type='str', required=True, choices=choices()), + parameters=dict(type='dict', required=False), + force=dict(type='bool', default=False), + ), + supports_check_mode=False, + ) + result = dict(changed=False, msg='', out={}, error='') + params = module.params + + port = params['port'] + username = params['username'] + password = params['password'] + hostname = params['hostname'] + function = params['function'] + parameters = params['parameters'] + force = params['force'] + + if not HAS_SUDS_LIBRARY: + module.fail_json( + msg=missing_required_lib('suds'), + exception=SUDS_LIBRARY_IMPORT_ERROR) + + # Validate arguments + if function == "StopDatabase" or function == "StopInstance": + if force is False: + module.fail_json(msg="Stop function requires force: True") + + # Determine if we should use local Unix socket connection + # Use local if hostname is localhost and no username/password provided + use_local = (hostname == "localhost" and + username is None and + password is None) + + if port is None: + try: + if use_local: + # Try local connection first + conn = connection(hostname, None, username, password, function, parameters, use_local=True) + else: + # Try HTTP ports + try: + conn = connection(hostname, "1129", username, password, function, parameters) + except Exception: + conn = connection(hostname, "1128", username, password, function, parameters) + except Exception as err: + result['error'] = str(err) + else: + try: + conn = connection(hostname, port, username, password, function, parameters, use_local=False) + except Exception as err: + result['error'] = str(err) + + if result['error'] != '': + connection_type = "Unix socket" if use_local else "SOAP API" + result['msg'] = 'Something went wrong connecting to the {0}.'.format(connection_type) + module.fail_json(**result) + + if conn is not None: + returned_data = recursive_dict(conn) + else: + returned_data = conn + + result['changed'] = True + result['msg'] = "Succesful execution of: " + function + result['out'] = [returned_data] + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/tests/sanity/ignore-2.14.txt b/tests/sanity/ignore-2.14.txt index 3883757..ea41c7a 100644 --- a/tests/sanity/ignore-2.14.txt +++ b/tests/sanity/ignore-2.14.txt @@ -2,6 +2,7 @@ plugins/modules/sap_pyrfc.py validate-modules:missing-gplv3-license # Licensed u plugins/modules/sap_company.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_control_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_hdbsql.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_hostctrl_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_snote.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_system_facts.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_task_list_execute.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 diff --git a/tests/sanity/ignore-2.15.txt b/tests/sanity/ignore-2.15.txt index 3883757..ea41c7a 100644 --- a/tests/sanity/ignore-2.15.txt +++ b/tests/sanity/ignore-2.15.txt @@ -2,6 +2,7 @@ plugins/modules/sap_pyrfc.py validate-modules:missing-gplv3-license # Licensed u plugins/modules/sap_company.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_control_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_hdbsql.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_hostctrl_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_snote.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_system_facts.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_task_list_execute.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 diff --git a/tests/sanity/ignore-2.16.txt b/tests/sanity/ignore-2.16.txt index 3883757..ea41c7a 100644 --- a/tests/sanity/ignore-2.16.txt +++ b/tests/sanity/ignore-2.16.txt @@ -2,6 +2,7 @@ plugins/modules/sap_pyrfc.py validate-modules:missing-gplv3-license # Licensed u plugins/modules/sap_company.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_control_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_hdbsql.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_hostctrl_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_snote.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_system_facts.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_task_list_execute.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 diff --git a/tests/sanity/ignore-2.17.txt b/tests/sanity/ignore-2.17.txt index 3883757..ea41c7a 100644 --- a/tests/sanity/ignore-2.17.txt +++ b/tests/sanity/ignore-2.17.txt @@ -2,6 +2,7 @@ plugins/modules/sap_pyrfc.py validate-modules:missing-gplv3-license # Licensed u plugins/modules/sap_company.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_control_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_hdbsql.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_hostctrl_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_snote.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_system_facts.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_task_list_execute.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 diff --git a/tests/sanity/ignore-2.18.txt b/tests/sanity/ignore-2.18.txt index 3883757..ea41c7a 100644 --- a/tests/sanity/ignore-2.18.txt +++ b/tests/sanity/ignore-2.18.txt @@ -2,6 +2,7 @@ plugins/modules/sap_pyrfc.py validate-modules:missing-gplv3-license # Licensed u plugins/modules/sap_company.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_control_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_hdbsql.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_hostctrl_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_snote.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_system_facts.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_task_list_execute.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 diff --git a/tests/sanity/ignore-2.19.txt b/tests/sanity/ignore-2.19.txt index 3883757..ea41c7a 100644 --- a/tests/sanity/ignore-2.19.txt +++ b/tests/sanity/ignore-2.19.txt @@ -2,6 +2,7 @@ plugins/modules/sap_pyrfc.py validate-modules:missing-gplv3-license # Licensed u plugins/modules/sap_company.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_control_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_hdbsql.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_hostctrl_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_snote.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_system_facts.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_task_list_execute.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 diff --git a/tests/sanity/ignore-2.20.txt b/tests/sanity/ignore-2.20.txt index 3883757..ea41c7a 100644 --- a/tests/sanity/ignore-2.20.txt +++ b/tests/sanity/ignore-2.20.txt @@ -2,6 +2,7 @@ plugins/modules/sap_pyrfc.py validate-modules:missing-gplv3-license # Licensed u plugins/modules/sap_company.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_control_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_hdbsql.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_hostctrl_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_snote.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_system_facts.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_task_list_execute.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 diff --git a/tests/sanity/ignore-2.21.txt b/tests/sanity/ignore-2.21.txt index 3883757..ea41c7a 100644 --- a/tests/sanity/ignore-2.21.txt +++ b/tests/sanity/ignore-2.21.txt @@ -2,6 +2,7 @@ plugins/modules/sap_pyrfc.py validate-modules:missing-gplv3-license # Licensed u plugins/modules/sap_company.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_control_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_hdbsql.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_hostctrl_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_snote.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_system_facts.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_task_list_execute.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 diff --git a/tests/sanity/ignore-2.22.txt b/tests/sanity/ignore-2.22.txt index 3883757..ea41c7a 100644 --- a/tests/sanity/ignore-2.22.txt +++ b/tests/sanity/ignore-2.22.txt @@ -2,6 +2,7 @@ plugins/modules/sap_pyrfc.py validate-modules:missing-gplv3-license # Licensed u plugins/modules/sap_company.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_control_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_hdbsql.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_hostctrl_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_snote.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_system_facts.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_task_list_execute.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 diff --git a/tests/sanity/ignore-2.23.txt b/tests/sanity/ignore-2.23.txt index 3883757..ea41c7a 100644 --- a/tests/sanity/ignore-2.23.txt +++ b/tests/sanity/ignore-2.23.txt @@ -2,6 +2,7 @@ plugins/modules/sap_pyrfc.py validate-modules:missing-gplv3-license # Licensed u plugins/modules/sap_company.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_control_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_hdbsql.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_hostctrl_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_snote.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_system_facts.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_task_list_execute.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 diff --git a/tests/sanity/ignore-2.24.txt b/tests/sanity/ignore-2.24.txt index 3883757..ea41c7a 100644 --- a/tests/sanity/ignore-2.24.txt +++ b/tests/sanity/ignore-2.24.txt @@ -2,6 +2,7 @@ plugins/modules/sap_pyrfc.py validate-modules:missing-gplv3-license # Licensed u plugins/modules/sap_company.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_control_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_hdbsql.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_hostctrl_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_snote.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_system_facts.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_task_list_execute.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 diff --git a/tests/sanity/ignore-2.25.txt b/tests/sanity/ignore-2.25.txt index 3883757..ea41c7a 100644 --- a/tests/sanity/ignore-2.25.txt +++ b/tests/sanity/ignore-2.25.txt @@ -2,6 +2,7 @@ plugins/modules/sap_pyrfc.py validate-modules:missing-gplv3-license # Licensed u plugins/modules/sap_company.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_control_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_hdbsql.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_hostctrl_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_snote.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_system_facts.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_task_list_execute.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 diff --git a/tests/unit/plugins/modules/test_sap_control_exec.py b/tests/unit/plugins/modules/test_sap_control_exec.py index dbf9f9f..4fcd16a 100644 --- a/tests/unit/plugins/modules/test_sap_control_exec.py +++ b/tests/unit/plugins/modules/test_sap_control_exec.py @@ -19,8 +19,12 @@ class TestSapcontrolModule(ModuleTestCase): def setUp(self): super(TestSapcontrolModule, self).setUp() self.module = sap_control_exec + # Patch HAS_SUDS_LIBRARY for all tests + self.patcher_suds = patch.object(self.module, 'HAS_SUDS_LIBRARY', True) + self.patcher_suds.start() def tearDown(self): + self.patcher_suds.stop() super(TestSapcontrolModule, self).tearDown() def define_rfc_connect(self, mocker): @@ -59,7 +63,12 @@ def test_error_connection(self): self.module.Client.side_effect = Mock(side_effect=Exception('Test')) with set_module_args(args): self.module.main() - self.assertEqual(result.exception.args[0]['msg'], 'Something went wrong connecting to the SAPCONTROL SOAP API.') + error_msg = result.exception.args[0]['msg'] + expected_messages = [ + 'Something went wrong connecting to the SOAP API.', + 'Something went wrong connecting to the Unix socket.' + ] + self.assertIn(error_msg, expected_messages) def test_error_port_sysnr(self): """tests fail multi provide parameters""" @@ -97,11 +106,13 @@ def test_success_sysnr(self): "sysnr": "01", "function": "GetProcessList" } - with patch.object(self.module, 'recursive_dict') as ret: - ret.return_value = {'item': [{'name': 'hdbdaemon', 'value': '1'}]} - with self.assertRaises(AnsibleExitJson) as result: - with set_module_args(args): - self.module.main() + with patch.object(self.module, 'connection') as mock_connection: + mock_connection.return_value = {'item': [{'name': 'hdbdaemon', 'value': '1'}]} + with patch.object(self.module, 'recursive_dict') as ret: + ret.return_value = {'item': [{'name': 'hdbdaemon', 'value': '1'}]} + with self.assertRaises(AnsibleExitJson) as result: + with set_module_args(args): + self.module.main() self.assertEqual(result.exception.args[0]['out'], [{'item': [{'name': 'hdbdaemon', 'value': '1'}]}]) def test_success_port(self): @@ -109,14 +120,16 @@ def test_success_port(self): args = { "hostname": "192.168.8.15", - "port": "50113", + "port": 50113, "function": "GetProcessList" } - with patch.object(self.module, 'recursive_dict') as ret: - ret.return_value = {'item': [{'name': 'hdbdaemon', 'value': '1'}]} - with self.assertRaises(AnsibleExitJson) as result: - with set_module_args(args): - self.module.main() + with patch.object(self.module, 'connection') as mock_connection: + mock_connection.return_value = {'item': [{'name': 'hdbdaemon', 'value': '1'}]} + with patch.object(self.module, 'recursive_dict') as ret: + ret.return_value = {'item': [{'name': 'hdbdaemon', 'value': '1'}]} + with self.assertRaises(AnsibleExitJson) as result: + with set_module_args(args): + self.module.main() self.assertEqual(result.exception.args[0]['out'], [{'item': [{'name': 'hdbdaemon', 'value': '1'}]}]) def test_success_string(self): diff --git a/tests/unit/plugins/modules/test_sap_hostctrl_exec.py b/tests/unit/plugins/modules/test_sap_hostctrl_exec.py new file mode 100644 index 0000000..03a9803 --- /dev/null +++ b/tests/unit/plugins/modules/test_sap_hostctrl_exec.py @@ -0,0 +1,180 @@ +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import sys +from unittest.mock import patch, MagicMock, Mock +from ansible_collections.community.sap_libs.tests.unit.plugins.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args + +sys.modules['suds.client'] = MagicMock() +sys.modules['suds.sudsobject'] = MagicMock() +sys.modules['suds'] = MagicMock() + +from ansible_collections.community.sap_libs.plugins.modules import sap_hostctrl_exec + + +class TestSapcontrolModule(ModuleTestCase): + + def setUp(self): + super(TestSapcontrolModule, self).setUp() + self.module = sap_hostctrl_exec + # Patch HAS_SUDS_LIBRARY for all tests + self.patcher_suds = patch.object(self.module, 'HAS_SUDS_LIBRARY', True) + self.patcher_suds.start() + + def tearDown(self): + self.patcher_suds.stop() + super(TestSapcontrolModule, self).tearDown() + + def define_rfc_connect(self, mocker): + return mocker.patch(self.module.call_rfc_method) + + def test_without_required_parameters(self): + """Failure must occurs when all parameters are missing""" + with self.assertRaises(AnsibleFailJson): + with set_module_args({}): + self.module.main() + + def test_error_module_not_found(self): + """tests fail module error""" + + args = { + "hostname": "192.168.8.15", + "function": "ListInstances" + } + with self.assertRaises(AnsibleFailJson) as result: + self.module.HAS_SUDS_LIBRARY = False + self.module.SUDS_LIBRARY_IMPORT_ERROR = 'Module not found' + with set_module_args(args): + self.module.main() + self.assertEqual(result.exception.args[0]['exception'], 'Module not found') + + def test_error_connection(self): + """tests fail module exception""" + + args = { + "hostname": "192.168.8.15", + "function": "ListInstances" + } + with self.assertRaises(AnsibleFailJson) as result: + self.module.Client.side_effect = Mock(side_effect=Exception('Test')) + with set_module_args(args): + self.module.main() + error_msg = result.exception.args[0]['msg'] + expected_messages = [ + 'Something went wrong connecting to the SOAP API.', + 'Something went wrong connecting to the Unix socket.' + ] + self.assertIn(error_msg, expected_messages) + + def test_error_missing_force(self): + """tests fail missing force""" + + args = { + "hostname": "192.168.8.15", + "function": "StopInstance" + } + + with self.assertRaises(AnsibleFailJson) as result: + with set_module_args(args): + self.module.main() + self.assertEqual(result.exception.args[0]['msg'], 'Stop function requires force: True') + + def test_success_list_instances(self): + """test success with ListInstances""" + + args = { + "hostname": "192.168.8.15", + "function": "ListInstances" + } + ret_dict = {'item': [{'mHostname': 'test-vm-001', + 'mSapVersionInfo': '793, patch 200, commit ec1833e294d84a70c04c6a1b01fd1a493f5c72fb', + 'mSid': 'TST', + 'mSystemNumber': '00'}]} + with patch.object(self.module, 'connection') as mock_connection: + with patch.object(self.module, 'recursive_dict') as ret: + ret.return_value = ret_dict + mock_connection.return_value = ret_dict + with self.assertRaises(AnsibleExitJson) as result: + with set_module_args(args): + self.module.main() + self.assertEqual(result.exception.args[0]['out'], [ret_dict]) + + def test_success_port(self): + """test success with port""" + + args = { + "hostname": "192.168.8.15", + "port": 1128, + "function": "ListInstances" + } + ret_dict = {'item': [{'mHostname': 'test-vm-001', + 'mSapVersionInfo': '793, patch 200, commit ec1833e294d84a70c04c6a1b01fd1a493f5c72fb', + 'mSid': 'TST', + 'mSystemNumber': '00'}]} + with patch.object(self.module, 'connection') as mock_connection: + with patch.object(self.module, 'recursive_dict') as ret: + ret.return_value = ret_dict + mock_connection.return_value = ret_dict + with self.assertRaises(AnsibleExitJson) as result: + with set_module_args(args): + self.module.main() + self.assertEqual(result.exception.args[0]['out'], [ret_dict]) + + def test_success_local(self): + """test success with local port""" + + args = { + "function": "ListInstances" + } + ret_dict = {'item': [{'mHostname': 'test-vm-001', + 'mSapVersionInfo': '793, patch 200, commit ec1833e294d84a70c04c6a1b01fd1a493f5c72fb', + 'mSid': 'TST', + 'mSystemNumber': '00'}]} + with patch.object(self.module, 'connection') as mock_connection: + with patch.object(self.module, 'recursive_dict') as ret: + ret.return_value = ret_dict + mock_connection.return_value = ret_dict + with self.assertRaises(AnsibleExitJson) as result: + with set_module_args(args): + self.module.main() + self.assertEqual(result.exception.args[0]['out'], [ret_dict]) + + def test_success_parameters(self): + """test success with parameters""" + + args = { + "hostname": "192.168.8.15", + "function": "StartInstance", + "parameters": { + "aInstance": {"mSid": "TST", "mSystemNumber": '00'}, + "aOptions": {"mTimeout": -1, "mSoftTimeout": 0, "mOptions": ["O-INSTANCE"]} + } + } + ret_dict = {"mOperationID": "42010A3F050C1FD0B3D4D6F5E5D41924", + "mOperationResults": {"item": [ + {"mMessageKey": "LogMsg/TimeStamp", "mMessageValue": "null"}, + {"mMessageKey": "LogMsg/Severity", "mMessageValue": "Info"}, + {"mMessageKey": "LogMsg/Source", "mMessageValue": "saphostcontrol"}, + {"mMessageKey": "LogMsg/Text", "mMessageValue": "exitcode=0"}, + {"mMessageKey": "LogMsg/TimeStamp", "mMessageValue": "null"}, + {"mMessageKey": "LogMsg/Severity", "mMessageValue": "Info"}, + {"mMessageKey": "LogMsg/Source", "mMessageValue": "saphostcontrol"}, + {"mMessageKey": "LogMsg/Text", "mMessageValue": "'sapcontrol -function Start' successfully executed"}, + {"mMessageKey": "LogMsg/TimeStamp", "mMessageValue": "null"}, + {"mMessageKey": "LogMsg/Severity", "mMessageValue": "Info"}, + {"mMessageKey": "LogMsg/Source", "mMessageValue": "saphostcontrol"}, + {"mMessageKey": "LogMsg/Text", "mMessageValue": "exitcode=0"}, + {"mMessageKey": "LogMsg/TimeStamp", "mMessageValue": "null"}, + {"mMessageKey": "LogMsg/Severity", "mMessageValue": "Info"}, + {"mMessageKey": "LogMsg/Source", "mMessageValue": "saphostcontrol"}, + {"mMessageKey": "LogMsg/Text", "mMessageValue": "'sapcontrol -function StartWait' successfully executed"}]}} + with patch.object(self.module, 'connection') as mock_connection: + with patch.object(self.module, 'recursive_dict') as ret: + ret.return_value = ret_dict + mock_connection.return_value = ret_dict + with self.assertRaises(AnsibleExitJson) as result: + with set_module_args(args): + self.module.main() + self.assertEqual(result.exception.args[0]['out'], [ret_dict])