Skip to content

Commit a9a08f2

Browse files
authored
Merge pull request #78 from oxya-dev/rework_module_utils
sap_control_exec, sap_hostctrl_exec: Refactor to use shared utilities
2 parents abaa8d9 + 75af038 commit a9a08f2

5 files changed

Lines changed: 202 additions & 257 deletions

File tree

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
4+
# Copyright: (c) 2026, Sean Freeman ,
5+
# Rainer Leber <rainerleber@gmail.com> <rainer.leber@sva.de>
6+
# Melvin Malagowski <mmalagowski@oxya.com>
7+
# Licensed under the Apache License, Version 2.0 (the "License");
8+
# you may not use this file except in compliance with the License.
9+
# You may obtain a copy of the License at
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
from __future__ import (absolute_import, division, print_function)
18+
__metaclass__ = type
19+
20+
import traceback
21+
import socket
22+
import os
23+
24+
try:
25+
from urllib.request import HTTPHandler
26+
except ImportError:
27+
from ansible.module_utils.urls import (
28+
UnixHTTPHandler as HTTPHandler,
29+
)
30+
31+
try:
32+
from http.client import HTTPConnection
33+
except ImportError:
34+
from httplib import HTTPConnection
35+
36+
try:
37+
from suds.client import Client
38+
from suds.sudsobject import asdict
39+
from suds.transport.http import HttpAuthenticated, HttpTransport
40+
HAS_SUDS_LIBRARY = True
41+
SUDS_LIBRARY_IMPORT_ERROR = None
42+
43+
class LocalSocketHttpAuthenticated(HttpAuthenticated):
44+
"""Authenticated HTTP transport using Unix domain sockets."""
45+
def __init__(self, socketpath, **kwargs):
46+
HttpAuthenticated.__init__(self, **kwargs)
47+
self._socketpath = socketpath
48+
49+
def u2handlers(self):
50+
handlers = HttpTransport.u2handlers(self)
51+
handlers.append(LocalSocketHandler(socketpath=self._socketpath))
52+
return handlers
53+
54+
except ImportError:
55+
HAS_SUDS_LIBRARY = False
56+
SUDS_LIBRARY_IMPORT_ERROR = traceback.format_exc()
57+
58+
# Dummy class when suds is not available (keeps imports stable in tests)
59+
class LocalSocketHttpAuthenticated(object):
60+
def __init__(self, socketpath, **kwargs):
61+
pass
62+
63+
def u2handlers(self):
64+
return []
65+
66+
67+
class LocalSocketHttpConnection(HTTPConnection):
68+
"""HTTP connection class that uses Unix domain sockets."""
69+
def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
70+
source_address=None, socketpath=None):
71+
super(LocalSocketHttpConnection, self).__init__(host, port, timeout, source_address)
72+
self.socketpath = socketpath
73+
74+
def connect(self):
75+
"""Connect to Unix domain socket."""
76+
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
77+
self.sock.connect(self.socketpath)
78+
79+
80+
class LocalSocketHandler(HTTPHandler):
81+
"""HTTP handler for Unix domain sockets."""
82+
def __init__(self, debuglevel=0, socketpath=None):
83+
self._debuglevel = debuglevel
84+
self._socketpath = socketpath
85+
86+
def http_open(self, req):
87+
return self.do_open(LocalSocketHttpConnection, req, socketpath=self._socketpath)
88+
89+
90+
def recursive_dict(suds_object):
91+
"""Convert a suds object to a plain Python dict, recursively.
92+
93+
Example output: ``{'item': [{'name': 'hdbdaemon', 'value': '1'}]}``
94+
"""
95+
out = {}
96+
if isinstance(suds_object, str):
97+
return suds_object
98+
for k, v in asdict(suds_object).items():
99+
if hasattr(v, '__keylist__'):
100+
out[k] = recursive_dict(v)
101+
elif isinstance(v, list):
102+
out[k] = []
103+
for item in v:
104+
if hasattr(item, '__keylist__'):
105+
out[k].append(recursive_dict(item))
106+
else:
107+
out[k].append(item)
108+
else:
109+
out[k] = v
110+
return out
111+
112+
113+
def connection(service_name, hostname, port, username, password, sysnr=None, use_local=False):
114+
"""
115+
Return a SOAP client for the given service (sapcontrol or saphostctrl).
116+
"""
117+
if use_local:
118+
# Use Unix domain socket for local connection
119+
if sysnr is not None:
120+
# For sapcontrol, the socket name includes the system number
121+
unix_socket = "/tmp/.sapstream5{0}13".format(str(sysnr).zfill(2))
122+
else:
123+
# For saphostctrl, the socket name is fixed
124+
unix_socket = "/tmp/.sapstream1128"
125+
126+
# Check if socket exists
127+
if not os.path.exists(unix_socket):
128+
raise Exception("SAP control Unix socket not found: {0}".format(unix_socket))
129+
130+
url = "http://localhost/{0}?wsdl".format(service_name)
131+
132+
try:
133+
localsocket = LocalSocketHttpAuthenticated(unix_socket)
134+
client = Client(url, transport=localsocket)
135+
except Exception as e:
136+
raise Exception("Failed to connect via Unix socket: {0}".format(str(e)))
137+
else:
138+
# Use HTTP connection (original behavior)
139+
url = 'http://{0}:{1}/{2}?wsdl'.format(hostname, port, service_name)
140+
client = Client(url, username=username, password=password)
141+
142+
return client
143+
144+
145+
def call_sap_control(hostname, port, username, password, function, parameters, sysnr=None, use_local=False):
146+
con = connection("sapcontrol", hostname, port, username, password, sysnr=sysnr, use_local=use_local)
147+
return call_function(con, function, parameters)
148+
149+
150+
def call_sap_hostctrl(hostname, port, username, password, function, parameters, use_local=False):
151+
con = connection("SAPHostControl/", hostname, port, username, password, sysnr=None, use_local=use_local)
152+
return call_function(con, function, parameters)
153+
154+
155+
def call_function(client, function, parameters=None):
156+
_function = getattr(client.service, function)
157+
if parameters is not None:
158+
if isinstance(parameters, dict):
159+
result = _function(**parameters)
160+
else:
161+
result = _function(parameters)
162+
else:
163+
result = _function()
164+
return result

plugins/modules/sap_control_exec.py

Lines changed: 18 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -228,74 +228,13 @@
228228
'''
229229

230230
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
231-
import traceback
232-
import socket
233-
import os
234-
235-
try:
236-
from urllib.request import HTTPHandler
237-
except ImportError:
238-
from ansible.module_utils.urls import (
239-
UnixHTTPHandler as HTTPHandler,
240-
)
241-
242-
try:
243-
from http.client import HTTPConnection
244-
except ImportError:
245-
from httplib import HTTPConnection
246-
247-
try:
248-
from suds.client import Client
249-
from suds.sudsobject import asdict
250-
from suds.transport.http import HttpAuthenticated, HttpTransport
251-
HAS_SUDS_LIBRARY = True
252-
SUDS_LIBRARY_IMPORT_ERROR = None
253-
254-
class LocalSocketHttpAuthenticated(HttpAuthenticated):
255-
"""Authenticated HTTP transport using Unix domain sockets."""
256-
def __init__(self, socketpath, **kwargs):
257-
HttpAuthenticated.__init__(self, **kwargs)
258-
self._socketpath = socketpath
259-
260-
def u2handlers(self):
261-
handlers = HttpTransport.u2handlers(self)
262-
handlers.append(LocalSocketHandler(socketpath=self._socketpath))
263-
return handlers
264-
265-
except ImportError:
266-
HAS_SUDS_LIBRARY = False
267-
SUDS_LIBRARY_IMPORT_ERROR = traceback.format_exc()
268-
269-
# Define dummy class when suds is not available
270-
class LocalSocketHttpAuthenticated(object):
271-
def __init__(self, socketpath, **kwargs):
272-
pass
273-
274-
def u2handlers(self):
275-
return []
276-
277-
278-
class LocalSocketHttpConnection(HTTPConnection):
279-
"""HTTP connection class that uses Unix domain sockets."""
280-
def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
281-
source_address=None, socketpath=None):
282-
super(LocalSocketHttpConnection, self).__init__(host, port, timeout, source_address)
283-
self.socketpath = socketpath
284-
285-
def connect(self):
286-
"""Connect to Unix domain socket."""
287-
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
288-
self.sock.connect(self.socketpath)
289-
290231

291-
class LocalSocketHandler(HTTPHandler):
292-
"""HTTP handler for Unix domain sockets."""
293-
def __init__(self, debuglevel=0, socketpath=None):
294-
self._debuglevel = debuglevel
295-
self._socketpath = socketpath
296-
297-
def http_open(self, req):
298-
return self.do_open(LocalSocketHttpConnection, req, socketpath=self._socketpath)
232+
from ..module_utils.sapstartsrv_client import (
233+
HAS_SUDS_LIBRARY,
234+
SUDS_LIBRARY_IMPORT_ERROR,
235+
call_sap_control as connection,
236+
recursive_dict,
237+
)
299238

300239

301240
def choices():
@@ -314,60 +253,6 @@ def choices():
314253
return retlist
315254

316255

317-
# converts recursively the suds object to a dictionary e.g. {'item': [{'name': hdbdaemon, 'value': '1'}]}
318-
def recursive_dict(suds_object):
319-
out = {}
320-
if isinstance(suds_object, str):
321-
return suds_object
322-
for k, v in asdict(suds_object).items():
323-
if hasattr(v, '__keylist__'):
324-
out[k] = recursive_dict(v)
325-
elif isinstance(v, list):
326-
out[k] = []
327-
for item in v:
328-
if hasattr(item, '__keylist__'):
329-
out[k].append(recursive_dict(item))
330-
else:
331-
out[k].append(item)
332-
else:
333-
out[k] = v
334-
return out
335-
336-
337-
def connection(hostname, port, username, password, function, parameter, sysnr=None, use_local=False):
338-
if use_local and sysnr is not None:
339-
# Use Unix domain socket for local connection
340-
unix_socket = "/tmp/.sapstream5{0}13".format(str(sysnr).zfill(2))
341-
342-
# Check if socket exists
343-
if not os.path.exists(unix_socket):
344-
raise Exception("SAP control Unix socket not found: {0}".format(unix_socket))
345-
346-
url = "http://localhost/sapcontrol?wsdl"
347-
348-
try:
349-
localsocket = LocalSocketHttpAuthenticated(unix_socket)
350-
client = Client(url, transport=localsocket)
351-
except Exception as e:
352-
raise Exception("Failed to connect via Unix socket: {0}".format(str(e)))
353-
else:
354-
# Use HTTP connection (original behavior)
355-
url = 'http://{0}:{1}/sapcontrol?wsdl'.format(hostname, port)
356-
client = Client(url, username=username, password=password)
357-
358-
_function = getattr(client.service, function)
359-
if parameter is not None:
360-
result = _function(parameter)
361-
elif function == "StartSystem":
362-
result = _function(waittimeout=0)
363-
elif function == "StopSystem" or function == "RestartSystem":
364-
result = _function(waittimeout=0, softtimeout=0)
365-
else:
366-
result = _function()
367-
368-
return result
369-
370-
371256
def main():
372257
module = AnsibleModule(
373258
argument_spec=dict(
@@ -413,6 +298,11 @@ def main():
413298
if force is False:
414299
module.fail_json(msg="Stop function requires force: True")
415300

301+
if function == "StartSystem":
302+
parameter = dict(waittimeout=0)
303+
elif function == "StopSystem" or function == "RestartSystem":
304+
parameter = dict(waittimeout=0, softtimeout=0)
305+
416306
# Determine if we should use local Unix socket connection
417307
# Use local if hostname is localhost and no username/password provided
418308
use_local = (hostname == "localhost" and
@@ -424,18 +314,18 @@ def main():
424314
try:
425315
if use_local:
426316
# Try local connection first
427-
conn = connection(hostname, None, username, password, function, parameter, sysnr, use_local=True)
317+
result_conn = connection(hostname, None, username, password, function, parameter, sysnr=sysnr, use_local=True)
428318
else:
429319
# Try HTTP ports
430320
try:
431-
conn = connection(hostname, "5{0}14".format((sysnr).zfill(2)), username, password, function, parameter, sysnr)
321+
result_conn = connection(hostname, "5{0}14".format((sysnr).zfill(2)), username, password, function, parameter, sysnr)
432322
except Exception:
433-
conn = connection(hostname, "5{0}13".format((sysnr).zfill(2)), username, password, function, parameter, sysnr)
323+
result_conn = connection(hostname, "5{0}13".format((sysnr).zfill(2)), username, password, function, parameter, sysnr)
434324
except Exception as err:
435325
result['error'] = str(err)
436326
else:
437327
try:
438-
conn = connection(hostname, port, username, password, function, parameter, sysnr, use_local=False)
328+
result_conn = connection(hostname, port, username, password, function, parameter, sysnr, use_local=False)
439329
except Exception as err:
440330
result['error'] = str(err)
441331

@@ -444,10 +334,10 @@ def main():
444334
result['msg'] = 'Something went wrong connecting to the {0}.'.format(connection_type)
445335
module.fail_json(**result)
446336

447-
if conn is not None:
448-
returned_data = recursive_dict(conn)
337+
if result_conn is not None:
338+
returned_data = recursive_dict(result_conn)
449339
else:
450-
returned_data = conn
340+
returned_data = result_conn
451341

452342
result['changed'] = True
453343
result['msg'] = "Succesful execution of: " + function

0 commit comments

Comments
 (0)