Skip to content

Commit

Permalink
Merge branch 'main' into align-file-headers
Browse files Browse the repository at this point in the history
sean-freeman authored Nov 28, 2023
2 parents 4a8245e + 3dda10c commit 4dd8602
Showing 5 changed files with 816 additions and 1 deletion.
1 change: 1 addition & 0 deletions plugins/module_utils/constants.py
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@
URL_SOFTWARE_DOWNLOAD = 'https://softwaredownloads.sap.com'
# Maintenance Planner
URL_MAINTENANCE_PLANNER = 'https://maintenanceplanner.cfapps.eu10.hana.ondemand.com'
URL_SYSTEMS_PROVISIONING = 'https://launchpad.support.sap.com/services/odata/i7p/odata/bkey'
URL_USERAPPS = 'https://userapps.support.sap.com/sap/support/mp/index.html'
URL_USERAPP_MP_SERVICE = 'https://userapps.support.sap.com/sap/support/mnp/services'
URL_LEGACY_MP_API = 'https://tech.support.sap.com/sap/support/mnp/services'
2 changes: 1 addition & 1 deletion plugins/module_utils/sap_api_common.py
Original file line number Diff line number Diff line change
@@ -34,7 +34,7 @@ def _request(url, **kwargs):
if 'allow_redirects' not in kwargs:
kwargs['allow_redirects'] = True

method = 'POST' if kwargs.get('data') else 'GET'
method = 'POST' if kwargs.get('data') or kwargs.get('json') else 'GET'
res = https_session.request(method, url, **kwargs)
res.raise_for_status()

400 changes: 400 additions & 0 deletions plugins/module_utils/sap_launchpad_systems_runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,400 @@
from . import constants as C
from .sap_api_common import _request
import json

from requests.exceptions import HTTPError


class InstallationNotFoundError(Exception):
def __init__(self, installation_nr, available_installations):
self.installation_nr = installation_nr
self.available_installations = available_installations


def validate_installation(installation_nr, username):
query_path = f"Installations?$filter=Ubname eq '{username}' and ValidateOnly eq ''"
installations = _request(_url(query_path), headers=_headers({})).json()['d']['results']
if not any(installation['Insnr'] == installation_nr for installation in installations):
raise InstallationNotFoundError(installation_nr, installations)


def get_systems(filter):
query_path = f"Systems?$filter={filter}"
return _request(_url(query_path), headers=_headers({})).json()['d']['results']


class SystemNrInvalidError(Exception):
def __init__(self, system_nr, details):
self.system_nr = system_nr
self.details = details


def get_system(system_nr, installation_nr, username):
query_path = f"Systems?$filter=Uname eq '{username}' and Insnr eq '{installation_nr}' and Sysnr eq '{system_nr}'"

try:
systems = _request(_url(query_path), headers=_headers({})).json()['d']['results']
except HTTPError as err:
# in case the system is not found, the backend doesn't return an empty result set or a 404, but a 400.
# to make the error checking here as resilient as possible,
# just consider an error 400 as an invalid user error and return it to the user.
if err.response.status_code == 400:
raise SystemNrInvalidError(system_nr, err.response.content)
else:
raise err

# not sure this case ever happens; catch it nevertheless.
if len(systems) == 0:
raise SystemNrInvalidError(system_nr, "no systems returned by API")

return systems[0]


class ProductNotFoundError(Exception):
def __init__(self, product, available_products):
self.product = product
self.available_products = available_products


def get_product(product_name, installation_nr, username):
query_path = f"SysProducts?$filter=Uname eq '{username}' and Insnr eq '{installation_nr}' and Sysnr eq '' and Nocheck eq ''"
products = _request(_url(query_path), headers=_headers({})).json()['d']['results']
product = next((product for product in products if product['Description'] == product_name), None)
if product is None:
raise ProductNotFoundError(product_name, products)

return product['Product']


class VersionNotFoundError(Exception):
def __init__(self, version, available_versions):
self.version = version
self.available_versions = available_versions


def get_version(version_name, product_id, installation_nr, username):
query_path = f"SysVersions?$filter=Uname eq '{username}' and Insnr eq '{installation_nr}' and Product eq '{product_id}' and Nocheck eq ''"
versions = _request(_url(query_path), headers=_headers({})).json()['d']['results']
version = next((version for version in versions if version['Description'] == version_name), None)
if version is None:
raise VersionNotFoundError(version_name, versions)

return version['Version']


def validate_system_data(data, version_id, system_nr, installation_nr, username):
"""Validate that the user-provided system data (SID, OS, etc.) is valid according to the SAP API.
In order to validate the data, the SAP API offers two endpoints:
- /SystData: returns the supported fields of a given product version and its supported values. Example:
{
"d": {
"results": [
{
"__metadata": {...},
...
"Output": "[
{ ...
\"FIELD\":\"sysid\",
\"VALUE\":\"System ID\",
\"REQUIRED\":\"X\"
\"DATA\":[]
},
...
{ ...
\"FIELD\":\"sysname\",
\"VALUE\":\"System Name\",
\"REQUIRED\":\"\",
},
{ ...
\"FIELD\":\"systype\",
\"VALUE\":\"System Type\",
\"REQUIRED\":\"X\",
\"DATA\": [
{\"NAME\":\"ARCHIVE\",\"VALUE\":\"Archive System\"},
{\"NAME\":\"BACKUP\",\"VALUE\":\"Backup system\"},
{\"NAME\":\"DEMO\",\"VALUE\":\"Demo system\"},
...
]
},
So to ensure the user provided valid system data values,
we fetch these fields and ensure all the required fields are set and contain valid options.
- Afterward, the validated data is sent to /SystemDataCheck to verify the data is accepted by the SAP API.
This endpoint might optionally return warnings (i.e. if the SID is used in more than one system), which are passed on to the user.
"""

query_path = f"SystData?$filter=Pvnr eq '{version_id}' and Insnr eq '{installation_nr}'"
results = _request(_url(query_path), headers=_headers({})).json()['d']['results'][0]
possible_fields = json.loads(results['Output'])
final_fields = _validate_user_data_against_supported_fields("system", data, possible_fields)

final_fields['Prodver'] = version_id
final_fields['Insnr'] = installation_nr
final_fields['Uname'] = username
final_fields['Sysnr'] = system_nr
final_fields = [{"name": k, "value": v} for k, v in final_fields.items()]
query_path = f"SystemDataCheck?$filter=Nocheck eq '' and Data eq '{json.dumps(final_fields)}'"
results = _request(_url(query_path), headers=_headers({})).json()['d']['results']

warning = None
if len(results) > 0:
warning = json.loads(results[0]['Data'])[0]['VALUE']

# interestingly, all downstream api calls require the names in lowercase. transform it for further usage.
final_fields = [{"name": entry["name"].lower(), "value": entry["value"]} for entry in final_fields]
return final_fields, warning


class LicenseTypeInvalidError(Exception):
def __init__(self, license_type, available_license_types):
self.license_type = license_type
self.available_license_types = available_license_types


def validate_licenses(licenses, version_id, installation_nr, username):
"""Validate that the user-provided licenses (license type and data like hardware key, expiry time) are valid
according to the SAP API.
In order to validate the data, this function makes use of the /LicenseType API endpoint which provides the supported
license data for a given product version. Example for S4HANA2022:
{
"d": {
"results": [
{
"__metadata": {...},
"INSNR": "123456789",
"PRODUCT": "73554900100800000266",
"PRODID": "Maintenance",
"LICENSETYPE": "Maintenance Entitlement",
"QtyUnit": "",
"Selfields": "[
{\"FIELD\":\"hwkey\",\"VALUE\":\"Hardware Key\",\"REQUIRED\":\"X\",\"DEFAULT\":\"\",\"DATA\":[], ...},
{\"FIELD\":\"expdate\",\"VALUE\":\"Valid until\",\"REQUIRED\":\"X\",\"DEFAULT\":\"20240130\",\"DATA\":[], ...}]",
...
So to ensure the user provided valid license values,
we fetch these fields and ensure that the license type exists and all the required fields are set and contain valid options.
"""

query_path = f"LicenseType?$filter=PRODUCT eq '{version_id}' and INSNR eq '{installation_nr}' and Uname eq '{username}' and Nocheck eq 'True'"
results = _request(_url(query_path), headers=_headers({})).json()['d']['results']

available_license_types = {result["LICENSETYPE"] for result in results}
license_data = []

for license in licenses:
result = next((result for result in results if result["LICENSETYPE"] == license['type']), None)
if result is None:
raise LicenseTypeInvalidError(license['type'], available_license_types)

final_fields = _validate_user_data_against_supported_fields(f'license {license["type"]}', license['data'],
json.loads(result["Selfields"]))
# for some reason, the downstream API calls require the keys in uppercase - transform them.
final_fields = {k.upper(): v for k, v in final_fields.items()}
final_fields["LICENSETYPE"] = result['PRODID']
final_fields["LICENSETYPETEXT"] = result['LICENSETYPE']
license_data.append(final_fields)

return license_data


def get_existing_licenses(system_nr, username):
query_path = f"LicenseKeys?$filter=Uname eq '{username}' and Sysnr eq '{system_nr}'"
results = _request(_url(query_path), headers=_headers({})).json()['d']['results']
# for some weird reason that probably only SAP knows, when updating the licenses based on the results here,
# they expect a completely different format. let's transform to the format the backend expects.
# this code most likely doesn't work for licenses that have different parameters than S4HANA or SAP HANA
# (which only use HWKEY, EXPDATE and QUANTITY), as I only tested it with those two license types.
# feel free to extend (or, even better, come up with a generic way to transform the parameters).
return [
{
"LICENSETYPETEXT": result["LicenseDescr"],
"LICENSETYPE": result["Prodid"],
"HWKEY": result["Hwkey"],
"EXPDATE": result["LidatC"],
"STATUS": result["Status"],
"STATUSCODE": result["StatusCode"],
"KEYNR": result["Keynr"],
"QUANTITY": result["Ulimit"],
"QUANTITY_C": result["UlimitC"],
"MAXEXPDATE": result["MaxLiDat"]
} for result in results
]


def keep_only_new_or_changed_licenses(existing_licenses, license_data):
"""Given a system's licenses (existing_licenses) and the user-provided licenses (license_data), return only new or changed licenses.
Why is this necessary? The SAP API Endpoint /BSHWKEY (in function generate_licenses) fails if an identical license
is generated twice - thus, this function removes identical licenses are removed from the user provided data.
"""

new_or_changed_licenses = []
for license in license_data:
if not any(license['HWKEY'] == lic['HWKEY'] and license['LICENSETYPE'] == lic['LICENSETYPE'] for lic in
existing_licenses):
new_or_changed_licenses.append(license)

return new_or_changed_licenses


def generate_licenses(license_data, existing_licenses, version_id, installation_nr, username):
body = {
"Prodver": version_id,
"ActionCode": "add",
"ExistingData": json.dumps(existing_licenses),
"Entry": json.dumps(license_data),
"Nocheck": "",
"Insnr": installation_nr,
"Uname": username
}
response = _request(_url("BSHWKEY"), json=body, headers=_headers({'x-csrf-token': _get_csrf_token()})).json()
return json.loads(response['d']['Result'])


def submit_system(is_new, system_data, generated_licenses, username):
body = {
"actcode": "add" if is_new else "edit",
"Uname": username,
"sysdata": json.dumps(system_data),
"matdata": json.dumps(
# again, SAP Backend requires a completely different format than it returned. let's map it.
# this code most likely doesn't work for licenses that have different parameters than S4HANA or SAP HANA
# (which only use HWKEY, EXPDATE and QUANTITY), as I only tested it with those two license types.
# feel free to extend (or, even better, come up with a generic way to transform the parameters).
[
{
"hwkey": license["HWKEY"],
"prodid": license["LICENSETYPE"],
"quantity": license["QUANTITY"],
"keynr": license["KEYNR"],
"expdat": license["EXPDATE"],
"status": license["STATUS"],
"statusCode": license["STATUSCODE"],
} for license in generated_licenses
]
)
}
response = _request(_url("Submit"), json=body, headers=_headers({'x-csrf-token': _get_csrf_token()})).json()
return json.loads(response['d']['licdata'])[0]['VALUE'] # contains system number


def get_license_key_numbers(license_data, system_nr, username):
key_nrs = []
for license in license_data:
query_path = f"LicenseKeys?$filter=Uname eq '{username}' and Sysnr eq '{system_nr}' and Prodid eq '{license['LICENSETYPE']}' and Hwkey eq '{license['HWKEY']}'"
results = _request(_url(query_path), headers=_headers({})).json()['d']['results']
key_nrs.append(results[0]['Keynr'])

return key_nrs


def download_licenses(key_nrs):
keys_json = json.dumps([{"Keynr": key_nr} for key_nr in key_nrs])
return _request(_url(f"FileContent(Keynr='{keys_json}')/$value")).content


def select_licenses_to_delete(key_nrs_to_keep, existing_licenses):
return [existing_license for existing_license in existing_licenses if
not existing_license['KEYNR'] in key_nrs_to_keep]


def delete_licenses(licenses_to_delete, existing_licenses, version_id, installation_nr, username):
body = {
"Prodver": version_id,
"ActionCode": "delete",
"ExistingData": json.dumps(existing_licenses),
"Entry": json.dumps(licenses_to_delete),
"Nocheck": "",
"Insnr": installation_nr,
"Uname": username
}
response = _request(_url("BSHWKEY"), json=body, headers=_headers({'x-csrf-token': _get_csrf_token()})).json()
return json.loads(response['d']['Result'])


def _url(query_path):
return f'{C.URL_SYSTEMS_PROVISIONING}/{query_path}'


def _headers(additional_headers):
return {**{'Accept': 'application/json'}, **additional_headers}


def _get_csrf_token():
return _request(C.URL_SYSTEMS_PROVISIONING, headers=_headers({'x-csrf-token': 'Fetch'})).headers['x-csrf-token']


class DataInvalidError(Exception):
def __init__(self, scope, unknown_fields, missing_required_fields, fields_with_invalid_option):
self.scope = scope
self.unknown_fields = unknown_fields
self.missing_required_fields = missing_required_fields
self.fields_with_invalid_option = fields_with_invalid_option


def _validate_user_data_against_supported_fields(scope, user_data, possible_fields):
"""Validates user-provided data against all supported fields (provided by the SAP API).
In various areas the SAP API provides which data attributes are supported for a given entity:
- i.e. for system data the supported fields are provided in /SystData (see function validate_system_data)
- i.e. for license data the supported fields are provided in /LicenseType (see function validate_licenses)
The SAP API provides the supported fields in a common format:
{ ...
\"FIELD\":\"free-text-field-name\",
\"REQUIRED\":\"X\"
\"DATA\":[]
},
...
{ ...
\"FIELD\":\"optional-field-name\",
\"REQUIRED\":\"\",
\"DATA\":[]
},
{ ...
\"FIELD\":field-with-predefined-options\",
\"REQUIRED\":\"X\",
\"DATA\": [
{\"NAME\":\"OPTION1\",\"VALUE\":\"Description of Option1\"},
{\"NAME\":\"OPTION2\",\"VALUE\":\"Description of Option2\"},
{\"NAME\":\"OPTION3\",\"VALUE\":\"Description of Option3\"},
...
]
}
This helper method uses those fields provided by the SAP API and the user-provided data and raises a DataInvalidError
if any of the following issues is detected
- DataInvalidError.missing_fields: a required field (= REQUIRED = 'X') is not provided by the user
- DataInvalidError.fields_with_invalid_option: the user specified a invalid option for a field which has defined options
- DataInvalidError.unknown_fields: user provided a field which is not supported by SAP API
"""

unknown_fields = {field for field, _ in user_data.items() if
not any(field == possible_field['FIELD'] for possible_field in possible_fields)}
missing_required_fields = {}
fields_with_invalid_option = {}
final_fields = {}

for possible_field in possible_fields:
user_value = user_data.get(possible_field["FIELD"])
if user_value is not None: # user has provided a value for this field
if len(possible_field["DATA"]) == 0: # there are no options for these fields = all inputs are ok.
final_fields[possible_field["FIELD"]] = user_value

else: # there are options for these fields - resolve their values by their description
resolved_value = next(
(entry["NAME"] for entry in possible_field["DATA"] if entry['VALUE'] == user_value), None)
if resolved_value is None:
fields_with_invalid_option[possible_field["FIELD"]] = possible_field["DATA"]
else:
final_fields[possible_field["FIELD"]] = resolved_value
elif possible_field['REQUIRED'] == "X": # missing required field
missing_required_fields[possible_field["FIELD"]] = possible_field["DATA"]

if len(unknown_fields) > 0 or len(missing_required_fields) > 0 or len(fields_with_invalid_option) > 0:
raise DataInvalidError(scope, unknown_fields, missing_required_fields, fields_with_invalid_option)

return final_fields
313 changes: 313 additions & 0 deletions plugins/modules/license_keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
from ansible.module_utils.basic import AnsibleModule

from ..module_utils.sap_launchpad_systems_runner import *
from ..module_utils.sap_id_sso import sap_sso_login


DOCUMENTATION = r'''
---
module: license_keys
short_description: Creates systems and license keys on me.sap.com/licensekey
description:
- This ansible module creates and updates systems and their license keys using the Launchpad API.
- It is closely modeled after the interactions in the portal U(https://me.sap.com/licensekey):
- First, a SAP system is defined by its SID, product, version and other data.
- Then, for this system, license keys are defined by license type, HW key and potential other attributes.
- The system and license data is then validated and submitted to the Launchpad API and the license key files returned to the caller.
- This module attempts to be as idempotent as possible, so it can be used in a CI/CD pipeline.
version_added: 1.1.0
options:
suser_id:
description:
- SAP S-User ID.
required: true
type: str
suser_password:
description:
- SAP S-User Password.
required: true
type: str
installation_nr:
description:
- Number of the Installation for which the system should be created/updated
required: true
type: str
system:
description:
- The system to create/update
required: true
type: dict
suboptions:
nr:
description:
- The number of the system to update. If this attribute is not provided, a new system is created.
required: false
type: str
product:
description:
- The product description as found in the SAP portal, e.g. SAP S/4HANA
required: true
type: str
version:
description:
- The description of the product version, as found in the SAP portal, e.g. SAP S/4HANA 2022
required: true
type: str
data:
description:
- The data attributes of the system. The possible attributes are defined by product and version.
- Running the module without any data attributes will return in the error message which attributes are supported/required.
required: true
type: dict
licenses:
description:
- List of licenses to create for the system.
- If the license does not exist, it is created.
- If it exists, it is updated.
required: true
type: list
elements: dict
suboptions:
type:
description:
- The license type description as found in the SAP portal, e.g. Maintenance Entitlement
required: true
type: str
data:
description:
- The data attributes of the licenses. The possible attributes are defined by product and version.
- Running the module without any data attributes will return in the error message which attributes are supported/required
- In practice, most license types require at least a hardware key (hwkey) and expiry date (expdate)
required: true
type: dict
delete_other_licenses:
description:
- Whether licenses other than the ones specified in the licenses attributes should be deleted.
- This is handy to clean up older licenses automatically.
type: bool
required: false
default: false
author:
- Lab for SAP Solutions
'''


EXAMPLES = r'''
- name: create license keys
community.sap_launchpad.license_keys:
suser_id: 'SXXXXXXXX'
suser_password: 'password'
installation_nr: 12345678
system:
nr: 23456789
product: SAP S/4HANA
version: SAP S/4HANA 2022
data:
sysid: H01
sysname: Test-System
systype: Development system
sysdb: SAP HANA database
sysos: Linux
sys_depl: Public - Microsoft Azure
licenses:
- type: Standard - Web Application Server ABAP or ABAP+JAVA
data:
hwkey: H1234567890
expdate: 99991231
- type: Maintenance Entitlement
data:
hwkey: H1234567890
expdate: 99991231
delete_other_licenses: true
register: result
- name: Display the license file containing the licenses
debug:
msg:
- "{{ result.license_file }}"
'''


RETURN = r'''
license_file:
description: |
The license file containing the digital signatures of the specified licenses.
All licenses that were provided in the licenses attribute are returned, no matter if they were modified or not.
returned: always
type: string
sample: |
----- Begin SAP License -----
SAPSYSTEM=H01
HARDWARE-KEY=H1234567890
INSTNO=0012345678
BEGIN=20231026
EXPIRATION=99991231
LKEY=MIIBO...
SWPRODUCTNAME=NetWeaver_MYS
SWPRODUCTLIMIT=2147483647
SYSTEM-NR=00000000023456789
----- Begin SAP License -----
SAPSYSTEM=H01
HARDWARE-KEY=H1234567890
INSTNO=0012345678
BEGIN=20231026
EXPIRATION=20240127
LKEY=MIIBO...
SWPRODUCTNAME=Maintenance_MYS
SWPRODUCTLIMIT=2147483647
SYSTEM-NR=00000000023456789
system_nr:
description: The number of the system which was created/updated.
returned: always
type: string
sample: 23456789
'''


def run_module():
# Define available arguments/parameters a user can pass to the module
module_args = dict(
suser_id=dict(type='str', required=True),
suser_password=dict(type='str', required=True, no_log=True),
installation_nr=dict(type='str', required=True),
system=dict(
type='dict',
options=dict(
nr=dict(type='str', required=False),
product=dict(type='str', required=True),
version=dict(type='str', required=True),
data=dict(type='dict')
)
),
licenses=dict(type='list', required=True, elements='dict', options=dict(
type=dict(type='str', required=True),
data=dict(type='dict'),
)),
delete_other_licenses=dict(type='bool', required=False, default=False),
)

# Define result dictionary objects to be passed back to Ansible
result = dict(
license_file='',
system_nr='',
# as we don't have a diff mechanism but always submit the system, we don't have a way to detect changes.
# it might always have changed.
changed=True,
)

# Instantiate module
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=False
)

username = module.params.get('suser_id')
password = module.params.get('suser_password')
installation_nr = module.params.get('installation_nr')
system = module.params.get('system')
system_nr = system.get('nr')
product = system.get('product')
version = system.get('version')
data = system.get('data')
licenses = module.params.get('licenses')

if len(licenses) == 0:
module.fail_json("licenses cannot be empty")

delete_other_licenses = module.params.get('delete_other_licenses')

sap_sso_login(username, password)


try:
validate_installation(installation_nr, username)
except InstallationNotFoundError as err:
module.fail_json("Installation could not be found", installation_nr=err.installation_nr,
available_installations=[inst['Text'] for inst in err.available_installations])

existing_system = None
if system_nr is not None:
try:
existing_system = get_system(system_nr, installation_nr, username)
except SystemNrInvalidError as err:
module.fail_json("System could not be found", system_nr=err.system_nr, details=err.details)

product_id = None
try:
product_id = get_product(product, installation_nr, username)
except ProductNotFoundError as err:
module.fail_json("Product could not be found", product=err.product,
available_products=[product['Description'] for product in err.available_products])

version_id = None
try:
version_id = get_version(version, product_id, installation_nr, username)
except VersionNotFoundError as err:
module.fail_json("Version could not be found", version=err.version,
available_versions=[version['Description'] for version in err.available_versions])

system_data = None
try:
system_data, warning = validate_system_data(data, version_id, system_nr, installation_nr, username)
if warning is not None:
module.warn(warning)
except DataInvalidError as err:
module.fail_json(f"Invalid {err.scope} data",
unknown_fields=err.unknown_fields,
missing_required_fields=err.missing_required_fields,
fields_with_invalid_option=err.fields_with_invalid_option)

license_data = None
try:
license_data = validate_licenses(licenses, version_id, installation_nr, username)
except LicenseTypeInvalidError as err:
module.fail_json(f"Invalid license type", license_type=err.license_type, available_license_types=err.available_license_types)
except DataInvalidError as err:
module.fail_json(f"Invalid {err.scope} data",
unknown_fields=err.unknown_fields,
missing_required_fields=err.missing_required_fields,
fields_with_invalid_option=err.fields_with_invalid_option)

generated_licenses = []
existing_licenses = []
new_or_changed_license_data = license_data

if existing_system is not None:
existing_licenses = get_existing_licenses(system_nr, username)
new_or_changed_license_data = keep_only_new_or_changed_licenses(existing_licenses, license_data)

if len(new_or_changed_license_data) > 0:
generated_licenses = generate_licenses(new_or_changed_license_data, existing_licenses, version_id,
installation_nr, username)

system_nr = submit_system(existing_system is None, system_data, generated_licenses, username)
key_nrs = get_license_key_numbers(license_data, system_nr, username)
result['license_file'] = download_licenses(key_nrs)
result['system_nr'] = system_nr

if delete_other_licenses:
existing_licenses = get_existing_licenses(system_nr, username)
licenses_to_delete = select_licenses_to_delete(key_nrs, existing_licenses)
if len(licenses_to_delete) > 0:
updated_licenses = delete_licenses(licenses_to_delete, existing_licenses, version_id, installation_nr, username)
submit_system(False, system_data, updated_licenses, username)

module.exit_json(**result)


def main():
run_module()


if __name__ == '__main__':
main()
101 changes: 101 additions & 0 deletions plugins/modules/systems_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from ansible.module_utils.basic import AnsibleModule

from ..module_utils.sap_launchpad_systems_runner import *
from ..module_utils.sap_id_sso import sap_sso_login

from requests.exceptions import HTTPError

DOCUMENTATION = r'''
---
module: systems_info
short_description: Queries registered systems in me.sap.com
description:
- Fetch Systems from U(me.sap.com) with ODATA query filtering and returns the discovered Systems.
- The query could easily copied from U(https://launchpad.support.sap.com/services/odata/i7p/odata/bkey)
version_added: 1.1.0
options:
suser_id:
description:
- SAP S-User ID.
required: true
type: str
suser_password:
description:
- SAP S-User Password.
required: true
type: str
filter:
description:
- An ODATA filter expression to query the systems.
required: true
type: str
author:
- Lab for SAP Solutions
'''


EXAMPLES = r'''
- name: get system by SID and product
community.sap_launchpad.systems_info:
suser_id: 'SXXXXXXXX'
suser_password: 'password'
filter: "Insnr eq '12345678' and sysid eq 'H01' and ProductDescr eq 'SAP S/4HANA'"
register: result
- name: Display the first returned system
debug:
msg:
- "{{ result.systems[0] }}"
'''


RETURN = r'''
systems:
description: the systems returned for the filter
returned: always
type: list
'''


def run_module():
module_args = dict(
suser_id=dict(type='str', required=True),
suser_password=dict(type='str', required=True, no_log=True),
filter=dict(type='str', required=True),
)

result = dict(
systems='',
)

module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=False
)

username = module.params.get('suser_id')
password = module.params.get('suser_password')
filter = module.params.get('filter')

sap_sso_login(username, password)

try:
result["systems"] = get_systems(filter)
except HTTPError as err:
module.fail_json("Error while querying systems", status_code=err.response.status_code,
response=err.response.content)

module.exit_json(**result)


def main():
run_module()


if __name__ == '__main__':
main()

0 comments on commit 4dd8602

Please sign in to comment.