At the beginning of the journey to conquer the API of HPe Primera storages was a simple task. Create a CLI monitoring tool that could send information do different ticketing and monitoring tools. One would think this is an easy task. Let’s take a look at why this is not so easy and also, how to make it fairly easy.
Why is APIv1 crap? How to use APIv3? Why did I choose that super sexy green featured image? I’ll try to answer some of these.
I’ll look at a 3 inch long stick, that should bring down an airplane (SNMP), will bisect the APIv1 (and kill it in the process) and eventually will go down the route of implementing the APIv3, which is the most capable, yet HP will not let you use it – officially (used by the GUI).
First things first
Let’s get rid of the obious things, that will bugger you most of the time.
- HPe Primera is reporting as a 3PAR
- is labeled as a 3PAR
- uses 3PAR MIBs
- needs SSMC (3PAR) to do the initial setup
- The super sexy green image was chosen as it was the only high-res HP related image 🙂
Case one: Official documentation and API v1
According to the official documentation, the way to authenticate and pull data is as follows (in Python3):
def login(user, password): print("Login") info = {'user': user, 'password': password} headers = {'Content-Type': 'application/json'} body = requests.post(f"{_API_PATH}/credentials", data=json.dumps(info), headers=headers, verify=False) result = json.loads(body.text) pprint(result) if 'key' in result.keys(): print("Login Successfull") return result['key'] else: print("Login failed") return False def logout(session_key): print('Logout') body = requests.delete(f'{_API_PATH}/credentials/{session_key}', verify=False) if body.status_code == 200: print("Logout successfull") return True else: return False # Let's set the variables _API_APPEND = "api/v1" _PORT = "443" _SYSTEM = "primera.internal" _API_PATH = f"https://{_SYSTEM}:{_PORT}/{_API_APPEND}" _USER='monitor' _PASSWORD='monitor' # And the call: if '__main__': try: session = login(_USER, _PASSWORD) except: SystemExit(1)
The result is OK, no problems here.
$ python alerts_list.py Login {'key': '0-d0b3423b42608483b50b65dd515b094d-1154905e'} Login Successfull Logout Logout successful
The problem starts with a more sophisticated call, namely I want to pull the alerts, currently present onthe system. So according to the documentation, I do:
def get_alerts(session_key, minutes): print('Get Alerts') headers = {'Content-Type': 'application/json'} headers.update({'x-hp3par-wsapi-sessionkey': session_key}) query = f'/minutes:{minutes}' body = requests.get(f'{_API_PATH}/eventlog{query}', verify=False, headers=headers) result = json.loads(body.text) pprint(result) if body.status_code == 200: return result elif body.status_code == 500: print("Internal error") return False elif body.status_code == 400: print("Bad request") return False else: return False alerts = get_alerts(session, 144000)
And voila, we get alerts
{'members': [], 'total': 0}
Oh, wait… maybe the time range is wrong, let’s put in some really big number, like 144000 (100 days)
Login {'key': '0-c38bd7c83f453a02a2c868c67bf8589c-9055905e'} Login Successfull Get Alerts {'members': [{'alertInfo': {'alertId': '26', 'messageCode': '0x00300de'}, 'category': 2, 'class': 1, 'component': 3, 'componentId': '2:3:1', 'components': 'sw_port:2:3:1', 'description': 'Port 2:3:1 Degraded (Target Mode Port Went ' 'Offline {0x3})', 'id': '7930925', 'isDataChanged': True, 'links': [{'href': 'https://primera.internal/api/v1/ports/2:3:1', 'rel': 'port'}], 'resource': 3, 'resourceId': '2:3:1', 'severity': 5, 'time': '2020-03-27T10:34:10+01:00', 'timeSecs': 1585301650, 'type': 'Component state change'}, ... , 'total': 65} Logout Logout successfull real 0m59,552s user 0m0,512s sys 0m0,071s
oh, how cool. We’ve got alerts. I will not get in deep with how the contents of these alerts is different from what you see in the console, but let’s take a look at the more acute problem: The TIME IT TAKES?! – honestly 60 seconds to query a DB is way too much. There are a couple things I have taken from the time I spent with the API and alerting
- MINUS: These alerts do NOT show up immediately, god knows why – tested several times
- MINUS: The time it takes to list is just too long
- MINUS: Different to what you get from CLI / GUI
- PLUS: Is documented
And therefore, APIv1 is crap (So far)
Luckily, there is a solution. As the GUI uses an API as well, data can be pulled via that interface, right? Plus you’ll get all HP Primera data you already can see in the GUI, guaranteed. Although, it iwll be slightly different from the CLI version.
A new dawn, API v3, but undocumented
Let’s start with the list from above. Pluses and minuses.
- PLUS: Kind of real-time
- PLUS: Near instant retrieval of data
- PLUS: GUI uses the same API, so… == GUI
- MINUS: Documentation
- Also, uses a more modern way of securing, with session cookies
I’ve done this the “reverse engineering” way, so please if you find anything incorrect, wrong, or plain bullshit, feel free to let me know.
Login and logout APIv3 on the Primera from HPe
There are a couple things that needed changing in the login process. First of all, you really need to specify the password in an array, or list (python). The API path has also changed.
_API_APPEND = "/api/v3" _PORT = "443" _SYSTEM = "primera.internal" _API_PATH = f"https://{_SYSTEM}:{_PORT}{_API_APPEND}" _USER='monitor' _PASSWORD=['m','o','n','i','t','o','r']
Set up a helper function is a good idea, I thoght.
def check_api_result(api_res_body): if api_res_body.status_code == 200: return True elif api_res_body.status_code == 500: print("ERROR: Internal error") return False elif api_res_body.status_code == 400: print("ERROR: Bad request") return False else: return False
Second, I added the session cookie value into the session response
def login(user, password): ''' Login and get session token ''' print("Login") info = {'user': user, 'password': password} headers = {'Content-Type': 'application/json'} body = requests.post(f"{_API_PATH}/credentials", data=json.dumps(info), headers=headers, verify=False) result = json.loads(body.text) if 'key' in result.keys(): session_token = body.cookies['sessionToken'] return {'key': result['key'], 'token': session_token} else: return False
The logout is also a little different to the original. After we delete the session on the credentials endpoint, we’ll check for the validity of the session once again. Logout is only valid of the session returns an error.
def logout(session_key): ''' Logout == invalidate the session token ''' print('Logout') headers = {'Content-Type': 'application/json'} headers.update({'x-hp3par-wsapi-sessionkey': session_key['key']}) jar = requests.cookies.RequestsCookieJar() jar.set('sessionToken', session_key['token']) body = requests.delete(f"{_API_PATH}/credentials", verify=False, headers=headers, cookies=jar) check_session = requests.get(f"{_API_PATH}/credentials", verify=False, headers=headers, cookies=jar) if check_api_result(body) and check_session.status_code == 401: return True else: return False
Now, whenever we call an endpoint from the API, we need to include a cookie along with the other data. Take an example from the alerts
def get_alerts(session_key): ''' Getting the alert objects from HP Primera ''' print('Get Alerts') headers = {'Content-Type': 'application/json'} headers.update({'Cache-control': 'no-cache'}) jar = requests.cookies.RequestsCookieJar() jar.set('sessionToken', session_key['token']) body = requests.get(f"{_API_PATH}/alerts", verify=False, headers=headers, cookies=jar) result = json.loads(body.text) if check_api_result(body): return result else: return False
Of course, there are better ways of calling this, constructing it into classes, building modules, but this was a quick and dirty way of proving the concept. Now a quick look at what we’ve accomplished:
if '__main__': _print = True session = login(_USER, _PASSWORD) if session: print("Login Successfull") alerts = get_alerts(session) pprint(alerts) else: # Login failed print("Login failed") SystemExit(1) if logout(session): print("Logout successfull") else: # Logout failed print("Logout failed") SystemExit(2)
Login Login Successfull Get Alerts {'members': {'0006049a8f20fa494e82c88c42e17a8b': {'action': None, 'associatedResource': {'category': 'systems', 'searchName': 'wwn', 'searchValue': '2FF70002AC02524F'}, 'caseDetails': None, 'count': 1, ... Logout Logout successfull real 0m3,707s user 0m1,366s sys 0m0,201s
3 seconds. HP, honestly? APIv1? Really?
Of course, there are some things you need to take care of, such as the alert origin time vs. last occurrence. In other words, if an alert is new, there will only be a value of {‘members’: {id: {‘originEvent’: ‘firstTime’}}}. While if it is a recurring event / alert, there is an additional field in the same originEvent, fileld with data and name ‘lastTime’. So just take it into account.
Why not go directly with the SNMP instead?
You might not want to, or cannot, as all the other monitoring is based on different protocols. The major problem is: there is no simple solution to the SNMP setup. Each alert will generate a clearing event, as well as state change event, as well as a trap event. In this regard, you really need to implement the whole stack of alerting and reporting, INCLUDING the clearing and state change events. Otherwise, you’ll end up with a lot of traps.
Most of the SNMP capable devices also support polling. I’ve not been able to find any documentation, or the configuration capabilities on the primera itself to support this functionality. Have not tried it – as I suspect ‘time lost on this’
Thx
Epilogue:
Milan Toman is a Storage engineer by trade and has shared the common path with HP(e) products since 2008, including the early adoption of Blade technology, EVAs, MSAs, XPs(which are Hitachi), remembers 3PAR as 3PAR and ultimately hacks on whatever he can get his hands on.