Resources

Arrow Image

Blog & News

Arrow Image

Leveraging Frame Admin API to Stick to your Public Cloud Budget

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.

News & Blog

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

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.

#!/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

Senior Solutions Architect

William Wong is the VP of Service Delivery for Dizzion, responsible for service delivery (professional and managed services), solutions architecture, and support. He works actively with customers to transform their business and operations leveraging DaaS in a hybrid and multi-cloud world. Before joining Dizzion as part of the Frame spinout from Nutanix, William was Head of Enterprise Solutions at Frame and following Nutanix's acquisition of Frame in 2018, Director of Solutions Architecture (Frame) at Nutanix. Prior to his work in DaaS, William led the development and adoption of innovative Internet software solutions and services, including Internet-based credit card and check processing and eCommerce platforms. William spent over 30 years at Cancer Commons, NetDeposit, Hewlett-Packard, VeriFone, and multiple Internet, payment, and eCommerce startups in executive management, program management, engineering management, and executive advisory positions. William received his B.S., M.S., and Ph.D. in Electrical Engineering from Stanford University.

More about the author

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!