Skip to content

Commit 9bf7d41

Browse files
authored
Merge pull request #73 from marcelmamula/facts_perm
sap_system_facts: Add SID and permission check to facts module
2 parents e7c4d80 + 77c6bf4 commit 9bf7d41

2 files changed

Lines changed: 140 additions & 34 deletions

File tree

plugins/modules/sap_system_facts.py

Lines changed: 119 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
3030
notes:
3131
- Supports C(check_mode).
32+
- The user executing the module must have execute permissions for C(/usr/sap/hostctrl/exe/sapcontrol).
33+
- Only directories matching SAP SID and Instance naming conventions are scanned.
3234
'''
3335

3436
EXAMPLES = r'''
@@ -84,57 +86,131 @@
8486

8587
def get_all_hana_sid():
8688
hana_sid = list()
87-
if os.path.isdir("/hana/shared"):
88-
# /hana/shared directory exists
89-
for sid in os.listdir('/hana/shared'):
90-
if os.path.isdir("/usr/sap/" + sid):
91-
hana_sid = hana_sid + [sid]
92-
if hana_sid:
93-
return hana_sid
89+
90+
# Expected SID pattern: 3 character in specific pattern (e.g., ABC, D01, etc.)
91+
sid_pattern = re.compile(r'^[A-Z][A-Z0-9][A-Z0-9]$')
92+
93+
shared_path = "/hana/shared"
94+
95+
if is_accessible_dir(shared_path):
96+
try:
97+
for sid in os.listdir(shared_path):
98+
if not sid_pattern.match(sid):
99+
continue
100+
101+
target_path = os.path.join("/usr/sap", sid)
102+
103+
try:
104+
if is_accessible_dir(target_path):
105+
hana_sid.append(sid)
106+
except OSError:
107+
# Individual SID folder is problematic, move to next
108+
continue
109+
110+
except OSError:
111+
# Entire shared_path is inaccessible
112+
pass
113+
114+
return hana_sid
94115

95116

96117
def get_all_nw_sid():
97118
nw_sid = list()
98-
if os.path.isdir("/sapmnt"):
99-
# /sapmnt directory exists
100-
for sid in os.listdir('/sapmnt'):
101-
if os.path.isdir("/usr/sap/" + sid):
102-
nw_sid = nw_sid + [sid]
103-
else:
104-
# Check to see if /sapmnt/SID/sap_bobj exists
105-
if os.path.isdir("/sapmnt/" + sid + "/sap_bobj"):
106-
# is a bobj system
107-
nw_sid = nw_sid + [sid]
108-
if nw_sid:
109-
return nw_sid
119+
120+
# Expected SID pattern with only 3 character in specific pattern (e.g., ABC, D01, etc.)
121+
sid_pattern = re.compile(r'^[A-Z][A-Z0-9][A-Z0-9]$')
122+
123+
sapmnt_path = "/sapmnt"
124+
125+
if is_accessible_dir(sapmnt_path):
126+
try:
127+
for sid in os.listdir(sapmnt_path):
128+
if not sid_pattern.match(sid):
129+
continue
130+
131+
target_path = os.path.join("/usr/sap", sid)
132+
133+
try:
134+
if is_accessible_dir(target_path):
135+
nw_sid.append(sid)
136+
else:
137+
# Check to see if /sapmnt/SID/sap_bobj exists and is accessible
138+
bobj_path = os.path.join(sapmnt_path, sid, "sap_bobj")
139+
if is_accessible_dir(bobj_path):
140+
nw_sid.append(sid)
141+
except OSError:
142+
# Individual SID folder is problematic, move to next
143+
continue
144+
145+
except OSError:
146+
# Entire shared_path is inaccessible
147+
pass
148+
149+
return nw_sid
110150

111151

112152
def get_hana_nr(sids, module):
113153
hana_list = list()
154+
155+
# Expected Instance pattern: HDB followed by exactly 2 digits (e.g., HDB00, HDB01, etc.)
156+
instance_pattern = re.compile(r'^HDB(\d{2})$')
157+
158+
sapcontrol_path = module.get_bin_path('/usr/sap/hostctrl/exe/sapcontrol', required=True)
159+
114160
for sid in sids:
115-
for instance in os.listdir('/usr/sap/' + sid):
116-
if 'HDB' in instance:
117-
instance_nr = instance[-2:]
118-
# check if instance number exists
119-
command = [module.get_bin_path('/usr/sap/hostctrl/exe/sapcontrol', required=True)]
161+
path = os.path.join('/usr/sap', sid)
162+
163+
if not is_accessible_dir(path):
164+
continue
165+
166+
for instance in os.listdir(path):
167+
match = instance_pattern.match(instance)
168+
if match:
169+
# 'match.group(1)' is the 2 digits captured by (\d{2})
170+
instance_nr = match.group(1)
171+
172+
command = [sapcontrol_path]
120173
command.extend(['-nr', instance_nr, '-function', 'GetProcessList'])
174+
121175
check_instance = module.run_command(command, check_rc=False)
122-
# sapcontrol returns c(0 - 5) exit codes only c(1) is unavailable
176+
177+
# sapcontrol returns (0-5) exit codes; (1) usually means unavailable
123178
if check_instance[0] != 1:
124-
hana_list.append({'NR': instance_nr, 'SID': sid, 'TYPE': 'HDB', 'InstanceType': 'HANA'})
179+
hana_list.append({
180+
'NR': instance_nr,
181+
'SID': sid,
182+
'TYPE': 'HDB',
183+
'InstanceType': 'HANA'
184+
})
185+
125186
return hana_list
126187

127188

128189
def get_nw_nr(sids, module):
129190
nw_list = list()
191+
192+
# Expected Instance pattern: letters followed by exactly 2 digits (e.g., ASCS00, D01)
193+
# Excludes 'SYS', 'exe', 'hdbclient', etc.
194+
instance_pattern = re.compile(r'^[a-zA-Z]+(\d{2})$')
195+
196+
sapcontrol_path = module.get_bin_path('/usr/sap/hostctrl/exe/sapcontrol', required=True)
130197
type = ""
198+
131199
for sid in sids:
132-
for instance in os.listdir('/usr/sap/' + sid):
133-
instance_nr = instance[-2:]
134-
command = [module.get_bin_path('/usr/sap/hostctrl/exe/sapcontrol', required=True)]
135-
# check if returned instance_nr is a number because sapcontrol returns all if a random string is provided
136-
if instance_nr.isdigit():
200+
path = os.path.join('/usr/sap', sid)
201+
202+
if not is_accessible_dir(path):
203+
continue
204+
205+
for instance in os.listdir(path):
206+
match = instance_pattern.match(instance)
207+
if match:
208+
# 'match.group(1)' is the 2 digits captured by (\d{2})
209+
instance_nr = match.group(1)
210+
211+
command = [sapcontrol_path]
137212
command.extend(['-nr', instance_nr, '-function', 'GetInstanceProperties'])
213+
138214
check_instance = module.run_command(command, check_rc=False)
139215
if check_instance[0] != 1:
140216
for line in check_instance[1].splitlines():
@@ -144,6 +220,7 @@ def get_nw_nr(sids, module):
144220
# split instance number
145221
type = type_raw[:-2]
146222
nw_list.append({'NR': instance_nr, 'SID': sid, 'TYPE': get_instance_type(type), 'InstanceType': 'NW'})
223+
147224
return nw_list
148225

149226

@@ -172,6 +249,10 @@ def get_instance_type(raw_type):
172249
return type
173250

174251

252+
def is_accessible_dir(path):
253+
return os.path.isdir(path) and os.access(path, os.R_OK)
254+
255+
175256
def run_module():
176257
module_args = dict()
177258
system_result = list()
@@ -186,6 +267,11 @@ def run_module():
186267
supports_check_mode=True,
187268
)
188269

270+
# Fail if execution user does not have permission for sapcontrol
271+
sapcontrol_path = module.get_bin_path('/usr/sap/hostctrl/exe/sapcontrol', required=True)
272+
if not os.access(sapcontrol_path, os.X_OK):
273+
module.fail_json(msg="Permission denied: Ansible user cannot execute {0}".format(sapcontrol_path))
274+
189275
hana_sid = get_all_hana_sid()
190276
if hana_sid:
191277
system_result = system_result + get_hana_nr(hana_sid, module)
@@ -196,8 +282,10 @@ def run_module():
196282

197283
if system_result:
198284
result['ansible_facts'] = {'sap': system_result}
285+
result['msg'] = "SAP System facts were collected."
199286
else:
200287
result['ansible_facts']
288+
result['msg'] = "No running SAP instances found or Ansible user cannot access them."
201289

202290
if module.check_mode:
203291
module.exit_json(**result)

tests/unit/plugins/modules/test_sap_system_facts.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,37 @@ def setUp(self):
2525
"""Setup."""
2626
super(Testsap_system_facts, self).setUp()
2727
self.module = sap_system_facts
28+
29+
# Mock get_bin_path
2830
self.mock_get_bin_path = patch.object(basic.AnsibleModule, 'get_bin_path', get_bin_path)
2931
self.mock_get_bin_path.start()
3032
self.addCleanup(self.mock_get_bin_path.stop)
3133

34+
# Mock os.access to always return True
35+
self.mock_os_access = patch('ansible_collections.community.sap_libs.plugins.modules.sap_system_facts.os.access')
36+
self.mock_access = self.mock_os_access.start()
37+
self.mock_access.return_value = True
38+
self.addCleanup(self.mock_os_access.stop)
39+
40+
# NEW: Mock os.path.isdir to return True for base paths
41+
self.mock_os_isdir = patch('ansible_collections.community.sap_libs.plugins.modules.sap_system_facts.os.path.isdir')
42+
self.mock_isdir = self.mock_os_isdir.start()
43+
self.mock_isdir.return_value = True
44+
self.addCleanup(self.mock_os_isdir.stop)
45+
3246
def tearDown(self):
3347
"""Teardown."""
3448
super(Testsap_system_facts, self).tearDown()
3549

3650
def test_no_systems_available(self):
3751
"""No SAP Systems"""
38-
with self.assertRaises(AnsibleExitJson) as result:
39-
with set_module_args({}):
40-
self.module.main()
52+
with patch.object(self.module, 'get_all_hana_sid', return_value=[]):
53+
with patch.object(self.module, 'get_all_nw_sid', return_value=[]):
54+
with self.assertRaises(AnsibleExitJson) as result:
55+
with set_module_args({}):
56+
self.module.main()
57+
58+
# Check that ansible_facts is empty or doesn't have 'sap'
4159
self.assertEqual(result.exception.args[0]['ansible_facts'], {})
4260

4361
def test_sap_system_facts_all(self):

0 commit comments

Comments
 (0)