Skip to main content

Leveraging Frame Admin API to Stick to your Public Cloud Budget

· 9 min read
David Horvath

Leverageing 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.

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).

Figure 1. API url with Customer ID

Figure 1. API url with Customer ID

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.

CapacityCheck.py
#!/bin/env python

import hashlib
import hmac
import time
import requests
import base64
import json

## _clnt_id = "<ClientID>"
## _clnt_secret = "<ClientSecret>"
## _cust_id = "<CustomerID>"
## _slack_url = "<Slack Web Hook Url>"

_clnt_id = "xxxxxxxxxxxxxxxxxxxxx.img.frame.nutanix.com"
_clnt_secret = b"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
_cust_id = "xxxxxxxxx-xxxxxx-xxxx-xxxx-xxxxxxxxxxxxxxxxx"
_slack_url = "https://hooks.slack.com/services/xxxxxxxxxxxxxxxxxxx"

## 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
}
}
]
)

## 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)

#_______________________________
# 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 ofFrame Education Customers for running workloads")

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.

Figure 2. Slack Message Example

Figure 2. Slack Message Example

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.

About the Author

David Horvath
More content created by David Horvath
David Horvath is a senior solutions architect with Nutanix Frame. He has been a part of the Frame team for almost five years and prior to that spent 20 years consulting on various information technology projects with the U.S. intelligence community.
© 2020-2023 Nutanix, Inc. All rights reserved. Nutanix, the Nutanix logo and all Nutanix product, feature and service names mentioned herein are registered trademarks or trademarks of Nutanix, Inc. in the United States and other countries. All other brand names mentioned herein are for identification purposes only and may be the trademarks of their respective holder(s). This post may contain links to external websites that are not part of Nutanix.com. Nutanix does not control these sites and disclaims all responsibility for the content or accuracy of any external site. Our decision to link to an external site should not be considered an endorsement of any content on such a site. Certain information contained in this post may relate to or be based on studies, publications, surveys and other data obtained from third-party sources and our own internal estimates and research. While we believe these third-party studies, publications, surveys and other data are reliable as of the date of this post, they have not independently verified, and we make no representation as to the adequacy, fairness, accuracy, or completeness of any information obtained from third-party sources.