1
\$\begingroup\$

I've written a small script for Zabbix to check the status of our ESXi Hosts. The request needs to be authenticated, so I went for this process:

  1. Read auth token from file
    1. If file does not exist, authenticate and store token into file
  2. Run request to get the information
    1. If I get permission denied, authenticate, store the token into file and try again

Is there a better way to handle this? It bothers me a little to have the get_host_state() call twice, but placing the handling of the permission error into the function would make it recursive, which would then need additional parameters to prevent an endless loop and which would make it more complex.

Could the functions themselves be improved?

Notes:

  • I'm aware that there are existing python modules to access the API, but I want to avoid the hassle and overhead of setting up a virtualenv and installing countless python modules on the Zabbix server for a simple check
  • I disabled the traceback on purpose because I get the first line of the output of the script directly in Zabbix when an error occurs and this way I see directly the error instead of the pretty useless first line of the stacktrace.

The response from the API is json, which is then parsed and handled in Zabbix.

full script:

import requests
import sys

vc_url = "vcenter.example.com"
vc_user = "[email protected]"
vc_password = "aHR0cDovL2JpdC5seS8xVHFjd243Cg=="
auth_store = "/tmp/zbx_esxi_connstate_auth"

esx_name = sys.argv[1]

session_id = ''
sys.tracebacklimit = 0

def vc_auth():
  response = requests.post(f"https://{vc_url}/api/session", auth=(vc_user, vc_password))
  if response.ok:
    session_id = response.json()
    with open(auth_store, "w") as file:
      file.write(session_id)
    return session_id
  else:
    raise PermissionError("Unable to retrieve a session ID.")

def read_auth():
  try:
    with open(auth_store, "r") as file:
      session_id = file.read()
      return session_id
  except FileNotFoundError:
    vc_auth()

def get_host_state():
  response = requests.get(f"https://{vc_url}/api/vcenter/host?names={esx_name}", headers={"vmware-api-session-id": session_id})
  if response.status_code == 401:
    raise PermissionError("Authentication required.")
  if response.ok:
    print(f"{response.text}")
  else:
    raise ValueError(response.text)

session_id = read_auth()
try:
  get_host_state()
except PermissionError:
  session_id = vc_auth()
  get_host_state()

Here is the response from the API for the authentication request:

HTTP/2 201
date: Thu, 15 Sep 2022 13:28:26 GMT
vmware-api-session-id: 39efaf9c1c0ababc005ce63795b773bc
content-type: application/json
x-envoy-upstream-service-time: 169
server: envoy

"39efaf9c1c0ababc005ce63795b773bc"
\$\endgroup\$
7
  • \$\begingroup\$ I sure hope that password isn't valuable \$\endgroup\$
    – Reinderien
    Commented Sep 15, 2022 at 10:22
  • \$\begingroup\$ I'm confused. If you have hard-coded credentials, why would you bother to send a first unauthenticated request? Why not just authenticate immediately? \$\endgroup\$
    – Reinderien
    Commented Sep 15, 2022 at 12:15
  • \$\begingroup\$ I'm caching the authentication token. The check runs every minute for every single host, there is no need to authenticate every time, just when the token is expired or isn't there yet. \$\endgroup\$ Commented Sep 15, 2022 at 12:44
  • \$\begingroup\$ Please include in your question a header dump from the response of the auth endpoint. It might be possible to simplify this. \$\endgroup\$
    – Reinderien
    Commented Sep 15, 2022 at 13:12
  • 1
    \$\begingroup\$ Sadly, the expiry time is not documented in the API documentation. In the vSphere vCenter configuration I can see a configured session timeout with a value of 120 minutes, but there is no indication if this value is used only for interactive sessions of the web UI or for all sessions. I presume it's the latter, but there is no way to be sure. \$\endgroup\$ Commented Sep 16, 2022 at 5:29

1 Answer 1

3
\$\begingroup\$

Instead of using:

esx_name = sys.argv[1]

prefer the argparse library which is more flexible and allows for variable argument positioning.

In read_auth you have: except FileNotFoundError but this is an exception that is preventable so it would be easier to just check that the file does exist eg:

from os.path import exists
if not exists(auth_store)...

or better yet:

from pathlib import Path
if not Path(auth_store).is_file()

Instead of doing plain plain requests.get or requests.post prefer requests.Session(). One benefit is that the session instance can contain predefined headers including the auth token, so there is no need to repeat those headers in every subsequent request. Just do session.post() etc, and you can still provide additional headers on a per request basis if needed. This will come in handy when and if you add more methods.

Have you considered writing a small class to wrap this all up? The session ID could be made a class property with an initial value of None.

There is one challenge in your program, it is that the token may expire after some time but you don't know when. Since the requests library has callbacks (called event hooks), it could be interesting to use the functionality to handle automatic and transparent re-authentication whenever needed. This could easily turn into another bigger challenge but that's an idea though.

Have a look here for a possible solution: Python Requests - retry request after re-authentication. Another option is to use the retry on failure capabilities of the requests lib. This is more advanced stuff and it requires you to tinker with HTTPAdapter but it's worth it for bigger projects surely.

Let's note that response.ok actually does not match a HTTP/200 (OK) response:

Returns True if status_code is less than 400, False if not.

This attribute checks if the status code of the response is between 400 and 600 to see if there was a client error or a server error. If the status code is between 200 and 400, this will return True. This is not a check to see if the response code is 200 OK.

Source: Developer Interface

It may be a convenient shorthand to handle both 200 and 201 though, as long as you are aware of how it works.

You can also use predefined codes for HTTP errors, for example for 401: requests.codes.unauthorized and 201: requests.codes.created. Use response.status_code to get the actual HTTP response code. And you can indeed use requests.codes.ok if you really want to check for HTTP/200.

Instead of using PermissionError it would make more sense to use the set of native requests exceptions. Or just use raise_for_status().

\$\endgroup\$
1
  • \$\begingroup\$ That's very insightful, thanks. I'll try these suggestions. \$\endgroup\$ Commented Sep 16, 2022 at 9:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.