2
\$\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 theget_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 requestsimport sysvc_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 = 0def 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 201date: Thu, 15 Sep 2022 13:28:26 GMTvmware-api-session-id: 39efaf9c1c0ababc005ce63795b773bccontent-type: application/jsonx-envoy-upstream-service-time: 169server: envoy"39efaf9c1c0ababc005ce63795b773bc"
askedSep 15, 2022 at 7:21
Gerald Schneider's user avatar
\$\endgroup\$
7
  • \$\begingroup\$I sure hope that password isn't valuable\$\endgroup\$CommentedSep 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\$CommentedSep 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\$CommentedSep 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\$CommentedSep 15, 2022 at 13:12
  • 1
    \$\begingroup\$Sadly, the expiry time is not documented in theAPI 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\$CommentedSep 16, 2022 at 5:29

1 Answer1

4
\$\begingroup\$

Instead of using:

esx_name = sys.argv[1]

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

Inread_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 existsif not exists(auth_store)...

or better yet:

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

Instead of doing plain plainrequests.get orrequests.post preferrequests.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 dosession.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 hascallbacks (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 theretry 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 thatresponse.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 servererror. If the status code is between 200 and 400, this will returnTrue. 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. Useresponse.status_code to get the actual HTTP response code. And you can indeed userequests.codes.ok if you really want to check for HTTP/200.

Instead of usingPermissionError it would make more sense to use the set of nativerequests exceptions. Or just useraise_for_status().

answeredSep 15, 2022 at 20:31
Kate's user avatar
\$\endgroup\$
0

You mustlog in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.