deploy/ansible/action_plugins/public_api.py (1,708 lines of code) (raw):

# Copyright (c) Microsoft Corporation. # Licensed under the MIT License. #!/usr/bin/env python3.9 from __future__ import absolute_import, division, print_function __metaclass__ = type import json import os import tempfile import requests from cryptography.fernet import Fernet from azure.common.exceptions import AuthenticationError from msrest.exceptions import ClientRequestError from azure.common.credentials import ServicePrincipalCredentials from azure.keyvault import KeyVaultClient from ansible.errors import AnsibleConnectionFailure, AnsibleActionFail from ansible.utils.display import Display from ansible.module_utils.urls import Request, ConnectionError from six.moves.urllib.error import HTTPError, URLError from ansible.plugins.action import ActionBase method_spec_product = dict( method=dict(type="str", required=True), calKeyvaultId=dict(type="str", required=True, no_log=True), clientId=dict(type="str", no_log=True), clientSecret=dict(type="str", no_log=True), ) method_spec_progress = dict( method=dict(type="str", required=True), calKeyvaultId=dict(type="str", required=True, no_log=True), clientId=dict(type="str", no_log=True), clientSecret=dict(type="str", no_log=True), systemId=dict(type="str", required=True), outputDirectoryPath=dict(type="str", no_log=True), outputFile=dict(type="str", no_log=True), ) method_spec_deployment = dict( method=dict(type="str", required=True), outputDirectoryPath=dict(type="str", no_log=True), outputFile=dict(type="str", no_log=True), calKeyvaultId=dict(type="str", required=True, no_log=True), clientId=dict(type="str", no_log=True), clientSecret=dict(type="str", no_log=True), tenantId=dict(type="str", no_log=True), accountId=dict(type="str", required=True, no_log=True), productId=dict(type="str", required=True, no_log=True), cloudProvider=dict(type="str", required=True), planTemplateId=dict(type="str", required=True, no_log=True), planTemplateName=dict(type="str", required=True, no_log=True), region=dict(type="str", default="eastus2"), availabilityScenario=dict( type="str", choices=["non-ha", "hana-system-replication", "clustering"], default="clustering", ), infrastructureParameterSet=dict( type="dict", required=True, options=dict( operatingSystem=dict( type="str", default="SUSE/sles-sap-15-sp3/gen1/2022.11.09" ), privateDnsZone=dict(type="str", required=True), reversePrivateDnsZone=dict(type="str", required=True, no_log=True), transitNetwork=dict(type="str", required=True, no_log=True), workloadNetwork=dict(type="str", required=True, no_log=True), sharedServicesNetwork=dict(type="str", required=True, no_log=True), sharedServicesSubnet=dict(type="str", required=True, no_log=True), workloadNetworkHanaSubnet=dict(type="str", required=True, no_log=True), workloadNetworkAsSubnet=dict(type="str", required=True, no_log=True), technicalCommunicationUser=dict(type="str", required=True, no_log=True), techUserPassword=dict(type="str", required=True, no_log=True), maintenancePlannerTransaction=dict(type="str", required=True, no_log=True), hanaVmSize=dict(type="str", required=False, default="Standard_E20ds_v5"), centralServicesVmSize=dict( type="str", required=False, default="Standard_D4ds_v5" ), enqueueReplicationServerVmSize=dict( type="str", required=False, default="Standard_D4ds_v5" ), applicationServerVmSize=dict( type="str", required=False, default="Standard_E4ds_v5" ), numberOfApplicationServers=dict(type="int", required=False, default="0"), webDispatcherVmSize=dict( type="str", required=False, default="Standard_D2s_v5" ), ), ), installationParameterSets=dict( type="dict", required=True, apply_defaults=True, options=dict( clientId=dict( type="str", required_if=[("availabilityScenario", "==", "clustering")], no_log=True, ), clientSecret=dict( type="str", required_if=[("availabilityScenario", "==", "clustering")], no_log=True, ), hanaDeployment=dict( type="dict", apply_defaults=True, options=dict( DBSID=dict(type="str", default="HDB"), DBSIDAdminUserId=dict(type="str", default="1050"), instanceNumber=dict(type="str", default="00"), frontendHostname=dict(type="str", default="vhdbdb"), primaryHanaPhysicalHostname=dict(type="str", default="phdbdbpr"), primaryHanaVirtualHostname=dict(type="str", default="vhdbdbpr"), secondaryHanaPhysicalHostname=dict(type="str", default="phdbdbsr"), secondaryHanaVirtualHostname=dict(type="str", default="vhdbdbsr"), ), ), s4hanaDeployment=dict( type="dict", apply_defaults=True, options=dict( SID=dict(type="str", default="S4H"), SAPSysAdminUserId=dict(type="str", default="1079"), SAPSysAdminGroupId=dict(type="str", default="79"), sapGuiDefaultLanguage=dict(type="str", default="en"), SAPSystemAdditionalLanguages=dict(type="str", default=""), numberOfDialogWorkProcesses=dict(type="int", default="10"), numberOfBatchWorkProcesses=dict(type="int", default="7"), ), ), centralServicesDeployment=dict( type="dict", apply_defaults=True, options=dict( instanceNumber=dict(type="str", default="00"), ABAPMessageServerPort=dict(type="str", default="3600"), physicalHostname=dict(type="str", default="ps4hcs"), virtualHostname=dict(type="str", default="vs4hcs"), ), ), enqueueReplicationServerDeployment=dict( type="dict", apply_defaults=True, options=dict( instanceNumber=dict(type="str", default="10"), physicalHostname=dict(type="str", default="ps4hers"), virtualHostname=dict(type="str", default="vs4hers"), ), ), primaryApplicationServerDeployment=dict( type="dict", apply_defaults=True, options=dict( instanceNumber=dict(type="str", default="00"), physicalHostname=dict(type="str", default="ps4hpas"), virtualHostname=dict(type="str", default="vs4hpas"), ), ), additionalApplicationServersDeployment=dict( type="list", elements="dict", apply_defaults=True, options=dict( instanceNumber=dict(type="str", default="00"), physicalHostname=dict(type="str", default="ps4haas1"), virtualHostname=dict(type="str", default="vs4haas1"), ), ), webDispatcherDeployment=dict( type="dict", apply_defaults=True, options=dict( installationType=dict( type="str", choices=["Standalone", "Embedded", "None", "External"], default="None", ), primaryInstanceNumber=dict(type="str", default="00"), primaryPhysicalHostname=dict(ype="str", default="ps4hwdpr"), primaryVirtualHostname=dict(type="str", default="vs4hwdpr"), secondaryInstanceNumber=dict(type="str", default="00"), secondaryPhysicalHostname=dict(type="str", default="ps4hwdsr"), secondaryVirtualHostname=dict(type="str", default="vs4hwdsr"), userIdOfSIDAdmin=dict(type="str", default="1080"), virtualHostname=dict(type="str", default="vs4hwdext"), fioriHostname=dict(type="str", default="vs4hwdext"), fioriHostPort=dict(type="int", default="44300"), productiveClientNumber=dict(type="str", default="500"), ), ), ), ), ) method_spec_provisioning = dict( method=dict(type="str", required=True), outputDirectoryPath=dict(type="str", no_log=True), outputFile=dict(type="str", no_log=True), productId=dict(type="str", required=True, no_log=True), planTemplateId=dict(type="str", no_log=True, default="default"), availabilityScenario=dict( type="str", choices=["non-ha", "hana-system-replication", "clustering"], default="clustering", ), calKeyvaultId=dict(type="str", required=True, no_log=True), clientId=dict(type="str", no_log=True), clientSecret=dict(type="str", no_log=True), tenantId=dict(type="str", no_log=True), infrastructureParameterSet=dict( type="dict", required=True, required_one_of=[ ["domainName", "privateDnsZone"], ["techUserPassword", "techUserPasswordReference"], ], mutually_exclusive=[ ["domainName", "privateDnsZone"], ["techUserPassword", "techUserPasswordReference"], ], options=dict( privateDnsZone=dict(type="str", no_log=True), domainName=dict(type="str", no_log=True), secretStoreId=dict(type="str", required=True, no_log=True), deploymentServerSubnet=dict(type="str", no_log=True), executionEngineSubnet=dict(type="str", no_log=True), technicalCommunicationUser=dict(type="str", required=True, no_log=True), techUserPassword=dict(type="str", no_log=True, default=""), techUserPasswordReference=dict(type="str", no_log=True), remoteOsUser=dict(type="str", required=True, no_log=True), deploymentServerResourceGroup=dict(type="str", required=False, no_log=True), sshPublicKeySecretName=dict(type="str", required=True, no_log=True), sshPrivateKeySecretName=dict(type="str", required=True, no_log=True), parameters=dict(type="str", no_log=True), ), ), installationParameterSets=dict( type="dict", required=True, apply_defaults=True, options=dict( hanaDeployment=dict( type="dict", apply_defaults=True, options=dict( primaryVmResourceId=dict(type="str", required=True), secondaryVmResourceId=dict(type="str", default=""), loadBalancerResourceId=dict(type="str", default=""), frontEndIp=dict(type="str", default=""), DBSID=dict(type="str", default="HDB"), DBSIDAdminUserId=dict(type="str", default="1050"), instanceNumber=dict(type="str", default="00"), frontendHostname=dict(type="str", default=""), primaryPhysicalHostname=dict(type="str", default=""), primaryVirtualHostname=dict(type="str", default=""), secondaryPhysicalHostname=dict(type="str", default=""), secondaryVirtualHostname=dict(type="str", default=""), ), ), s4hanaDeployment=dict( type="dict", apply_defaults=True, options=dict( SID=dict(type="str", default="S4H"), SAPSysAdminUserId=dict(type="str", default="1079"), SAPSysAdminGroupId=dict(type="str", default="79"), sapGuiDefaultLanguage=dict(type="str", default="en"), SAPSystemAdditionalLanguages=dict(type="str", default=""), numberOfDialogWorkProcesses=dict(type="str", default="10"), numberOfBatchWorkProcesses=dict(type="str", default="7"), ), ), centralServicesDeployment=dict( type="dict", apply_defaults=True, options=dict( vmResourceId=dict(type="str", required=True), loadBalancerResourceId=dict(type="str", default=""), frontEndIp=dict(type="str", default=""), instanceNumber=dict(type="str", default="00"), ABAPMessageServerPort=dict(type="str", default=""), physicalHostname=dict(type="str", default=""), virtualHostname=dict(type="str", default=""), loadBalancerHostname=dict( type="str", required_if=[("availabilityScenario", "==", "clustering")], ), ), ), enqueueReplicationServerDeployment=dict( type="dict", apply_defaults=True, options=dict( vmResourceId=dict(type="str", default=""), frontEndIp=dict(type="str", default=""), instanceNumber=dict(type="str", default="10"), physicalHostname=dict(type="str", default=""), virtualHostname=dict(type="str", default=""), loadBalancerHostname=dict(type="str"), ), ), applicationServersDeployment=dict( type="list", elements="dict", apply_defaults=True, options=dict( vmResourceId=dict(type="str", default=""), instanceNumber=dict(type="str", default="00"), physicalHostname=dict(type="str", default=""), virtualHostname=dict(type="str", default=""), ), ), fioriConfiguration=dict( type="dict", apply_defaults=True, options=dict( fioriHostname=dict(type="str", default=""), fioriHostPort=dict(type="str", default="44300"), productiveClientNumber=dict(type="str", default="500"), ossUser=dict(type="str", default=""), ossUserPassword=dict(type="str", default=""), ossUserPasswordReference=dict(type="str", default=""), ), ), webDispatcherDeployment=dict( type="dict", apply_defaults=True, options=dict( installationType=dict( type="str", choices=["Standalone", "Embedded", "None", "External"], default="None", ), virtualHostname=dict(type="str", default=""), primaryVmResourceId=dict(type="str", default=""), primaryInstanceNumber=dict(type="str", default="00"), primaryPhysicalHostname=dict(type="str", default=""), primaryVirtualHostname=dict(type="str", default=""), userIdOfSIDAdmin=dict(type="str", default="1080"), secondaryVmResourceId=dict(type="str", default=""), loadBalancerResourceId=dict(type="str", default=""), frontEndIp=dict(type="str", default=""), secondaryInstanceNumber=dict(type="str", default="00"), secondaryPhysicalHostname=dict(type="str", default=""), secondaryVirtualHostname=dict(type="str", default=""), ), ), ), ), ) required_together = [["clientId", "clientSecret", "tenantId"]] # Generate a key for encryption/decryption FERNET_KEY = os.environ.get("FERNET_KEY", Fernet.generate_key().decode()) fernet = Fernet(FERNET_KEY.encode()) class SAPsystem: def __init__(self, params): self.input_params = params method = params.get("method") scenario = params.get("availabilityScenario") self.infrastructureParameterSet = params.get("infrastructureParameterSet") self.installationParameterSets = params.get("installationParameterSets") webdisp_type = self.installationParameterSets.get( "webDispatcherDeployment" ).get("installationType") if method == "deployment": self.props = self.get_nonha_deployment_params() if scenario == "hana-system-replication": self.props.get("installationParameterSets").update( self.get_ha_deployment_params() ) elif scenario == "clustering": self.props.get("installationParameterSets").update( self.get_ha_deployment_params() ) self.props.get("installationParameterSets").update( self.get_cluster_deployment_params() ) if webdisp_type != "No": if webdisp_type == "Standalone": self.props["installationParameterSets"]["webDispatcherDeployment"][ "parameters" ] += self.get_webdisp_deployment_standalone_params().get( "parameters" ) self.props["installationParameterSets"]["webDispatcherDeployment"][ "parameters" ] += self.get_webdisp_deployment_params().get("parameters") if scenario != "NON_HA": self.props["installationParameterSets"][ "webDispatcherDeployment" ]["parameters"] += self.get_webdisp_ha_deployment_params().get( "parameters" ) else: self.props["installationParameterSets"]["webDispatcherDeployment"][ "parameters" ] += self.get_webdisp_deployment_params().get("parameters") elif method == "software_provisioning": self.props = self.get_nonha_provisioning_params() if scenario == "hana-system-replication": self.props.get("deploymentParameterSets").update( self.get_ha_provisioning_params() ) elif scenario == "clustering": self.props.get("deploymentParameterSets").update( self.get_ha_provisioning_params() ) def clean_parameters(self, parameters): # Filter out parameter dictionaries with value == "" or missing 'value' key return [param for param in parameters if param.get("value") not in [None, ""]] def clean_structure(self, structure): # Apply cleaning to the structure recursively if isinstance(structure, dict): cleaned_structure = {} for k, v in structure.items(): if k == "parameters" and isinstance(v, list): cleaned_structure[k] = self.clean_parameters(v) else: cleaned_structure[k] = self.clean_structure(v) return cleaned_structure elif isinstance(structure, list): return [self.clean_structure(item) for item in structure if item != ""] else: return structure def get_props(self): return self.clean_structure(self.props) def get_nonha_deployment_params(self): return { "accountId": self.input_params.get("accountId"), "productId": self.input_params.get("productId"), "planTemplateId": self.input_params.get("planTemplateId"), "planTemplateName": self.input_params.get("planTemplateName"), "region": self.input_params.get("region"), "cloudProvider": self.input_params.get("cloudProvider"), "availabilityScenario": self.input_params.get("availabilityScenario"), "infrastructureParameterSet": { "operatingSystem": self.infrastructureParameterSet.get( "operatingSystem" ), "privateDnsZone": self.infrastructureParameterSet.get("privateDnsZone"), "reversePrivateDnsZone": self.infrastructureParameterSet.get( "reversePrivateDnsZone" ), "transitNetwork": self.infrastructureParameterSet.get("transitNetwork"), "workloadNetwork": self.infrastructureParameterSet.get( "workloadNetwork" ), "sharedServicesNetwork": self.infrastructureParameterSet.get( "sharedServicesNetwork" ), "sharedServicesSubnet": self.infrastructureParameterSet.get( "sharedServicesSubnet" ), "workloadNetworkHanaSubnet": self.infrastructureParameterSet.get( "workloadNetworkHanaSubnet" ), "workloadNetworkAsSubnet": self.infrastructureParameterSet.get( "workloadNetworkAsSubnet" ), "hanaVmSize": self.infrastructureParameterSet.get("hanaVmSize"), "centralServicesVmSize": self.infrastructureParameterSet.get( "centralServicesVmSize" ), "enqueueReplicationServerVmSize": self.infrastructureParameterSet.get( "enqueueReplicationServerVmSize" ), "applicationServerVmSize": self.infrastructureParameterSet.get( "applicationServerVmSize" ), "numberOfApplicationServers": self.infrastructureParameterSet.get( "numberOfApplicationServers" ), "webDispatcherVmSize": self.infrastructureParameterSet.get( "webDispatcherVmSize" ), }, "installationParameterSets": { "downloadBinaries": { "name": "Download Binaries", "parameters": [ { "name": "technicalCommunicationUser", "value": self.infrastructureParameterSet.get( "technicalCommunicationUser" ), }, { "name": "techUserPassword", "value": self.infrastructureParameterSet.get( "techUserPassword" ), }, { "name": "maintenancePlannerTransaction", "value": self.infrastructureParameterSet.get( "maintenancePlannerTransaction" ), }, ], }, "hanaDeployment": { "name": "HANA Deployment", "parameters": [ { "name": "DBSID", "value": self.installationParameterSets.get( "hanaDeployment" ).get("DBSID"), }, { "name": "DBSIDAdminUserId", "value": self.installationParameterSets.get( "hanaDeployment" ).get("DBSIDAdminUserId"), }, { "name": "instanceNumber", "value": self.installationParameterSets.get( "hanaDeployment" ).get("instanceNumber"), }, { "name": "primaryHanaPhysicalHostname", "value": self.installationParameterSets.get( "hanaDeployment" ).get("primaryHanaPhysicalHostname"), }, { "name": "primaryHanaVirtualHostname", "value": self.installationParameterSets.get( "hanaDeployment" ).get("primaryHanaVirtualHostname"), }, ], }, "s4hanaDeployment": { "name": "S/4HANA Deployment", "parameters": [ { "name": "SID", "value": self.installationParameterSets.get( "s4hanaDeployment" ).get("SID"), }, { "name": "SAPSysAdminUserId", "value": self.installationParameterSets.get( "s4hanaDeployment" ).get("SAPSysAdminUserId"), }, { "name": "SAPSysAdminGroupId", "value": self.installationParameterSets.get( "s4hanaDeployment" ).get("SAPSysAdminGroupId"), }, { "name": "sapGuiDefaultLanguage", "value": self.installationParameterSets.get( "s4hanaDeployment" ).get("sapGuiDefaultLanguage"), }, { "name": "SAPSystemAdditionalLanguages", "value": self.installationParameterSets.get( "s4hanaDeployment" ).get("SAPSystemAdditionalLanguages"), }, { "name": "numberOfDialogWorkProcesses", "value": self.installationParameterSets.get( "s4hanaDeployment" ).get("numberOfDialogWorkProcesses"), }, { "name": "numberOfBatchWorkProcesses", "value": self.installationParameterSets.get( "s4hanaDeployment" ).get("numberOfBatchWorkProcesses"), }, ], }, "centralServicesDeployment": { "name": "ABAP SAP Central Services Deployment", "parameters": [ { "name": "instanceNumber", "value": self.installationParameterSets.get( "centralServicesDeployment" ).get("instanceNumber"), }, { "name": "ABAPMessageServerPort", "value": self.installationParameterSets.get( "centralServicesDeployment" ).get("ABAPMessageServerPort"), }, { "name": "physicalHostname", "value": self.installationParameterSets.get( "centralServicesDeployment" ).get("physicalHostname"), }, { "name": "virtualHostname", "value": self.installationParameterSets.get( "centralServicesDeployment" ).get("virtualHostname"), }, ], }, "primaryApplicationServerDeployment": { "name": "Primary Application Server Deployment", "parameters": [ { "name": "instanceNumber", "value": self.installationParameterSets.get( "primaryApplicationServerDeployment" ).get("instanceNumber"), }, { "name": "physicalHostname", "value": self.installationParameterSets.get( "primaryApplicationServerDeployment" ).get("physicalHostname"), }, { "name": "virtualHostname", "value": self.installationParameterSets.get( "primaryApplicationServerDeployment" ).get("virtualHostname"), }, ], }, "additionalApplicationServersDeployment": self.installationParameterSets.get( "additionalApplicationServersDeployment" ), "webDispatcherDeployment": { "name": "SAP Web Dispatcher and Fiori Configuration", "parameters": [ { "name": "installationType", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("installationType"), } ], }, }, } def get_nonha_provisioning_params(self): params = { "productId": self.input_params.get("productId"), "planTemplateId": self.input_params.get("planTemplateId"), "availabilityScenario": self.input_params.get("availabilityScenario"), "adaptiveDeployment": "false", "dryRun": "false", "infrastructureParameterSet": { ### privateDnsZone or domainName is added ### "deploymentServerSubnet": self.infrastructureParameterSet.get( "deploymentServerSubnet" ), "executionEngineSubnet": self.infrastructureParameterSet.get( "executionEngineSubnet" ), "osUser": self.infrastructureParameterSet.get("remoteOsUser"), "secretStoreId": self.infrastructureParameterSet.get("secretStoreId"), "sshPublicKeySecretName": self.infrastructureParameterSet.get( "sshPublicKeySecretName" ), "sshPrivateKeySecretName": self.infrastructureParameterSet.get( "sshPrivateKeySecretName" ), "deploymentServerResourceGroup": self.infrastructureParameterSet.get( "deploymentServerResourceGroup" ), "parameters": [], }, "deploymentParameterSets": { "downloadUser": { "name": "Download User", "parameters": [ { "name": "technicalCommunicationUser", "value": self.infrastructureParameterSet.get( "technicalCommunicationUser" ), }, { "name": "techUserPassword", "value": self.infrastructureParameterSet.get( "techUserPassword" ), }, ], }, "hanaDeployment": { "name": "HANA Deployment", "parameters": [ { "name": "primaryVmResourceId", "value": self.installationParameterSets.get( "hanaDeployment" ).get("primaryVmResourceId"), }, { "name": "DBSID", "value": self.installationParameterSets.get( "hanaDeployment" ).get("DBSID"), }, { "name": "DBSIDAdminUserId", "value": self.installationParameterSets.get( "hanaDeployment" ).get("DBSIDAdminUserId"), }, { "name": "instanceNumber", "value": self.installationParameterSets.get( "hanaDeployment" ).get("instanceNumber"), }, { "name": "primaryPhysicalHostname", "value": self.installationParameterSets.get( "hanaDeployment" ).get("primaryPhysicalHostname"), }, { "name": "primaryVirtualHostname", "value": self.installationParameterSets.get( "hanaDeployment" ).get("primaryVirtualHostname"), }, ], }, "s4hanaDeployment": { "name": "S/4HANA Deployment", "parameters": [ { "name": "SID", "value": self.installationParameterSets.get( "s4hanaDeployment" ).get("SID"), }, { "name": "SAPSysAdminUserId", "value": self.installationParameterSets.get( "s4hanaDeployment" ).get("SAPSysAdminUserId"), }, { "name": "SAPSysAdminGroupId", "value": self.installationParameterSets.get( "s4hanaDeployment" ).get("SAPSysAdminGroupId"), }, { "name": "sapGuiDefaultLanguage", "value": self.installationParameterSets.get( "s4hanaDeployment" ).get("sapGuiDefaultLanguage"), }, { "name": "SAPSystemAdditionalLanguages", "value": self.installationParameterSets.get( "s4hanaDeployment" ).get("SAPSystemAdditionalLanguages"), }, { "name": "numberOfDialogWorkProcesses", "value": self.installationParameterSets.get( "s4hanaDeployment" ).get("numberOfDialogWorkProcesses"), }, { "name": "numberOfBatchWorkProcesses", "value": self.installationParameterSets.get( "s4hanaDeployment" ).get("numberOfBatchWorkProcesses"), }, ], }, "centralServicesDeployment": { "name": "ABAP SAP Central Services Deployment", "parameters": [ { "name": "vmResourceId", "value": self.installationParameterSets.get( "centralServicesDeployment" ).get("vmResourceId"), }, { "name": "instanceNumber", "value": self.installationParameterSets.get( "centralServicesDeployment" ).get("instanceNumber"), }, { "name": "ABAPMessageServerPort", "value": self.installationParameterSets.get( "centralServicesDeployment" ).get("ABAPMessageServerPort"), }, { "name": "physicalHostname", "value": self.installationParameterSets.get( "centralServicesDeployment" ).get("physicalHostname"), }, { "name": "virtualHostname", "value": self.installationParameterSets.get( "centralServicesDeployment" ).get("virtualHostname"), }, ], }, "fioriConfiguration": { "name": "SAP Fiori Configuration", "parameters": [ { "name": "fioriHostname", "value": self.installationParameterSets.get( "fioriConfiguration" ).get("fioriHostname"), }, { "name": "fioriHostPort", "value": self.installationParameterSets.get( "fioriConfiguration" ).get("fioriHostPort"), }, { "name": "productiveClientNumber", "value": self.installationParameterSets.get( "fioriConfiguration" ).get("productiveClientNumber"), }, { "name": "ossUser", "value": self.installationParameterSets.get( "fioriConfiguration" ).get("ossUser"), }, { "name": "ossUserPassword", "value": self.installationParameterSets.get( "fioriConfiguration" ).get("ossUserPassword"), }, ], }, "webDispatcherDeployment": { "name": "SAP Web Dispatcher Configuration", "parameters": [ { "name": "installationType", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("installationType"), }, { "name": "primaryVmResourceId", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("primaryVmResourceId"), }, { "name": "virtualHostname", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("virtualHostname"), }, { "name": "primaryInstanceNumber", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("primaryInstanceNumber"), }, { "name": "primaryPhysicalHostname", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("primaryPhysicalHostname"), }, { "name": "primaryVirtualHostname", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("primaryVirtualHostname"), }, { "name": "userIdOfSIDAdmin", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("userIdOfSIDAdmin"), }, ], }, }, } self.transform_application_servers() params["deploymentParameterSets"]["applicationServersDeployment"] = ( self.installationParameterSets.get("applicationServersDeployment") ) # Check if privateDnsZone is provided, and add it to infrastructure parameters if true if self.infrastructureParameterSet.get("privateDnsZone") is not None: params["infrastructureParameterSet"]["privateDnsZone"] = ( self.infrastructureParameterSet.get("privateDnsZone") ) # Check if domainName is provided, and add it to infrastructure parameters if true if self.infrastructureParameterSet.get("domainName") is not None: params["infrastructureParameterSet"]["domainName"] = ( self.infrastructureParameterSet.get("domainName") ) if self.infrastructureParameterSet.get("techUserPasswordReference") is not None: new_parameter = { "name": "passwordReference", "value": self.infrastructureParameterSet.get( "techUserPasswordReference" ), } params["deploymentParameterSets"]["downloadUser"]["parameters"].append( new_parameter ) if ( self.installationParameterSets.get("fioriConfiguration").get( "ossUserPasswordReference" ) is not None ): new_parameter = { "name": "ossUserPasswordReference", "value": self.installationParameterSets.get("fioriConfiguration").get( "ossUserPasswordReference" ), } params["deploymentParameterSets"]["fioriConfiguration"][ "parameters" ].append(new_parameter) return params def get_ha_deployment_params(self): return dict( hanaDeployment={ "name": "HANA Deployment", "parameters": [ { "name": "DBSID", "value": self.installationParameterSets.get( "hanaDeployment" ).get("DBSID"), }, { "name": "DBSIDAdminUserId", "value": self.installationParameterSets.get( "hanaDeployment" ).get("DBSIDAdminUserId"), }, { "name": "instanceNumber", "value": self.installationParameterSets.get( "hanaDeployment" ).get("instanceNumber"), }, { "name": "frontendHostname", "value": self.installationParameterSets.get( "hanaDeployment" ).get("frontendHostname"), }, { "name": "primaryHanaPhysicalHostname", "value": self.installationParameterSets.get( "hanaDeployment" ).get("primaryHanaPhysicalHostname"), }, { "name": "primaryHanaVirtualHostname", "value": self.installationParameterSets.get( "hanaDeployment" ).get("primaryHanaVirtualHostname"), }, { "name": "secondaryHanaPhysicalHostname", "value": self.installationParameterSets.get( "hanaDeployment" ).get("secondaryHanaPhysicalHostname"), }, { "name": "secondaryHanaVirtualHostname", "value": self.installationParameterSets.get( "hanaDeployment" ).get("secondaryHanaVirtualHostname"), }, ], }, enqueueReplicationServerDeployment={ "name": "Enqueue Replication Server Deployment", "parameters": [ { "name": "instanceNumber", "value": self.installationParameterSets.get( "enqueueReplicationServerDeployment" ).get("instanceNumber"), }, { "name": "physicalHostname", "value": self.installationParameterSets.get( "enqueueReplicationServerDeployment" ).get("physicalHostname"), }, { "name": "virtualHostname", "value": self.installationParameterSets.get( "enqueueReplicationServerDeployment" ).get("virtualHostname"), }, ], }, ) def get_cluster_deployment_params(self): return dict( clustering={ "name": "Service Principal for High Availability Cluster", "parameters": [ { "name": "clientId", "value": self.installationParameterSets.get("clientId"), }, { "name": "clientSecret", "value": self.installationParameterSets.get("clientSecret"), }, ], } ) def get_webdisp_deployment_standalone_params(self): return dict( parameters=( { "name": "primaryInstanceNumber", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("primaryInstanceNumber"), }, { "name": "primaryPhysicalHostname", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("primaryPhysicalHostname"), }, { "name": "primaryVirtualHostname", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("primaryVirtualHostname"), }, { "name": "userIdOfSIDAdmin", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("userIdOfSIDAdmin"), }, { "name": "fioriHostPort", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("fioriHostPort"), }, { "name": "virtualHostname", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("virtualHostname"), }, ) ) def get_webdisp_ha_deployment_params(self): return dict( parameters=( { "name": "secondaryInstanceNumber", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("secondaryInstanceNumber"), }, { "name": "secondaryPhysicalHostname", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("secondaryPhysicalHostname"), }, { "name": "secondaryVirtualHostname", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("secondaryVirtualHostname"), }, { "name": "fioriHostname", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("fioriHostname"), }, { "name": "fioriHostPort", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("fioriHostPort"), }, { "name": "productiveClientNumber", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("productiveClientNumber"), }, ) ) def get_webdisp_deployment_params(self): return dict( parameters=( { "name": "fioriHostname", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("fioriHostname"), }, { "name": "fioriHostPort", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("fioriHostPort"), }, { "name": "productiveClientNumber", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("productiveClientNumber"), }, ) ) def get_ha_provisioning_params(self): params = dict( hanaDeployment={ "name": "HANA Deployment", "parameters": [ { "name": "primaryVmResourceId", "value": self.installationParameterSets.get( "hanaDeployment" ).get("primaryVmResourceId"), }, { "name": "DBSID", "value": self.installationParameterSets.get( "hanaDeployment" ).get("DBSID"), }, { "name": "DBSIDAdminUserId", "value": self.installationParameterSets.get( "hanaDeployment" ).get("DBSIDAdminUserId"), }, { "name": "instanceNumber", "value": self.installationParameterSets.get( "hanaDeployment" ).get("instanceNumber"), }, { "name": "primaryPhysicalHostname", "value": self.installationParameterSets.get( "hanaDeployment" ).get("primaryPhysicalHostname"), }, { "name": "primaryVirtualHostname", "value": self.installationParameterSets.get( "hanaDeployment" ).get("primaryVirtualHostname"), }, { "name": "secondaryVmResourceId", "value": self.installationParameterSets.get( "hanaDeployment" ).get("secondaryVmResourceId"), }, { "name": "loadBalancerResourceId", "value": self.installationParameterSets.get( "hanaDeployment" ).get("loadBalancerResourceId"), }, { "name": "frontEndIp", "value": self.installationParameterSets.get( "hanaDeployment" ).get("frontEndIp"), }, { "name": "frontendHostname", "value": self.installationParameterSets.get( "hanaDeployment" ).get("frontendHostname"), }, { "name": "secondaryPhysicalHostname", "value": self.installationParameterSets.get( "hanaDeployment" ).get("secondaryPhysicalHostname"), }, { "name": "secondaryVirtualHostname", "value": self.installationParameterSets.get( "hanaDeployment" ).get("secondaryVirtualHostname"), }, ], }, centralServicesDeployment={ "name": "ABAP SAP Central Services Deployment", "parameters": [ { "name": "loadBalancerResourceId", "value": self.installationParameterSets.get( "centralServicesDeployment" ).get("loadBalancerResourceId"), }, { "name": "loadBalancerHostname", "value": self.installationParameterSets.get( "centralServicesDeployment" ).get("loadBalancerHostname"), }, { "name": "frontEndIp", "value": self.installationParameterSets.get( "centralServicesDeployment" ).get("frontEndIp"), }, { "name": "vmResourceId", "value": self.installationParameterSets.get( "centralServicesDeployment" ).get("vmResourceId"), }, { "name": "instanceNumber", "value": self.installationParameterSets.get( "centralServicesDeployment" ).get("instanceNumber"), }, { "name": "ABAPMessageServerPort", "value": self.installationParameterSets.get( "centralServicesDeployment" ).get("ABAPMessageServerPort"), }, { "name": "physicalHostname", "value": self.installationParameterSets.get( "centralServicesDeployment" ).get("physicalHostname"), }, { "name": "virtualHostname", "value": self.installationParameterSets.get( "centralServicesDeployment" ).get("virtualHostname"), }, ], }, enqueueReplicationServerDeployment={ "name": "Enqueue Replication Server Deployment", "parameters": [ { "name": "vmResourceId", "value": self.installationParameterSets.get( "enqueueReplicationServerDeployment" ).get("vmResourceId"), }, { "name": "loadBalancerHostname", "value": self.installationParameterSets.get( "enqueueReplicationServerDeployment" ).get("loadBalancerHostname"), }, { "name": "frontEndIp", "value": self.installationParameterSets.get( "enqueueReplicationServerDeployment" ).get("frontEndIp"), }, { "name": "instanceNumber", "value": self.installationParameterSets.get( "enqueueReplicationServerDeployment" ).get("instanceNumber"), }, { "name": "physicalHostname", "value": self.installationParameterSets.get( "enqueueReplicationServerDeployment" ).get("physicalHostname"), }, { "name": "virtualHostname", "value": self.installationParameterSets.get( "enqueueReplicationServerDeployment" ).get("virtualHostname"), }, ], }, webDispatcherDeployment={ "name": "SAP Web Dispatcher and Fiori Configuration", "parameters": [ { "name": "installationType", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("installationType"), }, { "name": "primaryVmResourceId", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("primaryVmResourceId"), }, { "name": "virtualHostname", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("virtualHostname"), }, { "name": "primaryInstanceNumber", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("primaryInstanceNumber"), }, { "name": "primaryPhysicalHostname", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("primaryPhysicalHostname"), }, { "name": "primaryVirtualHostname", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("primaryVirtualHostname"), }, { "name": "userIdOfSIDAdmin", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("userIdOfSIDAdmin"), }, { "name": "secondaryVmResourceId", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("secondaryVmResourceId"), }, { "name": "loadBalancerResourceId", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("loadBalancerResourceId"), }, { "name": "frontEndIp", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("frontEndIp"), }, { "name": "secondaryInstanceNumber", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("secondaryInstanceNumber"), }, { "name": "secondaryPhysicalHostname", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("secondaryPhysicalHostname"), }, { "name": "secondaryVirtualHostname", "value": self.installationParameterSets.get( "webDispatcherDeployment" ).get("secondaryVirtualHostname"), }, ], }, ) return params def transform_application_servers(self): application_servers = self.installationParameterSets.get( "applicationServersDeployment", [] ) transformed_application_servers = [] for index, server in enumerate(application_servers, start=1): name = f"Application Server {index} Deployment" parameters = [ {"name": "vmResourceId", "value": server.get("vmResourceId", "")}, {"name": "instanceNumber", "value": server.get("instanceNumber", "")}, { "name": "physicalHostname", "value": server.get("physicalHostname", ""), }, {"name": "virtualHostname", "value": server.get("virtualHostname", "")}, ] transformed_application_servers.append( {"name": name, "parameters": parameters} ) self.installationParameterSets["applicationServersDeployment"] = ( transformed_application_servers ) class Connection: def __init__(self, address, outputDir, outputFile): self._address = address.rstrip("/") self._headers = {} self._client = Request() self.logLocation = f"{outputDir}/{outputFile}" def _request(self, method, path, payload=None): headers = self._headers.copy() data = None if payload: data = json.dumps(payload) headers["Content-Type"] = "application/json" url = self._address + path r_data = {} # Initialize r_data to avoid referencing an uninitialized variable try: r = self._client.open(method, url, data=data, headers=headers, timeout=60) r_status = r.getcode() r_headers = dict(r.headers) data = r.read().decode("utf-8") r_data = json.loads(data) if data else {} except HTTPError as e: r_status = e.code r_headers = dict(e.headers) try: r_data = e.read().decode("utf-8") except UnicodeDecodeError: raise AnsibleConnectionFailure(f"HTTPError {r_status}: {r_headers}") raise AnsibleConnectionFailure( f"HTTPError {r_status}: {r_headers} Response {r_data}" ) finally: if isinstance(r_data, str): r_data = json.loads(r_data) file_data = r_data.copy() with open(self.logLocation, "w") as f: if file_data.get("access_token"): file_data.pop("access_token") json.dump(file_data, f, sort_keys=True, indent=4) return r_status, r_headers, r_data def get(self, path): return self._request("GET", path) def post(self, path, payload=None): return self._request("POST", path, payload) def delete(self, path): return self._request("DELETE", path) def get_full_path(self, file_name): absolute_path = os.path.dirname(__file__) relative_path = file_name full_path = os.path.join(absolute_path, relative_path) return full_path def login(self, oauthServerUrl, apiEndpoint): self._address = oauthServerUrl self._client.client_cert, cert_temp_file = self.create_temp_file_from_encrypted( self.get_full_path("cert_file.pem") ) self._client.client_key, key_temp_file = self.create_temp_file_from_encrypted( self.get_full_path("key_file") ) status, headers, data = self.post("") try: if status in [200, 201, 204, 206]: token = data.get("access_token") self._address = apiEndpoint if token is not None: self._headers["Authorization"] = "Bearer " + token else: raise AnsibleActionFail( "Unable to fetch CAL token. Exit code %s" % status ) finally: # Clean up temporary files if self.get_full_path("cert_file.pem"): os.remove(self.get_full_path("cert_file.pem")) if self.get_full_path("key_file"): os.remove(self.get_full_path("key_file")) self._client.client_cert = None self._client.client_key = None def create_temp_file_from_encrypted(self, encrypted_file_path): with open(encrypted_file_path, "rb") as file: encrypted_data = file.read() decrypted_data = fernet.decrypt(encrypted_data).decode() fd, temp_file_path = tempfile.mkstemp() with os.fdopen(fd, "w") as tmp: tmp.write(decrypted_data) return temp_file_path, temp_file_path def decrypt_file(self, file_path): with open(file_path, "rb") as file: encrypted_data = file.read() decrypted_data = fernet.decrypt(encrypted_data).decode() with open(file_path, "w") as file: file.write(decrypted_data) class AzureKeyVaultManager: def __init__(self, vault_url, client_id=None, secret=None, tenant=None): self.vault_url = vault_url self.client_id = client_id self.secret = secret self.tenant = tenant self.token = None self.token_acquired = False self.get_token() def get_token(self): display = Display() token_params = { "api-version": "2018-02-01", "resource": "https://vault.azure.net", } token_headers = {"Metadata": "true"} try: token_res = requests.get( "http://169.254.169.254/metadata/identity/oauth2/token", params=token_params, headers=token_headers, ) token = token_res.json().get("access_token") if token is not None: self.token_acquired = True self.token = token else: display.v("No token was available.") except requests.exceptions.RequestException: display.v( "Try using service principal if provided. Unable to fetch MSI token. " ) self.token_acquired = False def get_secrets(self, secrets): ret = [] if self.vault_url is None: raise AnsibleActionFail("Failed to get a valid vault URL.") if self.token_acquired: secret_params = {"api-version": "2016-10-01"} secret_headers = {"Authorization": "Bearer " + self.token} for secret in secrets: try: secret_res = requests.get( self.vault_url + "/secrets/" + secret, params=secret_params, headers=secret_headers, ) ret.append(secret_res.json()["value"]) except requests.exceptions.RequestException: raise AnsibleActionFail( "Failed to fetch secret: " + secret + " via MSI endpoint." ) except KeyError: raise AnsibleActionFail("Failed to fetch secret " + secret + ".") return ret else: return self.get_secret_non_msi(secrets) def get_secret_non_msi(self, secrets): try: credentials = ServicePrincipalCredentials( client_id=self.client_id, secret=self.secret, tenant=self.tenant ) client = KeyVaultClient(credentials) except AuthenticationError: raise AnsibleActionFail( "Invalid credentials for the subscription provided." ) ret = [] for secret in secrets: try: secret_val = client.get_secret(self.vault_url, secret, "").value ret.append(secret_val) except ClientRequestError: raise AnsibleActionFail("Error occurred in the request") return ret def create_certificates_files(self, client_cert, client_key): script_dir = os.path.dirname(os.path.abspath(__file__)) cert_file_path = os.path.join(script_dir, "cert_file.pem") key_file_path = os.path.join(script_dir, "key_file") # Encrypt and save the certificates self.encrypt_and_save(client_cert, cert_file_path) self.encrypt_and_save(client_key, key_file_path) def encrypt_and_save(self, data, file_path): encrypted_data = fernet.encrypt(data.encode()) with open(file_path, "wb") as file: file.write(encrypted_data) class ActionModule(ActionBase): def __init__(self, *args, **kwargs): super(ActionModule, self).__init__(*args, **kwargs) self._supports_check_mode = False def run(self, tmp=None, task_vars=None): result = super(ActionModule, self).run(tmp, task_vars) # Get parameters from task arguments method = self._task.args.get("method") output_directory = self._task.args.get("outputDirectoryPath", "/tmp") output_file = self._task.args.get("outputFile", "output.txt") azure_arg_mapping = { "calKeyvaultId": "vault_url", "clientId": "client_id", "clientSecret": "secret", "tenantId": "tenant", } # Extract relevant arguments and map them to AzureKeyVaultManager constructor argument names azure_args = { azure_arg_mapping[key]: value for key, value in self._task.args.items() if key in azure_arg_mapping } # Retrieve secrets from Azure Key Vault azure_mngr = AzureKeyVaultManager(**azure_args) api_secrets = azure_mngr.get_secrets( ["apiEndpoint", "clientCertificate", "clientPrivateKey", "oauthServerUrl"] ) apiEndPoint, clientCertificate, clientPrivateKey, oathUrl = api_secrets # Create certificate files azure_mngr.create_certificates_files(clientCertificate, clientPrivateKey) conn = Connection("", output_directory, output_file) if method == "get_product": validation_result, new_module_args = self.validate_argument_spec( method_spec_product, required_together=required_together ) conn.login(oathUrl, apiEndPoint) status, _, data = conn.get("/solutions/v1/products") result.update(status=status, response=str(data)) elif method == "get_progress": validation_result, new_module_args = self.validate_argument_spec( method_spec_progress, required_together=required_together ) conn.login(oathUrl, apiEndPoint) system_id = new_module_args.get("systemId") status, _, data = conn.get( "/workloads/v1/systems/" + system_id + "/provisioningProgress" ) result.update(status=status, response=str(data)) elif method == "deployment": validation_result, new_module_args = self.validate_argument_spec( method_spec_deployment, required_together=required_together ) conn.login(oathUrl, apiEndPoint) status, _, data = conn.get("/solutions/v1/products") if data is not None: products_dict = {p["productId"]: p for p in data.get("products")} product = products_dict.get(new_module_args.get("productId")) product_constraints = [ item for item in product.get("availableProviders") if "Microsoft Azure" in item["name"] ][0] if not product: raise AnsibleActionFail( "Product not found. Choose from the available products' list %s" % products_dict ) method_spec_deployment.get("infrastructureParameterSet").get("options").get( "operatingSystem" ).update({"choices": product_constraints.get("availableOperatingSystems")}) method_spec_deployment.get("infrastructureParameterSet").get("options").get( "hanaVmSize" ).update({"choices": product_constraints.get("availableHanaVmSizes")}) method_spec_deployment.get("infrastructureParameterSet").get("options").get( "centralServicesVmSize" ).update( {"choices": product_constraints.get("availableCentralServicesVmSizes")} ) method_spec_deployment.get("infrastructureParameterSet").get("options").get( "enqueueReplicationServerVmSize" ).update( { "choices": product_constraints.get( "availableEnqueueReplicationServerVmSizes" ) } ) method_spec_deployment.get("infrastructureParameterSet").get("options").get( "applicationServerVmSize" ).update( { "choices": product_constraints.get( "availableApplicationServerVmSizes" ) } ) method_spec_deployment.get("infrastructureParameterSet").get("options").get( "webDispatcherVmSize" ).update( {"choices": product_constraints.get("availableWebDispatcherVmSizes")} ) validation_result, new_module_args = self.validate_argument_spec( method_spec_deployment ) system = SAPsystem(new_module_args) system_request = system.get_props() status, _, data = conn.post( "/workloads/v1/systems/provisioning", payload=system_request ) result.update(status=status, response=str(data)) elif method == "software_provisioning": conn.login(oathUrl, apiEndPoint) validation_result, new_module_args = self.validate_argument_spec( method_spec_provisioning, required_together=required_together ) system = SAPsystem(new_module_args) system_request = system.get_props() status, _, data = conn.post( "/workloads/v1/systems/softwareProvisioning", payload=system_request ) result.update( status=status, response=str(data) ) # Write response to output file result["changed"] = True return result