Notifications are one of the primary tools for a Cloud Management Platform consumer to know what is happening with their deployment. This article will present an alternative method from the out-of-the-box functionality in vRealize Automation in order to improve on the design, content, and architecture of these email notifications. Notifications can be sent via Email, Slack, Teams, and Zoom channels as desired.
This ABX action has been developed/tested with:
- vRA 8.61
- Microsoft Exchange 2019
You can jump down to the Notification Scenario Examples section to view some examples of the format and content.
Let’s start from a good place – the requirements:
Requirements
- Environment agnostic. The personality and environment information contained within the notifications should be abstracted from the ABX code – thus supporting multi-tenancy and different email formats and infrastructure within the boundaries of a vRA Project
- Email infrastructure agnostic – should work with any Enterprise-grade on-prem or cloud-based email service that supports SMTP
- Corporate branding. The default VMware email templates you get when activating notifications through the Service Broker are fine for what they are, but many Enterprises have gone through anti-phishing campaigns and users are trained not to click on suspicious links pointing to services they are not aware of. While placing a corporate logo with a matching colour scheme is not a guarantee that the email is genuine, it does lend some authenticity to the message and users will soon come to recognise the email design and content as coming from their internal cloud management platform (i.e. vRA). It also lends a more professional look.
- Notification content – should contain basic info such as:
- Requestor name
- Deployment name and description
- Deployment components and specifications
- Any approval policy associated
- Price estimate
- Notification Scenarios:
- Deployment completion status
- Successful, failed, aborted etc
- Containing information regarding the generated hostnames/IP addresses for each deployed resource.
- Approval status – when the approval request is rejected either manually by the approver, or by the “Auto expiry decision” in the approval policy. No email will be sent when it is approved – the requestor will get an email upon deployment completion.
- Deployment Lease Expired
- Deployment Deleted
- Deployment Updated
- Deployment completion status
- HTML formatted emails only – who uses plain-text email clients these days?
- Cloud-based App Notification channels – Email is the more standard use-case, but you should be able to choose popular cloud notification channels if desired
- Flexibility of Email connection transport and security
Features
The ABX Flow described here has the following features:
- CSS for formatting of HTML emails – Creating CSS is outside the scope of this document but there are a number of resources on the Internet that can help you such as this one.
- Property group tied to a Project for ABX action constants. This allows different email infrastructure, credentials, corporate logo etc to be set for each vRA Project as desired
- Secure API access via generated access tokens
- ABX Subscriptions triggered by Blueprint IDs and/or internal event topic data
- Leverages the latest vRA API version 2021-07-15
- Flexibility of corporate logo size
- Email server communication transport and security options (smtp session security, TCP port, authenticated etc)
- Estimated expenses information generated and included in notifications
- Support for Time Zone conversion
- Leverages the system-generated action for lease expiry made available in vRA 8.6.1
- Secrets securely stored in vRA per-project
- Cloud notifications via Zoom, Slack, and/or Teams
- Inclusion of custom properties in the notifications
Pre-requisites
- AD service account credential for vRA to authenticate to MS Exchange (sender’s email)
- AD service account credential to communicate to the vRA API (can be the same as above)
- Exchange and API permissions for the service account(s)
- Exchange Receive/Send connectors configured appropriately
- vROPs integration completed if you want price estimates included in the emails
- vRA Lease and Approval Policies defined
- Slack, Teams, and/or Zoom requirements met (as per each platform)
- Slack, Teams, and/or Zoom channels created and users invited
OK, let’s dive into it!
Set-up Tasks
Disable Default vRA Notifications
The basic emails sent by vRA are by default enabled in Service Broker / Content & Policies / Scenarios. You should disable all of these as they are duplicated within this ABX with the exception of Pending Approval Request (Notification sent to approver). I will provide an enhancement to this ABX in the near future for this scenario.

Environment
In order to have a different look and feel for notifications depending on the vRA Project, we need to abstract this the environment configuration from the code. Property Groups and Secrets to the rescue (again)!
Secrets
Create the following secrets in Infrastructure / Secrets:
Project | Name | Value | Description | Example |
Select your Project | smtp_password | The password for your AD service account for vRA to send emails | Exchange account password | P@ssw0rd! |
Select your Project | vra_service_account_password | The password for the AD account used to authenticate to the vRA API and vCenter | vRA Service Account password | Sup3rs3cret! |
Select your Project | zoom_token | The token to authenticate to Zoom | Zoom API access token | aBcDeFG12345… |
Note: After you create a channel, the first message you receive from Zoom will include the endpoint uRL and the Zoom verification token Of course, you only need to do this if you want to use a Zoom notification channel
You will be able to select these secrets when creating each property by selecting Secret instead of Constant Value – more on this below.
Property Group
A Property Group should be defined for each unique environment with the following constants:
- Logo in base64
- Time Zone information
- Cloud Platform name (you may want to call it different things for each tenant)
- Email server target
- Email communications security (SSL, STARTTLS etc) and transport
- Email server credentials
- Zoom, Slack, and Teams webhooks
- Custom Properties you would like included in the notification
Create the Property Group as follows:
- Navigate to Cloud Assembly / Design / Property Groups and select New Property Group
- Change the PG type to Constant Values
- Give it a name, Display Name, and Description
- Select the Project. This is important as some properties will link to the secrets that can only be defined at the Project level
Note: You will not be able to rename the Property Group or change the Scope after it has been created.

Property: Logo
- Prepare your logo by converting it to Base64.
- Go to this site (or another of your choice) and upload your image. https://codebeautify.org/image-to-base64-converter
Note: Due to restrictions in vRA as to the string length in a property, the maximum file size should not exceed 12 KB before encoding - Copy the resultant Base64 string to your clipboard
- Go to this site (or another of your choice) and upload your image. https://codebeautify.org/image-to-base64-converter
- In vRA, create a new property named logo with type String
- Paste the copied Base64 string as the Constant value and select Create

Properties: Logo size
Create new string-type properties to define the size of the logo you want in your email. This will take some trial and error – I found that 250 x 110 works well for my logo.
- logo_company_width_pixels – the width in pixels for the logo – whatever looks good for you.
- logo_company_height_pixels – as above
Property: Platform Name
Create a new string for the property platform_name representing the internal brand name of your vRA service – e.g. “My Company Provisioning Portal” or some such

Properties: Email Infrastructure:
Create properties for the Email server:
Property Name | Type | Select Type | Value | Example |
smtp_server | String | Constant Value | The FQDN of your Exchange server | mail.mycompany.com |
smtp_user | String | Constant Value | The Exchange service account in UPN format | svc_vra@mycompany.com |
smtp_password | String | Secret | Password for the Exchange service account | Chosen from a list of secrets for the Project |
smtp_port | String | Constant Value | The TCP port | 587 |
smtp_connection_security | String | Constant Value | Vales are none, SSL, or starttls | starttls |
smtp_authenticated | Boolean | Constant Value | True or False | Ticked for True |
sender_email | String | Constant Value | The email address of the service account used to send the email | svc_vra@mycompany.com |
Property: Time Zone
Create the property for conversion of time values in emails. I added this because my vRA server gave me times in UTC and my local time zone was GMT +4
- timeZone – Value from the TZ database time zones e.g. Asia/Dubai
Property: Custom Properties
Every notification will have a base set of information like deployment name, description, price estimate etc, but I wanted some more flexibility to add additional fields in the notification for some of my custom properties – for example the environment, the application name etc. In order to achieve this, the ABX action grabs these custom property names from a property group and includes them and their values in the notifications.
- Create a new property named custom_property_display
- Change the type to Array
- Add a list of custom blueprint properties that you would like to include in the notification. Each property must be prepended by a dash

Note: If you do not have any custom properties in your blueprint, you do not need to create the above in the property group – the ABX script will not fail if the property is not present.
Properties: Cloud Notification Channels
Create the following properties if you are using any of these services:
Property Name | Type | Select Type | Value | Example |
zoom_notifications | Boolean | Constant Value | True or False | Ticked for True if you want to use Zoom. If False, you do not need to create the webhook property |
slack_notifications | Boolean | Constant Value | True or False | Ticked for True if you want to use Slack. If False, you do not need to create the webhook property |
teams_notifications | Boolean | Constant Value | True or False | Ticked for True if you want to use Teams. If False, you do not need to create the webhook property |
zoom_webhook | String | Constant Value | From Zoom | https://inbots.zoom.us/incoming/hook/abc123… |
slack_webhook | String | Constant Value | From Slack | https://hooks.slack.com/services/abc123… |
teams_webhook | String | Constant Value | From Teams | https://creeacompany.webhook.office.com/webhookb2/abc123… |
Instructions on how to create Channels and Webhooks need to be obtained from the respective provider – see the pre-requisites section above.
Finally, select Create again to create the new Property Group. The final property list should have 20 items and look something like this:

Property Group Association with a Project
In order to have the ability to select different email infrastructure and company branding etc, we will link the property group to a Project and query these values from the ABX action scripts.
- Navigate to Infrastructure / Projects and open your Project
- Select the Provisioning tab and scroll to Custom Properties
- Create a new property named propertyGroup and the enter the name of the Property Group created previously.
- Save the Project

Now it is time for the ABX Actions…
ABX Flow
There are four types of extensibility actions in vRA – Flow, Rest request, Rest poll, and Script. Many of us are more familiar with the polyglot Script type but for this example we are also using Flow to orchestrate different smaller Script actions together. First we need to create the script actions…
ABX Script Action: Get vRA Token
- Navigate to Extensibility / Actions and select New Action
- Fill out the Name, Description, and Project and select Next

Select the script language as Python and copy the code below
# ABX Action to get refresh and bearer tokens as part of ABX Flow: Ultimate Notifications
# Created by Guillermo Martinez and Dennis Gerolymatos
# Version 1.1 - 22.12.2021
import requests # query the API
import json # API query responses to json.
def handler(context, inputs):
## get variables ##
vraUserName=context.getSecret(inputs["vra_service_account"]) # vRAuserName from inputs
vraPassword=context.getSecret(inputs["vra_service_account_password"]) # vRApassword from the inputs
vraUserName=vraUserName.split("@") # Removing the @domain part of username.
vraUrl=inputs["vra_fqdn"] # vRA url
#get a refresh token.
print('Generating Refresh Token...')
body= {
"username": vraUserName[0],
"password": vraPassword
}
headers={'Content-Type': 'application/json','accept': 'application/json'}
response=requests.post('https://' + vraUrl + '/csp/gateway/am/api/login?access_token', headers=headers, data=json.dumps(body), verify=False)
if response.status_code==200:
vraRefreshToken=response.json()['refresh_token']
else:
print('[?] Unexpected Error: [HTTP {0}]: Content: {1}'.format(response.status_code, response.content))
# get a bearer token.
print('Generating Bearer Token...')
body={
"refreshToken": vraRefreshToken
}
response_bearerToken=requests.post('https://' + vraUrl + '/iaas/api/login', data=json.dumps(body), verify=False)
if response_bearerToken.status_code==200:
vraBearerToken=response_bearerToken.json()['token']
bearer="Bearer "
bearerToken=bearer + vraBearerToken
else:
print('[?] Unexpected Error: [HTTP {0}]: Content: {1}'.format(response_bearerToken.status_code, response_bearerToken.content))
outputs={}
outputs['bearerToken']=bearerToken
return outputs
- Create the following Default inputs:
- Action Constant – vra_service_account
- Action Constant – vra_fqdn
- Secret – vra_service_account_password
- Add the dependency requests
- Create the ABX Action

ABX Script Action: Send to Email
- Similar to the above, create a new action named Send to Email and copy the following code
# ABX Action to send to email as part of ABX Flow: Ultimate Notifications
# Created by Guillermo Martinez and Dennis Gerolymatos
# Version 1.4 - 22.12.2021
import json # API query responses to json.
import requests # query the API
import smtplib # send email
import sys # exit the script
import pytz # time zone
import ssl # SSL email
from email.mime.text import MIMEText # mime objects on Email
from email.mime.multipart import MIMEMultipart # emails with HTML content
from json2table import convert # diccionaries to html
from datetime import datetime # current time
def handler(context, inputs):
# VARIABLES
global apiVersion; apiVersion="2021-07-15" # tested with version 2021-07-15
# Creates a dictionary with all neccesary data from VRa API and context inputs
depInfoAndRes=create_dictionary(inputs)
# calls generate_html function to populate the HTML body
html=generate_html(inputs,depInfoAndRes)
# calls the function for sending the email.
send_email(context,inputs,html,depInfoAndRes)
outputs={}
outputs['depInfoAndRes']=depInfoAndRes
outputs['messageSubject']=depInfoAndRes['status']+" - Status of deployment "+depInfoAndRes["name"]+" by "+depInfoAndRes['proGrpContent']['platform_name']['const'] # Subject for the notification
return outputs
# Gets inputs from the VRa API and the deployment context and build a Dictionary
def create_dictionary(inputs):
# VARIABLES
global apiVersion
orgId=inputs["orgId"] # gets organization ID from the inputs
depInfoAndRes={}# Declaring the main dictionary
bearer=inputs['bearerToken'] #Before querying the API, a bearer token needs to be obtained.
projectId=inputs["projectId"] # reads the project ID from the inputs
deploymentId=inputs['deploymentId'] # deployment ID from the inputs
userName=inputs["userName"] # username from the context inputs
vraUrl=inputs["vra_fqdn"] # vRA url
eventType=inputs["eventType"] if "eventType" in inputs else "EXPIRE_NOTIFICATION" # evenType from the context inputs
eventTopicId=inputs["__metadata"]["eventTopicId"] # event topic ID from the context inputs
# defining a common header for all the subsequent API queries.
headers={"Accept":"application/json","Content-Type":"application/json", "Authorization":bearer}
# test vRA API Connection
print("Testing vRA API Connection...")
apiAbout=requests.get('https://' + vraUrl + '/project-service/api/about?apiVersion='+apiVersion, data='', headers=headers, verify=False)
if apiAbout.status_code==200:
print("Connection to vRA tested succesfully...")
else:
print('[?] Unexpected Error: [HTTP {0}]: Content: {1}'.format(apiAbout.status_code, apiAbout.content))
sys.exit("Error: Connection to vRA API was not made succesfully")
# Getting inputs from property group by querying the API
print('Querying API to get property group name...')
projectInfoJson=requests.get('https://' + vraUrl + '/project-service/api/projects/'+projectId+'?apiVersion='+apiVersion , data='', headers=headers, verify=False)
propGrp=projectInfoJson.json()['properties']['propertyGroup']
print('Getting inputs from property group...')
propGrpInpJson=requests.get('https://' + vraUrl + '/properties/api/property-groups/?apiVersion='+apiVersion+'&name=' + propGrp , data='', headers=headers, verify=False)
proGrpInp=propGrpInpJson.json()
# Adding all property group variables to the dictionary
depInfoAndRes['proGrpContent']=proGrpInp["content"][0]['properties']
# Time Zone settings #
localTZ=pytz.timezone(depInfoAndRes['proGrpContent']['timeZone']['const'])
# Discovering Deployment info and resources by querying the API
print('Discovering deployment info and resources...')
deploymentInfoJson=requests.get('https://' + vraUrl + '/deployment/api/deployments/' + deploymentId + '?apiVersion='+apiVersion+'&deleted=true&expand=project&expand=resources', data='', headers=headers, verify=False)
depInfo=deploymentInfoJson.json()
# Date and Time Formating and Time Zone Convertion
createdAtConverted=depInfo['createdAt'].replace("T"," ").replace("Z","").split(".")
createdAtConverted=datetime.strptime(createdAtConverted[0],"%Y-%m-%d %H:%M:%S").astimezone(localTZ).strftime("%Y-%m-%d %H:%M:%S")
lastUpdatedConverted=depInfo['lastUpdatedAt'].replace("T"," ").replace("Z","").split(".")
lastUpdatedConverted=datetime.strptime(lastUpdatedConverted[0],"%Y-%m-%d %H:%M:%S").astimezone(localTZ).strftime("%Y-%m-%d %H:%M:%S")
if "leaseExpireAt" in depInfo:
leaseExpireConverted=depInfo['leaseExpireAt'].replace("T"," ").replace("Z","").split(".")
leaseExpireConverted=datetime.strptime(leaseExpireConverted[0],"%Y-%m-%d %H:%M:%S").astimezone(localTZ).strftime("%Y-%m-%d %H:%M:%S")
else:
leaseExpireConverted=""
# Populate main dictionary with more data
depInfoAndRes["name"]=depInfo['name'] if "name" in depInfo else " "
depInfoAndRes["description"]=depInfo['description'] if "description" in depInfo else " "
depInfoAndRes["id"]=depInfo['id'] if "id" in depInfo else " "
depInfoAndRes["status"]=depInfo['status'] if "status" in depInfo else " "
depInfoAndRes["createdAt"]=createdAtConverted
depInfoAndRes["leaseExpireAt"]=leaseExpireConverted
depInfoAndRes["createdBy"]=depInfo['createdBy'] if "createdBy" in depInfo else " "
depInfoAndRes["ownedBy"]=depInfo['ownedBy'] if "ownedBy" in depInfo else " "
depInfoAndRes["lastUpdatedAt"]=lastUpdatedConverted
depInfoAndRes["projectName"]=depInfo['project']['name'] if "name" in depInfo['project'] else " "
depInfoAndRes["lastUpdatedBy"]=depInfo['lastUpdatedBy'] if "lastUpdatedBy" in depInfo else " "
#Loop through all resources in the deployment and create a nested dictionary with the resources details.
i=0
resDetails={}
depResources=depInfo["resources"]
while i < len(depResources):
# Date and Time Formating and Time Zone Convertion
createdAtConverted=depResources[i]["createdAt"].replace("T"," ").replace("Z","").split(".") # date from the API, unrecognized characters are removed.
createdAtConverted=datetime.strptime(createdAtConverted[0],"%Y-%m-%d %H:%M:%S").astimezone(localTZ).strftime("%Y-%m-%d %H:%M:%S") # date from the API is converted to time object and to local time zone
resourceName=depResources[i]["name"] if depResources[i]["type"]=="Cloud.NSX.Network" else depResources[i]["properties"]["resourceName"]
resDetails[resourceName]={
"Name": resourceName,
"Type": depResources[i]["type"],
"State": depResources[i]["state"],
"started At": createdAtConverted
}
#if resouce type is Cloud.vSphere.Machine, query the API for additional resource details.
if depResources[i]["type"]=="Cloud.vSphere.Machine":
resourceId=depResources[i]["id"]
VMDetailsJson=requests.get('https://' + vraUrl + '/deployment/api/resources/' + resourceId + '?apiVersion='+apiVersion+'', data='', headers=headers, verify=False)
VMDetails=VMDetailsJson.json()
VMDetailsProperties=VMDetails["properties"]
resDetails[resourceName]["IP Address"]=VMDetailsProperties["address"] if "address" in VMDetailsProperties else ""
resDetails[resourceName]["CPU count"]= VMDetailsProperties["cpuCount"] if "cpuCount" in VMDetailsProperties else ""
resDetails[resourceName]["Total Memory MB"]= VMDetailsProperties["totalMemoryMB"] if "totalMemoryMB" in VMDetailsProperties else ""
resDetails[resourceName]["Operating System"]= VMDetailsProperties["softwareName"] if "softwareName" in VMDetailsProperties else ""
#Loop through all disks and add them to the dictionary.
if "disks" in VMDetailsProperties["storage"]:
j=0
while j < len(VMDetailsProperties["storage"]["disks"]):
resDetails[resourceName]["disk "+str(j)]={
"Name":VMDetailsProperties["storage"]["disks"][j]["name"],
"Type":VMDetailsProperties["storage"]["disks"][j]["type"],
"Capacity GB":VMDetailsProperties["storage"]["disks"][j]["capacityGb"]
}
j+=1
i+=1
#adds an aditional entry to the dictionary with the resource details.
depInfoAndRes["Resources"]=resDetails
# Getting details about the request and adding them to the dictionary.
requestInfo=requests.get('https://' + vraUrl + '/deployment/api/requests/'+inputs["id"]+'?apiVersion='+apiVersion, data='', headers=headers, verify=False)
requestInfoJson=requestInfo.json()
# Checking if approval is required.
if eventType=="CREATE_DEPLOYMENT" and eventTopicId=="deployment.request.pre":
print("Checking if approval is required...")
while int(requestInfoJson["completedTasks"]) < 4:
if requestInfoJson["status"]=="APPROVAL_PENDING":
depInfoAndRes['status']="APPROVAL_PENDING"
print("Approval is required...")
break
requestInfo=requests.get('https://' + vraUrl + '/deployment/api/requests/'+inputs["id"]+'?apiVersion='+apiVersion, data='', headers=headers, verify=False)
requestInfoJson=requestInfo.json()
depInfoAndRes['requestDetails']=requestInfoJson["details"] if (requestInfoJson["details"]!="") else "No additional details."
depInfoAndRes['requestStatus']=requestInfoJson["status"] if "status" in requestInfoJson else "No Status"
# setting variables in case the lease has expired.
if userName=="system-user" and inputs['actionName']=="Expire":
userName=depInfoAndRes['createdBy']
depInfoAndRes['status']="LEASE_EXPIRED"
# Discovering Requestor's Email and First Name by querying the API
print("Discovering Requestor's Email...")
userId=inputs['userId'].split(":")[1]
response_Email=requests.get('https://' + vraUrl + '/csp/gateway/am/api/users/' + userId + '/orgs/' + orgId + '/info?apiVersion='+apiVersion, data='', headers=headers, verify=False)
depInfoAndRes['requestorEmail']=response_Email.json()['user']['email'] # gets the email from the user who launched the deployment.
depInfoAndRes['requestorFirstName']=response_Email.json()['user']['firstName'] # gets the first name from the user who launched the deployment.
return depInfoAndRes
# Format HTML body of the email.
def generate_html(inputs,depInfoAndRes):
#VARIABLES
global apiVersion
localTZ=pytz.timezone(depInfoAndRes['proGrpContent']['timeZone']['const']) # Time Zone settings
bearer=inputs['bearerToken'] #Before querying the API, a bearer token needs to be obtained.
bulkRequestCount="1" # for expenses simulation
deploymentId=inputs['deploymentId'] # deployment ID from the inputs
vraUrl=inputs["vra_fqdn"] # vRA url
eventType=inputs["eventType"] if "eventType" in inputs else "EXPIRE_NOTIFICATION" # evenType from the context inputs
eventTopicId=inputs["__metadata"]["eventTopicId"] # event topic ID from the context inputs
headers={"Accept":"application/json","Content-Type":"application/json", "Authorization":bearer} # defining a common header for all the subsequent API queries.
logoWidth =depInfoAndRes['proGrpContent']['logo_company_width_pixels']['const'] if 'logo_company_width_pixels' in depInfoAndRes['proGrpContent'] else " " # defines the width size of the logo in pixels.
logoHeight=depInfoAndRes['proGrpContent']['logo_company_height_pixels']['const'] if 'logo_company_height_pixels' in depInfoAndRes['proGrpContent'] else " " # defines the heights size of the logo in pixelso.
logoCompany=depInfoAndRes['proGrpContent']['logo']['const'] if 'logo' in depInfoAndRes['proGrpContent'] else " " # gets the string corresponding to the base64 encoded JPG logo.
userNameFirstName=depInfoAndRes['requestorFirstName'] # Requestors First Name
dateAndTime=datetime.now().astimezone(localTZ).strftime("%Y-%m-%d %H:%M:%S") # gets current date and time, applies a format and convert to local time zone
build_direction="LEFT_TO_RIGHT" # Neccesary for the convert dictionary to HTML function
# applies the same style to all tables, rows and cells on the HTML body in all Event Types
htmlStyle = f'''
<style>
table {{
width: 100%;
border: 1px solid black;
border-radius: 20px;
}}
td {{
text-align: left;
padding: 8px;
border: 1px solid black;
background-color: #F7F9F9;
border-radius: 10px;
}}
th {{
text-align: left;
padding: 8px;
border: 1px solid black;
background-color: #EBF5FB;
border-radius: 10px;
}}
tr {{
background-color: #FDFEFE;
}}
.container {{
width: {logoWidth}px;
height: {logoHeight}px;
}}
img {{
width: 100%;
height: 100%;
object-fit: cover;
}}
</style style="width:100%">
'''
# uses the global variable logoCompany for adding the logo to the HTML body
logo=f'''\
<div class="container">
<img src="data:image/png;base64, {logoCompany}" alt="Image" />
</div>
'''
mailFooter=f'''\
<table>
<tr>
<td>
<a href="https://{vraUrl}/automation-ui/#/deployment-ui;ash=%2Fworkload%2Fdeployment%2F{deploymentId}">Click here to see your request</a>
</td>
</tr>
</table>
'''
# write the HTML Template for each Event Type
if eventType=="CREATE_DEPLOYMENT" and eventTopicId=="deployment.request.pre": # The deployment has just started
if depInfoAndRes['status']=="APPROVAL_PENDING":
mailHeader=f'''\
<p>Your request for deployment <strong>{depInfoAndRes["name"]}</strong>
is pending for approval.</p>
<table>
<tr>
<td>
<h2><strong>Deployment Information:</strong></h2>
<ul>
<li> Deployment name: <strong> {depInfoAndRes["name"]} </strong></li>
<li> Deployment Description: <strong> {depInfoAndRes["description"]} </strong></li>
<li> Deployment started at: <strong>{depInfoAndRes["createdAt"]}</strong></li>
<li> Deployment status: <strong> {depInfoAndRes['status']}</strong></li>
<li> Deployment details: <strong> {depInfoAndRes['requestDetails']}</strong></li>
</ul>
</td>
</tr>
</table>
'''
else:
mailHeader=f'''\
<p>Your request for deployment <strong>{depInfoAndRes["name"]}</strong>
has been received and is in progress.</p>
<table>
<tr>
<td>
<h2><strong>Deployment Information:</strong></h2>
<ul>
<li> Deployment name: <strong> {depInfoAndRes["name"]} </strong></li>
<li> Deployment Description: <strong> {depInfoAndRes["description"]} </strong></li>
<li> Deployment started at: <strong>{depInfoAndRes["createdAt"]}</strong></li>
<li> Deployment status: <strong> {depInfoAndRes["status"]}</strong></li>
<li> Deployment details: <strong> {depInfoAndRes['requestDetails']}</strong></li>
</ul>
</td>
</tr>
</table>
'''
# Getting only basic data from the input
reqInputs=inputs['requestInputs']
reqInputsCleanedUp={
"Node Size": reqInputs['nodeSize'] if 'nodeSize' in reqInputs else "",
"Node count": reqInputs['nodeCount'] if 'nodeCount' in reqInputs else "",
"Target Network": reqInputs['targetNetwork'] if 'targetNetwork' in reqInputs else "",
"Operating System": reqInputs['operatingSystem'].split(",")[0] if 'operatingSystem' in reqInputs else ""
}
if 'custom_property_display' in depInfoAndRes['proGrpContent']:
x=0
while x < len(depInfoAndRes['proGrpContent']['custom_property_display']['const']):
reqInputsCleanedUp[depInfoAndRes['proGrpContent']['custom_property_display']['const'][x]]=reqInputs[depInfoAndRes['proGrpContent']['custom_property_display']['const'][x]] if depInfoAndRes['proGrpContent']['custom_property_display']['const'][x] in reqInputs else " "
x+=1
# If the deployment has started from CATALOG, calculate up front daily prices
if inputs['requestType']=="CATALOG":
body= {
"bulkRequestCount": bulkRequestCount,
"deploymentName": depInfoAndRes["name"]+" - Daily Price Estimate",
"inputs": reqInputs,
"projectId": inputs['projectId'],
"version": inputs['catalogItemVersion']
}
requestUpfrontCost=requests.post('https://' + vraUrl + '/catalog/api/items/'+inputs['catalogItemId']+'/upfront-prices/?apiVersion='+apiVersion, headers=headers, data=json.dumps(body), verify=False)
statusUpfrontPrice=""
while statusUpfrontPrice != "SUCCESS":
upFrontInfo=requests.get('https://' + vraUrl + '/catalog/api/items/'+inputs['catalogItemId']+'/upfront-prices/'+requestUpfrontCost.json()['upfrontPriceId']+'?apiVersion='+apiVersion, data='', headers=headers, verify=False)
statusUpfrontPrice=upFrontInfo.json()["status"]
integ,decim=str(upFrontInfo.json()["dailyTotalPrice"]).split(".")
reqInputsCleanedUp["Daily Price Estimate"]= "AED "+integ+"."+decim[0:2]
html_resources=convert(reqInputsCleanedUp, build_direction=build_direction) #converts inputs to HTML
#Building the HTML body.
html=f"""\
<html>
<body>
{htmlStyle}
{logo}
<br>
<p><strong>Date and Time:</strong> {dateAndTime}<br></p>
<p>Hello <strong> {depInfoAndRes['requestorFirstName']},</strong></p>
{mailHeader}
<table>
<tr>
<td>
<h2><strong>Requested Resources:</strong></h2>
{html_resources}
</tr>
</td>
</table>
{mailFooter}
</body>
</html>
"""
elif (eventType=="CREATE_DEPLOYMENT" or eventType=="UPDATE_DEPLOYMENT") and eventTopicId=="deployment.request.post": # The deployment has finished or has been updated
html_resources=convert(depInfoAndRes["Resources"], build_direction=build_direction)
# Building the HTML body.
# checking if request Failed
if (depInfoAndRes["status"])=="CREATE_FAILED":
depInfoAndRes['status']==depInfoAndRes['requestStatus']
html=f"""\
<html>
<body>
{htmlStyle}
{logo}
<br>
<p><strong>Date and Time:</strong> {dateAndTime}<br></p>
<p>Hello <strong> {depInfoAndRes['requestorFirstName']},</strong></p>
<p>Your request for deployment <strong>{depInfoAndRes["name"]}</strong>
has failed.</p>
<table>
<tr>
<td>
<h2><strong>Deployment Information:</strong></h2>
<ul>
<li> Deployment name: <strong> {depInfoAndRes["name"]} </strong></li>
<li> Deployment Description: <strong> {depInfoAndRes["description"]} </strong></li>
<li> Deployment started at: <strong>{depInfoAndRes["createdAt"]}</strong></li>
<li> Deployment finished at: <strong>{dateAndTime}</strong></li>
<li> Deployment status: <strong> {depInfoAndRes["status"]}</strong></li>
<li> Request details: <strong> {depInfoAndRes['requestDetails']}</strong></li>
</ul>
</td>
</tr>
</table>
<table>
<tr>
<td>
</tr>
</td>
</table>
{mailFooter}
</body>
</html>
"""
else:
html=f"""\
<html>
<body>
{htmlStyle}
{logo}
<br>
<p><strong>Date and Time:</strong> {dateAndTime}<br></p>
<p>Hello <strong> {depInfoAndRes['requestorFirstName']},</strong></p>
<p>Your request for deployment <strong>{depInfoAndRes["name"]}</strong>
has been completed.</p>
<table>
<tr>
<td>
<h2><strong>Deployment Information:</strong></h2>
<ul>
<li> Deployment name: <strong> {depInfoAndRes["name"]} </strong></li>
<li> Deployment Description: <strong> {depInfoAndRes["description"]} </strong></li>
<li> Deployment started at: <strong>{depInfoAndRes["createdAt"]}</strong></li>
<li> Deployment finished at: <strong>{dateAndTime}</strong></li>
<li> Deployment lease expires: <strong>{depInfoAndRes["leaseExpireAt"]}</strong></li>
<li> Deployment status: <strong> {depInfoAndRes["status"]}</strong></li>
<li> Request details: <strong> {depInfoAndRes['requestDetails']}</strong></li>
</ul>
</td>
</tr>
</table>
<table>
<tr>
<td>
<h2><strong>Resources Details:</strong></h2>
{html_resources}
</tr>
</td>
</table>
{mailFooter}
</body>
</html>
"""
elif eventType=="DESTROY_DEPLOYMENT" and eventTopicId=="deployment.request.post": #The deployment has been deleted.
# Building the HTML body.
html=f"""\
<html>
<body>
{htmlStyle}
{logo}
<br>
<p><strong>Date and Time:</strong> {dateAndTime}<br></p>
<p>Hello <strong> {depInfoAndRes['requestorFirstName']},</strong></p>
<p>Your request to delete the deployment <strong>{depInfoAndRes["name"]}</strong>
has been completed.</p>
<table>
<tr>
<td>
<h2><strong>Deployment Information:</strong></h2>
<ul>
<li> Deployment name: <strong> {depInfoAndRes["name"]} </strong></li>
<li> Deployment Description: <strong> {depInfoAndRes["description"]} </strong></li>
<li> Deployment created at: <strong>{depInfoAndRes["createdAt"]}</strong></li>
<li> Deployment deleted at: <strong>{dateAndTime}</strong></li>
<li> Deployment status: <strong> {depInfoAndRes["status"]}</strong></li>
</ul>
</td>
</tr>
</table>
</body>
</html>
"""
elif eventType=="EXPIRE_NOTIFICATION" and eventTopicId=="deployment.action.pre": #The deployment has expired.
print("Your deployment has expired")
html=f"""\
<html>
<body>
{htmlStyle}
{logo}
<br>
<p><strong>Date and Time:</strong> {dateAndTime}<br></p>
<p>Hello <strong> {depInfoAndRes['requestorFirstName']},</strong></p>
<p>Your deployment <strong>{depInfoAndRes["name"]}</strong>
has expired.</p>
<table>
<tr>
<td>
<h2><strong>Deployment Information:</strong></h2>
<ul>
<li> Deployment name: <strong> {depInfoAndRes["name"]} </strong></li>
<li> Deployment Description: <strong> {depInfoAndRes["description"]} </strong></li>
<li> Deployment created at: <strong>{depInfoAndRes["createdAt"]}</strong></li>
<li> Deployment lease expires: <strong>{depInfoAndRes["leaseExpireAt"]}</strong></li>
</ul>
</td>
</tr>
</table>
</body>
</html>
"""
else:
sys.exit("Error: Unrecognized event type!")
return html
# sends the email notification
def send_email(context,inputs,html,depInfoAndRes):
# Variables #
smtp_port=depInfoAndRes['proGrpContent']['smtp_port']['const'] # smtp_port
smtp_server=depInfoAndRes['proGrpContent']['smtp_server']['const'] # FQDN for SMTP
smtp_user=depInfoAndRes['proGrpContent']['smtp_user']['const'] # Login to access SMTP Server
smtp_password=context.getSecret(inputs["smtp_password"]) # gets password from the secrets
sender_email=depInfoAndRes['proGrpContent']['sender_email']['const'] # Email Address for Sender, make sure the Display Name in the account is a friendly name.
platform_name=depInfoAndRes['proGrpContent']['platform_name']['const'] # To be included in the subject of the email.
smtp_auth_enabled=depInfoAndRes['proGrpContent']['smtp_authenticated']['const'] # Is smtp authentication needed
smtp_security=depInfoAndRes['proGrpContent']['smtp_connection_security']['const'] # Configure to any of these options (SSL, starttls, none)
myEmail=depInfoAndRes['requestorEmail'] # Requestor's email
print("sending an email to: "+myEmail)
# Send Email Notification #
messageSubject=depInfoAndRes['status']+" - Status of deployment "+depInfoAndRes["name"]+" by "+depInfoAndRes['proGrpContent']['platform_name']['const'] # Subject of the email
message=MIMEMultipart("alternative")
message["Subject"]=messageSubject
message["From"]=sender_email # Sender email address
message["To"]=myEmail # Recipient email address
# attach the HTML MIME object to the MIMEMultipart message
part1=MIMEText(html, "html")
message.attach(part1)
# send email message
try:
context=ssl._create_unverified_context()
if smtp_security=="SSL":
print("SSL security")
with smtplib.SMTP_SSL(smtp_server, smtp_port, context=context) as server:
if smtp_auth_enabled:
print("authentication enabled")
server.login(smtp_user, smtp_password)
server.sendmail(sender_email, myEmail, message.as_string())
server.close()
if smtp_security=="starttls":
print("starttls security")
with smtplib.SMTP(smtp_server, smtp_port) as server:
server.starttls(context=context)
if smtp_auth_enabled:
print("authentication enabled")
server.login(smtp_user, smtp_password)
server.sendmail(sender_email, myEmail, message.as_string())
server.close()
else:
print("non SSL , non starttls security")
with smtplib.SMTP(smtp_server, smtp_port) as server:
if smtp_auth_enabled:
print("authentication enabled")
server.login(smtp_user, smtp_password)
server.sendmail(sender_email, myEmail, message.as_string())
server.close()
except (gaierror, ConnectionRefusedError):
print('Failed to connect to the server. Bad connection settings?')
except smtplib.SMTPServerDisconnected:
print('Failed to connect to the server. Wrong user/password?')
except smtplib.SMTPException as e:
print('SMTP error occurred: ' + str(e))
except smtplib.SMTPAuthenticationError as e:
print('SMTP Authentication error: ' + str(e))
except smtplib.SMTPSenderRefused as e:
print('Sender address refused: ' + str(e))
except smtplib.SMTPRecipientsRefused as e:
print('Recipient addresses refused: ' + str(e))
- Create the following Default inputs:
- Action Constant – vra_fqdn
- Secret – smtp_password
- Add the dependencies:
- requests
- json2table
- pytz

ABX Script Action: Send to App Webhook
- Create a new action named Send to App Webhook andcopy the following code
# ABX Action to send to App Webhooks as part of ABX Flow: Ultimate Notifications
# Created by Guillermo Martinez and Dennis Gerolymatos
# Version 1.2 - 22.12.2021
import requests # query the API
import json # API query responses to json.
def handler(context, inputs):
# Variables
zoom_token=context.getSecret(inputs["zoom_token"])
# Sends Slack Notifications.
if inputs['depInfoAndRes']['proGrpContent']['slack_notifications']['const']:
body= {
"username": "VMwareCodeBot",
"color": "Blue",
"type": "home",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": f"{inputs['messageSubject']}"
}
},
{"type": "divider"},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": f"*Deployment Name:*\n{inputs['depInfoAndRes']['name']}"
},
{
"type": "mrkdwn",
"text": f"*Request Description:*\n{inputs['depInfoAndRes']['description']}."
}
]
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": f"*Creation Date: *\n{inputs['depInfoAndRes']['createdAt']}"
},
{
"type": "mrkdwn",
"text": f"*Created By:*\n{inputs['depInfoAndRes']['createdBy']}"
}
]
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": f"*Deployment Owner:*\n{inputs['depInfoAndRes']['ownedBy']}"
},
{
"type": "mrkdwn",
"text": f"*Deployment Status:*\n{inputs['depInfoAndRes']['status']}"
}
]
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": f"*Last Updated By :*\n{inputs['depInfoAndRes']['lastUpdatedBy']}"
},
{
"type": "mrkdwn",
"text": f"*Last Updated At:*\n{inputs['depInfoAndRes']['lastUpdatedAt']}"
}
]
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": f"*Project Name :*\n{inputs['depInfoAndRes']['projectName']}"
},
{
"type": "mrkdwn",
"text": f"*Deployment ID:*\n{inputs['depInfoAndRes']['id']}"
}
]
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": f"*Request Details:*\n{inputs['depInfoAndRes']['requestDetails']}."
},
{
"type": "mrkdwn",
"text": f"<https://{inputs['vra_fqdn']}/automation-ui/#/deployment-ui;ash=%2Fworkload%2Fdeployment%2F{inputs['deploymentId']}|Click here to see your request>"
}
]
},
{"type": "divider"}
]
}
headers={'Content-Type': 'application/json','accept': 'application/json'}
resSlack=requests.post(inputs['depInfoAndRes']['proGrpContent']['slack_webhook']['const'], headers=headers, data=json.dumps(body), verify=False)
if resSlack.status_code==200:
print("Sending Slack Notification...")
else:
print('[?] Unexpected Error: [HTTP {0}]: Content: {1}'.format(resSlack.status_code, resSlack.content))
# Sends Teams Notifications.
if inputs['depInfoAndRes']['proGrpContent']['teams_notifications']['const']:
body= {
"@type": "MessageCard",
"themeColor": "0076D7",
"summary": "Create Deployments",
"sections": [{
"activityTitle": f"Task: {inputs['messageSubject']} ",
"activitySubtitle": f"Project: {inputs['depInfoAndRes']['projectName']}",
"facts": [
{
"name": "Owner",
"value": f"{inputs['depInfoAndRes']['ownedBy']}"
}, {
"name": "Deployment Name",
"value": f"{inputs['depInfoAndRes']['name']}"
}, {
"name": "Request Description",
"value": f" {inputs['depInfoAndRes']['description']}"
}, {
"name": "Request Details",
"value": f" {inputs['depInfoAndRes']['requestDetails']}"
}, {
"name": "Creation Date",
"value": f"{inputs['depInfoAndRes']['createdAt']}"
}, {
"name": "Last Updated at",
"value": f"{inputs['depInfoAndRes']['lastUpdatedAt']}"
}, {
"name": "Last Updated By",
"value": f"{inputs['depInfoAndRes']['lastUpdatedBy']}"
}, {
"name": "Created By",
"value": f"{inputs['depInfoAndRes']['createdBy']}"
}, {
"name": "Deployment ID",
"value": f"{inputs['depInfoAndRes']['id']}"
},
{
"name": "Status",
"value": f"{inputs['depInfoAndRes']['status']}"
},
{
"name": "Deployment URL",
"value": f"https://{inputs['vra_fqdn']}/automation-ui/#/deployment-ui;ash=%2Fworkload%2Fdeployment%2F{inputs['deploymentId']}"
},
],
"markdown": "true"
}]
}
headers={'Content-Type': 'application/json','accept': 'application/json'}
resTeams=requests.post(inputs['depInfoAndRes']['proGrpContent']['teams_webhook']['const'], headers=headers, data=json.dumps(body), verify=False)
if resTeams.status_code==200:
print("Sending Teams Notification...")
else:
print('[?] Unexpected Error: [HTTP {0}]: Content: {1}'.format(resTeams.status_code, resTeams.content))
# Sends Teams Notifications.
if inputs['depInfoAndRes']['proGrpContent']['zoom_notifications']['const']:
body={
"head": {
"text": f"{inputs['messageSubject']}",
"style":{
"bold": "true"
}
},
"body": [
{
"type": "section",
"sections": [
{
"type": "message",
"text": "Deployment Name:",
"style":{
"bold": "true"
}
},
{
"type": "message",
"text": f"{inputs['depInfoAndRes']['name']}"
}
]
},
{
"type": "section",
"sections": [
{
"type": "message",
"text": "Request Description:",
"style":{
"bold": "true"
}
},
{
"type": "message",
"text": f"{inputs['depInfoAndRes']['description']}"
}
]
},
{
"type": "section",
"sections": [
{
"type": "message",
"text": "Request Details:",
"style":{
"bold": "true"
}
},
{
"type": "message",
"text": f"{inputs['depInfoAndRes']['requestDetails']}"
}
]
},
{
"type": "section",
"sections": [
{
"type": "message",
"text": "Creation Date:",
"style":{
"bold": "true"
}
},
{
"type": "message",
"text": f"{inputs['depInfoAndRes']['createdAt']}"
}
]
},
{
"type": "section",
"sections": [
{
"type": "message",
"text": "Creation By:",
"style":{
"bold": "true"
}
},
{
"type": "message",
"text": f"{inputs['depInfoAndRes']['createdBy']}"
}
]
},
{
"type": "section",
"sections": [
{
"type": "message",
"text": "Deployment Owner:",
"style":{
"bold": "true"
}
},
{
"type": "message",
"text": f"{inputs['depInfoAndRes']['ownedBy']}"
}
]
},
{
"type": "section",
"sections": [
{
"type": "message",
"text": "Deployment Status:",
"style":{
"bold": "true"
}
},
{
"type": "message",
"text": f"{inputs['depInfoAndRes']['status']}"
}
]
},
{
"type": "section",
"sections": [
{
"type": "message",
"text": "Last Updated By:",
"style":{
"bold": "true"
}
},
{
"type": "message",
"text": f"{inputs['depInfoAndRes']['lastUpdatedBy']}"
}
]
},
{
"type": "section",
"sections": [
{
"type": "message",
"text": "Last Updated At:",
"style":{
"bold": "true"
}
},
{
"type": "message",
"text": f"{inputs['depInfoAndRes']['lastUpdatedAt']}"
}
]
},
{
"type": "section",
"sections": [
{
"type": "message",
"text": "Project Name:",
"style":{
"bold": "true"
}
},
{
"type": "message",
"text": f"{inputs['depInfoAndRes']['projectName']}"
}
]
},
{
"type": "section",
"sections": [
{
"type": "message",
"text": "Deployment ID:",
"style":{
"bold": "true"
}
},
{
"type": "message",
"text": f"{inputs['depInfoAndRes']['id']}"
}
]
},
{
"type": "section",
"sections": [
{
"type": "message",
"text": "Click here to see your request:",
"link": f"https://{inputs['vra_fqdn']}/automation-ui/#/deployment-ui;ash=%2Fworkload%2Fdeployment%2F{inputs['deploymentId']}"
}
]
}
]
}
headers={'Content-Type': 'application/json','accept': '*/*','Authorization': "Bearer "+zoom_token}
resZoom=requests.post(inputs['depInfoAndRes']['proGrpContent']['zoom_webhook']['const']+"?format=full", headers=headers, data=json.dumps(body), verify=False)
if resZoom.status_code==200:
print("Sending Zoom Notification...")
else:
print('[?] Unexpected Error: [HTTP {0}]: Content: {1}'.format(resZoom.status_code, resZoom.content))
- Create the following Default inputs:
- Action Constant – vra_fqdn
- Secret – zoom_token
- Add the dependency requests
- Create the ABX Action

ABX Flow Action: Ultimate Notifications
- Create a new action, but this time change the Type to be Flow

- Copy and paste the following code
# ABX Flow: Ultimate Notifications
# Created by Guillermo Martinez and Dennis Gerolymatos
# Version 1.0 - 22.12.2021
---
version: 1
flow:
flow_start:
next: action1
action1:
action: Get vRA Token
next: action2
action2:
action: Send to Email
next: action3
action3:
action: Send to App Webhook
next: flow_end
- Review the flow schema diagram. It should look like this:

- Select Create to complete the action
Create the Subscriptions
To meet all the notification scenarios, we need three Subscriptions with different Event Topic triggers and filters:
- Ultimate Notifications – Provision Pre
- Ultimate Notifications – Provision Post and Removal Post
- Ultimate Notifications – Expire
Ultimate Notifications – Provision Pre
Create a new Subscription with the following configuration:
- Name: Ultimate Notifications – Provision Pre
- Status: Enabled
- Organization Scope – your tenant
- Event Topic: Deployment Requested
- Condition:
This must include event.data.eventType == “CREATE_DEPLOYMENT” plus any other condition you may want – for example, to limit this subscription to three particular blueprints, we must specify the blueprint IDs with an “OR” operator, and include the eventType with an “AND” operator like such:
(event.data.blueprintId == ‘165a6765-c1d4-4a54-a8cb-e735a00f0167′ || event.data.blueprintId == ’12af6ef3-6dde-43ec-a30e-92209de03aee’ || event.data.blueprintId == ’54a0d8cb-7259-4193-817e-e5002e7ddbc5′) &&
event.data.eventType == “CREATE_DEPLOYMENT”
- Action/workflow: Ultimate Notifications
- Blocking: Unset
- Project scope: As desired

Ultimate Notifications – Provision Post and Removal Post
Create a new Subscription with the following configuration:
- Name: Ultimate Notifications – Provision Post and Removal Post
- Status: Enabled
- Organization Scope – your tenant
- Event Topic: Deployment completed
- Condition: Any conditions you may want – for example, to limit this subscription to three particular blueprints:
event.data.blueprintId == ’12af6ef3-6dde-43ec-a30e-92209de03aee’ ||
event.data.blueprintId == ’54a0d8cb-7259-4193-817e-e5002e7ddbc5′ ||
event.data.blueprintId == ‘165a6765-c1d4-4a54-a8cb-e735a00f0167’
- Action/workflow: Ultimate Notifications
- Blocking: Unset
- Project scope: As desired

Ultimate Notifications – Expire
Create a new Subscription with the following configuration:
- Name: Ultimate Notifications – Expire
- Status: Enabled
- Organization Scope – your tenant
- Event Topic: Deployment action requested
- Condition: event.data.actionName == ‘Expire’
- Action/workflow: Ultimate Notifications
- Blocking: Set
- Project scope: As desired

Notification Scenario Examples
Here are some samples of what some of the notifications will look like:
CREATE_INPROGRESS

Slack

Teams

Zoom

CREATE_SUCCESSFUL

Slack

Teams

Zoom

DELETE_SUCCESSFUL

Slack

Teams

Zoom

Closing Thoughts
I have been thinking about this one for some time and finally managed the hamster-cycles to put it together. I’m sure it can be improved on, so if you have some ideas please let me know! I already have a few enhancements to make and hopefully will be able to release these shortly.
As usual, Like, Share, Follow, and Spread the word. Until next time…