Skip to content

Commit f1e0216

Browse files
authored
Merge pull request #55 from marcelmamula/workflows2
collection: Add ansible-test sanity workflow and fix sanity errors
2 parents e07f371 + 37c6e50 commit f1e0216

33 files changed

+618
-123
lines changed

.ansible-lint

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
---
2-
# Collection wide lint-file
3-
# DO NOT CHANGE
2+
## Collection wide ansible-lint configuration file.
3+
# Changes for ansible-lint v25.7.0+
4+
# - Always executed from collection root using collection configuration.
5+
# - .ansible-lint-ignore can be used to ignore files, not folders.
6+
## Execution examples:
7+
# ansible-lint
8+
# ansible-lint roles/sap_swpm
9+
# ansible-lint roles/sap_install_media_detect -c roles/sap_install_media_detect/.ansible-lint
10+
411
exclude_paths:
512
- .ansible/
613
- .cache/
714
- .github/
8-
# - docs/
915
- changelogs/
1016
- playbooks/
1117
- tests/
18+
1219
enable_list:
1320
- yaml
21+
1422
skip_list:
1523
# We don't want to enforce new Ansible versions for Galaxy:
1624
- meta-runtime[unsupported-version]

.github/workflows/.ansible-lint

Lines changed: 0 additions & 10 deletions
This file was deleted.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
name: Ansible Lint - sap_software_download
3+
4+
on:
5+
push:
6+
branches:
7+
- main
8+
- dev
9+
paths:
10+
- 'roles/sap_software_download/**'
11+
pull_request:
12+
branches:
13+
- main
14+
- dev
15+
paths:
16+
- 'roles/sap_software_download/**'
17+
18+
workflow_dispatch:
19+
20+
jobs:
21+
ansible-lint:
22+
runs-on: ubuntu-latest
23+
24+
steps:
25+
- uses: actions/checkout@v5
26+
27+
# Use @v25 to automatically track the latest release from the year 2025.
28+
# ansible-lint uses Calendar Versioning (e.g., v25.9.0 -> YYYY.MM.PATCH).
29+
# Avoid using @main, which can introduce breaking changes unexpectedly.
30+
- uses: ansible/ansible-lint@v25
31+
with:
32+
# v25.7.0 no longer uses 'working_directory' and role path is set in 'args'.
33+
# Role specific .ansible-lint can be added with argument '-c'.
34+
args: roles/sap_software_download
35+
# Use the shared requirements file from the collection root for dependency context.
36+
requirements_file: ./requirements.yml

.github/workflows/ansible-lint.yml

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
1-
name: Ansible Lint
1+
---
2+
name: Ansible Lint - Collection
23

3-
on: [push, pull_request]
4+
on:
5+
schedule:
6+
# This is 03:05 UTC, which is 5:05 AM in Prague/CEST.
7+
- cron: '5 3 * * 1'
8+
9+
workflow_dispatch:
410

511
jobs:
612
ansible-lint:
7-
813
runs-on: ubuntu-latest
914

1015
steps:
11-
- uses: actions/checkout@v4
16+
- uses: actions/checkout@v5
1217

13-
- name: Ansible Lint Action
14-
uses: ansible/ansible-lint@v6
18+
# Use @v25 to automatically track the latest release from the year 2025.
19+
# ansible-lint uses Calendar Versioning (e.g., v25.9.0 -> YYYY.MM.PATCH).
20+
# Avoid using @main, which can introduce breaking changes unexpectedly.
21+
- uses: ansible/ansible-lint@v25
22+
with:
23+
# Use the shared requirements file from the collection root for dependency context.
24+
requirements_file: ./requirements.yml
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
---
2+
# Always check ansible-core support matrix before configuring units matrix.
3+
# https://docs.ansible.com/ansible/latest/reference_appendices/release_and_maintenance.html#ansible-core-support-matrix
4+
5+
name: Ansible Test - Sanity
6+
7+
on:
8+
schedule:
9+
# This is 01:05 UTC, which is 3:05 AM in Prague/CEST
10+
- cron: '5 3 * * 1'
11+
12+
pull_request:
13+
branches:
14+
- main
15+
- dev
16+
17+
workflow_dispatch:
18+
19+
jobs:
20+
sanity-supported:
21+
runs-on: ubuntu-latest
22+
name: Sanity (Supported Ⓐ${{ matrix.ansible }})
23+
strategy:
24+
fail-fast: false # Disabled so we can see all failed combinations.
25+
# Define a build matrix to test compatibility across multiple Ansible versions.
26+
# Each version listed below will spawn a separate job that runs in parallel.
27+
matrix:
28+
ansible:
29+
- 'stable-2.18' # Python 3.11 - 3.13
30+
- 'stable-2.19' # Python 3.11 - 3.13
31+
- 'devel' # Test against the upcoming development version.
32+
33+
steps:
34+
- uses: actions/checkout@v5
35+
36+
- name: ansible-test - sanity
37+
uses: ansible-community/ansible-test-gh-action@release/v1
38+
with:
39+
ansible-core-version: ${{ matrix.ansible }}
40+
testing-type: sanity
41+
42+
sanity-eol:
43+
runs-on: ubuntu-latest
44+
# This job only runs if the supported tests pass
45+
needs: sanity-supported
46+
name: Sanity (EOL Ⓐ${{ matrix.ansible }}+py${{ matrix.python }})
47+
continue-on-error: true # This entire job is allowed to fail
48+
strategy:
49+
fail-fast: false # Disabled so we can see all failed combinations.
50+
# Define a build matrix to test compatibility across multiple Ansible versions.
51+
# Each version listed below will spawn a separate job that runs in parallel.
52+
matrix:
53+
ansible:
54+
- 'stable-2.14' # Python 3.9 - 3.11
55+
- 'stable-2.15' # Python 3.9 - 3.11
56+
- 'stable-2.16' # Python 3.10 - 3.12
57+
- 'stable-2.17' # Python 3.10 - 3.12
58+
python:
59+
- '3.9'
60+
- '3.10'
61+
- '3.11'
62+
- '3.12'
63+
exclude:
64+
# Exclusions for incompatible Python versions.
65+
- ansible: 'stable-2.14'
66+
python: '3.12'
67+
68+
- ansible: 'stable-2.15'
69+
python: '3.12'
70+
71+
- ansible: 'stable-2.16'
72+
python: '3.9'
73+
74+
- ansible: 'stable-2.17'
75+
python: '3.9'
76+
steps:
77+
- uses: actions/checkout@v5
78+
79+
- name: ansible-test - sanity
80+
uses: ansible-community/ansible-test-gh-action@release/v1
81+
with:
82+
ansible-core-version: ${{ matrix.ansible }}
83+
target-python-version: ${{ matrix.python }}
84+
testing-type: sanity

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ When an SAP User ID (e.g. S-User) is enabled with and part of an SAP Universal I
120120
- the SAP User ID
121121
- the password for login with the SAP Universal ID
122122

123-
In addition, if a SAP Universal ID is used then the recommendation is to check and reset the SAP User ID Account Password in the [SAP Universal ID Account Manager](https://account.sap.com/manage/accounts), which will help to avoid any potential conflicts.
123+
In addition, if a SAP Universal ID is used then the recommendation is to check and reset the SAP User ID `Account Password` in the [SAP Universal ID Account Manager](https://account.sap.com/manage/accounts), which will help to avoid any potential conflicts.
124124

125125
For further information regarding connection errors, please see the FAQ section [Errors with prefix 'SAP SSO authentication failed - '](./docs/FAQ.md#errors-with-prefix-sap-sso-authentication-failed---).
126126

docs/FAQ.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ The error HTTP 401 refers to either:
2020
- Unauthorized, the SAP User ID being used belongs to an SAP Company Number (SCN) with one or more Installation Number/s which do not have license agreements for these files
2121
- Unauthorized, the SAP User ID being used does not have SAP Download authorizations
2222
- Unauthorized, the SAP User ID is part of an SAP Universal ID and must use the password of the SAP Universal ID
23-
- In addition, if a SAP Universal ID is used then the recommendation is to check and reset the SAP User ID Account Password in the [SAP Universal ID Account Manager](https://account.sap.com/manage/accounts), which will help to avoid any potential conflicts.
23+
- In addition, if a SAP Universal ID is used then the recommendation is to check and reset the SAP User ID `Account Password` in the [SAP Universal ID Account Manager](https://account.sap.com/manage/accounts), which will help to avoid any potential conflicts.
2424

2525
This is documented under [Execution - Credentials](https://github.com/sap-linuxlab/community.sap_launchpad#requirements-dependencies-and-testing).
2626

plugins/module_utils/auth.py

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,57 @@
1+
from __future__ import absolute_import, division, print_function
2+
3+
__metaclass__ = type
4+
15
import json
26
import re
7+
from functools import wraps
38

49
from urllib.parse import parse_qs, quote_plus, urljoin
5-
from bs4 import BeautifulSoup
6-
from requests.models import HTTPError
710

811
from . import constants as C
912
from . import exceptions
1013

14+
try:
15+
from bs4 import BeautifulSoup
16+
except ImportError:
17+
HAS_BS4 = False
18+
BeautifulSoup = None
19+
else:
20+
HAS_BS4 = True
21+
22+
try:
23+
from requests.models import HTTPError
24+
except ImportError:
25+
HAS_REQUESTS = False
26+
HTTPError = None
27+
else:
28+
HAS_REQUESTS = True
29+
1130
_GIGYA_SDK_BUILD_NUMBER = None
1231

1332

33+
def require_bs4(func):
34+
# A decorator to check for the 'beautifulsoup4' library before executing a function.
35+
@wraps(func)
36+
def wrapper(*args, **kwargs):
37+
if not HAS_BS4:
38+
raise ImportError("The 'beautifulsoup4' library is required but was not found.")
39+
return func(*args, **kwargs)
40+
return wrapper
41+
42+
43+
def require_requests(func):
44+
# A decorator to check for the 'requests' library before executing a function.
45+
@wraps(func)
46+
def wrapper(*args, **kwargs):
47+
if not HAS_REQUESTS:
48+
raise ImportError("The 'requests' library is required but was not found.")
49+
return func(*args, **kwargs)
50+
return wrapper
51+
52+
53+
@require_requests
54+
@require_bs4
1455
def login(client, username, password):
1556
# Main authentication function.
1657
#
@@ -57,18 +98,20 @@ def login(client, username, password):
5798
'samlContext': params['samlContext']
5899
}
59100
endpoint, meta = get_sso_endpoint_meta(client, idp_endpoint,
60-
params=context,
61-
allow_redirects=False)
101+
params=context,
102+
allow_redirects=False)
62103

63104
while (endpoint != C.URL_LAUNCHPAD + '/'):
64105
endpoint, meta = get_sso_endpoint_meta(client, endpoint,
65-
data=meta,
66-
headers=C.GIGYA_HEADERS,
67-
allow_redirects=False)
106+
data=meta,
107+
headers=C.GIGYA_HEADERS,
108+
allow_redirects=False)
68109

69110
client.post(endpoint, data=meta, headers=C.GIGYA_HEADERS)
70111

71112

113+
@require_requests
114+
@require_bs4
72115
def get_sso_endpoint_meta(client, url, **kwargs):
73116
# Scrapes an HTML page to find the next SSO form action URL and its input fields.
74117
method = 'POST' if kwargs.get('data') or kwargs.get('json') else 'GET'
@@ -100,6 +143,7 @@ def get_sso_endpoint_meta(client, url, **kwargs):
100143
return (endpoint, metadata)
101144

102145

146+
@require_requests
103147
def _get_gigya_login_params(client, url, data):
104148
# Follows a redirect and extracts parameters from the resulting URL's query string.
105149
gigya_idp_res = client.post(url, data=data)
@@ -109,9 +153,10 @@ def _get_gigya_login_params(client, url, data):
109153
return params
110154

111155

156+
@require_requests
112157
def _gigya_websdk_bootstrap(client, params):
113158
# Performs the initial bootstrap call to the Gigya WebSDK.
114-
page_url = f'{C.URL_ACCOUNT_SAML_PROXY}?apiKey=' + params['apiKey'],
159+
page_url = f'{C.URL_ACCOUNT_SAML_PROXY}?apiKey=' + params['apiKey']
115160
params.update({
116161
'pageURL': page_url,
117162
'sdk': 'js_latest',
@@ -120,10 +165,11 @@ def _gigya_websdk_bootstrap(client, params):
120165
})
121166

122167
client.get(C.URL_ACCOUNT_CDC_API + '/accounts.webSdkBootstrap',
123-
params=params,
124-
headers=C.GIGYA_HEADERS)
168+
params=params,
169+
headers=C.GIGYA_HEADERS)
125170

126171

172+
@require_requests
127173
def _gigya_login(client, username, password, api_key):
128174
# Performs a login using the standard Gigya accounts.login API.
129175
# This avoids a custom SAP endpoint that triggers password change notifications.
@@ -154,6 +200,7 @@ def _gigya_login(client, username, password, api_key):
154200
return login_response.get('login_token')
155201

156202

203+
@require_requests
157204
def _get_id_token(client, saml_params, login_token):
158205
# Exchanges a Gigya login token for a JWT ID token.
159206
query_params = {
@@ -166,6 +213,7 @@ def _get_id_token(client, saml_params, login_token):
166213
return token
167214

168215

216+
@require_requests
169217
def _get_uid(client, saml_params, login_token):
170218
# Retrieves the user's unique ID (UID) using the login token.
171219
query_params = {
@@ -177,6 +225,7 @@ def _get_uid(client, saml_params, login_token):
177225
return uid
178226

179227

228+
@require_requests
180229
def _get_uid_details(client, uid, id_token):
181230
# Fetches detailed account information for a given UID.
182231
url = f'{C.URL_ACCOUNT_CORE_API}/accounts/{uid}'
@@ -187,16 +236,18 @@ def _get_uid_details(client, uid, id_token):
187236
return uid_details_response
188237

189238

239+
@require_requests
190240
def _is_uid_linked_multiple_sids(uid_details):
191241
# Checks if a Universal ID (UID) is linked to more than one S-User ID.
192242
accounts = uid_details['accounts']
193243
linked = []
194-
for _, v in accounts.items():
244+
for _account_type, v in accounts.items():
195245
linked.extend(v['linkedAccounts'])
196246

197247
return len(linked) > 1
198248

199249

250+
@require_requests
200251
def _select_account(client, uid, sid, id_token):
201252
# Selects a specific S-User ID when a Universal ID is linked to multiple accounts.
202253
url = f'{C.URL_ACCOUNT_CORE_API}/accounts/{uid}/selectedAccount'
@@ -207,6 +258,7 @@ def _select_account(client, uid, sid, id_token):
207258
return client.request('PUT', url, headers=headers, json=data)
208259

209260

261+
@require_requests
210262
def _get_sdk_build_number(client, api_key):
211263
# Fetches the gigya.js file to extract and cache the SDK build number.
212264
global _GIGYA_SDK_BUILD_NUMBER
@@ -224,6 +276,7 @@ def _get_sdk_build_number(client, api_key):
224276
return build_number
225277

226278

279+
@require_requests
227280
def _cdc_api_request(client, endpoint, saml_params, query_params):
228281
# Helper to make requests to the Gigya/CDC API, handling common parameters and errors.
229282
url = '/'.join((C.URL_ACCOUNT_CDC_API, endpoint))
@@ -251,7 +304,7 @@ def _cdc_api_request(client, endpoint, saml_params, query_params):
251304

252305
error_code = json_response['errorCode']
253306
if error_code != 0:
254-
http_error_msg = '{} Error: {} for url: {}'.format(
307+
http_error_msg = '{0} Error: {1} for url: {2}'.format(
255308
json_response['statusCode'], json_response['errorMessage'], res.url)
256309
raise HTTPError(http_error_msg, response=res)
257310
return json_response

0 commit comments

Comments
 (0)