-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into align-file-headers
Showing
5 changed files
with
816 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |