HP Primera API. API v3 and API v1

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.

Leave a Reply

Your email address will not be published. Required fields are marked *