Leveraging Frame Admin API to Stick to your Public Cloud Budget
The Frame Education team uses public cloud infrastructure to conduct labs that demonstrate the power and capabilities of Frame. Sometimes during the course of these labs, students change the capacity settings of their Frame lab accounts to have machines running 24x7. Since these machines are in a public cloud, this can cause unnecessary and unexpected cloud expenses. To combat this, I developed a script to check all of the Frame accounts in the Frame Education customer entity and send alerts to a Slack channel if an account has a machine setup to run outside the hours of the Frame Lab. This allows the Frame instructors to identify and shutdown machines running outside of the lab's hours and shut them down. In this blog, I will go over how I used the Frame Admin API to accomplish this.
WRITTEN BY
TABLE OF CONTENT
API credentials
To use the Frame Admin API, the first thing you need to do is get a set of credentials via the Frame Administration User interface. This process is documented here. Since I want to check the entire Frame “customer” for running machines, I made sure I set up my credentials at the customer level and I gave the credentials the “Customer Administrator” role. You will also need to grab the Customer ID, which can be found in the url from the API page (Customer ID is the blurred area below).
The final authorization piece you will need is the Slack Webhook URL. To get this, you will need to work with your Slack administrator to set up a webhook on the Slack channel you want to send the alerts. Directions on how to set this up can be found here.Here is a list of the values you will need to capture.
_clnt_id = "<ClientID>"
_clnt_secret = "<ClientSecret>"
_cust_id = "<CustomerID>"
_slack_url = "<Slack Web Hook Url>"
Python
To develop this script, I decided to use this opportunity to brush up on my Python skills and created a Python 3.x script to accomplish that task. When using Python, it is recommended that you set up a “virtual environment” for your script in order to make sure it has the modules needed to execute. To do that, I created a directory and ran the following command to create the virtual environment.
python3 -m venv venv
I then activated the environment so that future commands would be run in the proper context.
source venv/bin/activate
There are two modules I needed to install so I used pip to grab those modules from the repositories.
pip install requests
pip install slack_sdk
“Requests” provides the functions to make a web request and the “slack_sdk” includes the slack webhook code. Using Python virtual environments puts all the code you need in a portable container which makes it easy to move and run in other environments.
With the environment setup I can now run my python script which I will go over in more detail below.
Python Script explanation
#! venv/bin/python3
import hashlib
import hmac
import time
import requests
import base64
import json
#_clnt_id = "<ClientID>"
#_clnt_secret = b"<ClientSecret>"
#_cust_id = "<CustomerID>"
#_slack_url = "<Slack Web Hook Url>"
The first part of the script defines the variables we collected above. Note that the 'b' preceding the client secret string is required since we are using it as a byte array and not a string. Byte array is required for the signing of the API request which is the main authentication mechanism used for the REST API calls.
Next, I will define some functions that I will use in the main part of the script. This is not strictly required, but it provides reusable features that you can use in other scripts.
# Function to send alert to the Frame Education Slack Channel
# msg_text: Markup formated text to send to Slack
def alert_to_slack (msg_text):
# use the slack_sdk Webhookclient
from slack_sdk.webhook import WebhookClient
# Create and send the formated message
webhook = WebhookClient(\_slack_url)
response = webhook.send(
text="fallback",
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": msg_text
}
}
]
)
The “alert_to_slack” function above is a simple wrapper for the slack hook API that takes in a text string and formats it with the JSON required by the slack webhook.
## Function to use GET to get Frame API information
# api_url: the url of the API formated with the right request information
def get_FrameAPICall (api_url):
# Create signature
timestamp = int(time.time())
to_sign = "%s%s" % (timestamp, \_clnt_id)
signature = hmac.new(\_clnt_secret, to_sign.encode('utf-8'), hashlib.sha256).hexdigest()
# Prepare http request headers
headers = { "X-Frame-ClientId": \_clnt_id, "X-Frame-Timestamp": str(timestamp), "X-Frame-Signature": signature }
# Make request
r = requests.get(api_url, headers=headers)
if (r.status_code == 200) :
return (r.content)
return(r.status_code)
The “get_FrameAPICall” creates the authentication signature for calling the Frame Admin API endpoint specified in api_url. It also does some rudimentary error handling by only passing on the content of successful API calls.
Below is the main part of the python script which is a nested set of loops that start at the customer level, iterating through all the organizations one at a time and for each organization, iterates through all of the accounts, one at a time. For each account, it iterates through all of the “pools” and filters on pools where the “kind” value is “production”. For these pools, it checks if the minimum number of servers is greater than zero or if the buffer servers are greater than zero. If either of these is true it sends an alert to the slack channel with the name of the organization, the name of the account, and the capacity settings of the pool.
#\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
# Main part of the python Script
#\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
alert_to_slack("Checking Frame Education Customers for running workloads\\n\_\_\_\_\_\_\_\_\_")
# Get a list of Organizations under the Frame customer
orgs=get_FrameAPICall("https://api.console.nutanix.com/api/rest/v1/organizations?" + \_cust_id + "=&show_deleted=false")
# Convert the Response to JSON
orgs_json=json.loads(orgs)
# Iterate through each Org
for org in orgs_json :
# Get a list of accounts under a specific organization
# print (org\['id'\])
accts=get_FrameAPICall("https://api.console.nutanix.com/api/rest/v1/accounts/?organization\_id=" + str(org\['id'\]) + "&active=true")
# Convert the Response to JSON
accts_json=json.loads(accts)
for acct in accts_json :
#print ("\\t" + acct\['name'\])
# Get a list of the pools under the account
pools=get_FrameAPICall("https://api.console.nutanix.com/api/rest/v1/accounts/" + str(acct\['id'\]) + "/pools")
# Convert the Response to JSON
pools_json=json.loads(pools)
for pool in pools_json :
# Focus on production pools only.
if pool\['kind'\] == 'production' :
# Get the capacity settings of the pool
cap_set=get_FrameAPICall("https://api.console.nutanix.com/api/rest/v1/pools/" + str(pool\['id'\]) + "/elasticity_settings")
cap_json=json.loads(cap_set)
#print ("\\t\\t\\tmin "+ str(cap_json\['min_servers'\]) + "\\n\\t\\t\\tbuffer "+str(cap_json\['buffer_servers'\])+"\\n\\t\\t\\tmax "+str(cap_json\['max_servers'\]) )
#Check for non-zero min or buffer setting and alert
if cap_json\['min_servers'\] > 0 or cap_json\['buffer_servers'\] > 0:
slack_text = '\*Organization:\* ' + org\['name'\] + "\\n\\t\*Account:\* " + acct\['name'\] + "\\n\\t\*min:\* " +\\
str(cap_json\['min_servers'\]) + " \*buf:\* " + str(cap_json\['buffer_servers'\]) +" \*max:\* " +str(cap_json\['max_servers'\])
alert_to_slack(slack_text)
alert_to_slack("\_\_\_\_\_\_\_\_\_\\nCompleted the check of Frame Education Customers for running workloads")
Use the dropdown menu below to review and copy the entire python script.
Conclusion
Once the above script was confirmed to work, I created a cron job on a small Linux server provisioned for this purpose. That job runs automatically at the close of business and administrators can confirm the script has run successfully by monitoring the slack channel for the starting and ending message sent by the script.
Any accounts that have pools with running servers can be investigated and mitigated if needed.
That’s it. Learning how to properly set up the Python environment was the big learning curve issue for me, but once that was completed, the rest of the coding was pretty straightforward based on other Frame API projects I have done in the past. The script can be modified to run at the organization or account level directly if desired by simply starting the outer loop at the level you want to check on. In the future, I may explore how to containerize this script and deploy it in a “serverless” manner so be sure to keep an eye on my Frame blogspace for that update.
Subscribe to our newsletter
Register for our newsletter now to unlock the full potential of Dizzion's Resource Library. Don't miss out on the latest industry insights – sign up today!
Dizzion values your privacy. By completing this form, you agree to the processing of your personal data in the manner indicated in the Dizzion Privacy Policy and consent to receive communications from Dizzion about our products, services, and events.