# -*- coding: utf-8 -*
###
# (C) Copyright [2020] Hewlett Packard Enterprise Development LP
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
###
"""
connection.py
~~~~~~~~~~~~~~
This module maintains communication with the appliance.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from builtins import open
from builtins import str
from future import standard_library
standard_library.install_aliases()
import http.client
import json
import logging
import shutil # for shutil.copyfileobj()
import mmap # so we can upload the iso without having to load it in memory
import os
import ssl
import time
import traceback
from hpeOneView.exceptions import HPEOneViewException
logger = logging.getLogger(__name__)
ONEVIEW_CLIENT_INVALID_PROXY = 'Invalid Proxy format'
[docs]class connection(object):
def __init__(self, applianceIp, api_version=None, sslBundle=False, timeout=None, proxy=None):
self._session = None
self._host = applianceIp
self._cred = None
self._proxyHost = None
self._proxyPort = None
self._doProxy = False
self.set_proxy(proxy)
self._sslTrustAll = True
self._sslBundle = sslBundle
self._sslTrustedBundle = self.set_trusted_ssl_bundle(sslBundle)
self._nextPage = None
self._prevPage = None
self._numTotalRecords = 0
self._numDisplayedRecords = 0
self._validateVersion = False
self._timeout = timeout
if not api_version:
api_version = self.get_default_api_version()
self._apiVersion = int(api_version)
self._headers = {
'X-API-Version': self._apiVersion,
'Accept': 'application/json',
'Content-Type': 'application/json'}
[docs] def get_default_api_version(self):
self._headers = {
'Accept': 'application/json',
'Content-Type': 'application/json'}
version = self.get(uri['version'])
return version['currentVersion']
[docs] def validateVersion(self):
version = self.get(uri['version'])
if 'minimumVersion' in version:
if self._apiVersion < version['minimumVersion']:
raise HPEOneViewException('Unsupported API Version')
if 'currentVersion' in version:
if self._apiVersion > version['currentVersion']:
raise HPEOneViewException('Unsupported API Version')
self._validateVersion = True
[docs] def set_proxy(self, proxy):
if proxy and len(proxy) > 0:
splitted = proxy.split(':')
if len(splitted) != 2:
raise ValueError(ONEVIEW_CLIENT_INVALID_PROXY)
self._proxyHost = splitted[0]
self._proxyPort = int(splitted[1])
self._doProxy = True
[docs] def set_trusted_ssl_bundle(self, sslBundle):
if sslBundle:
self._sslTrustAll = False
return sslBundle
[docs] def get_session(self):
return self._session
[docs] def get_session_id(self):
return self._headers.get('auth')
[docs] def set_session_id(self, session_id):
self._headers['auth'] = session_id
self._session = True
[docs] def get_host(self):
return self._host
[docs] def get_by_uri(self, xuri):
return self.get(xuri)
[docs] def make_url(self, path):
return 'https://%s%s' % (self._host, path)
[docs] def do_http(self, method, path, body, custom_headers=None):
http_headers = self._headers.copy()
if custom_headers:
http_headers.update(custom_headers)
bConnected = False
conn = None
while bConnected is False:
try:
conn = self.get_connection()
conn.request(method, path, body, http_headers)
resp = conn.getresponse()
tempbytes = ''
try:
tempbytes = resp.read()
tempbody = tempbytes.decode('utf-8')
except UnicodeDecodeError: # Might be binary data
tempbody = tempbytes
conn.close()
bConnected = True
return resp, tempbody
if tempbody:
try:
body = json.loads(tempbody)
except ValueError:
body = tempbody
conn.close()
bConnected = True
except http.client.BadStatusLine:
logger.warning('Bad Status Line. Trying again...')
if conn:
conn.close()
time.sleep(1)
continue
except http.client.HTTPException:
raise HPEOneViewException('Failure during login attempt.\n %s' % traceback.format_exc())
return resp, body
[docs] def download_to_stream(self, stream_writer, url, body='', method='GET', custom_headers=None):
http_headers = self._headers.copy()
if custom_headers:
http_headers.update(custom_headers)
chunk_size = 4096
conn = None
successful_connected = False
while not successful_connected:
try:
conn = self.get_connection()
conn.request(method, url, body, http_headers)
resp = conn.getresponse()
if resp.status >= 400:
self.__handle_download_error(resp, conn)
if resp.status == 302:
return self.download_to_stream(stream_writer=stream_writer,
url=resp.getheader('Location'),
body=body,
method=method,
custom_headers=http_headers)
tempbytes = True
while tempbytes:
tempbytes = resp.read(chunk_size)
if tempbytes: # filter out keep-alive new chunks
stream_writer.write(tempbytes)
conn.close()
successful_connected = True
except http.client.BadStatusLine:
logger.warning('Bad Status Line. Trying again...')
if conn:
conn.close()
time.sleep(1)
continue
except http.client.HTTPException:
raise HPEOneViewException('Failure during login attempt.\n %s' % traceback.format_exc())
return successful_connected
def __handle_download_error(self, resp, conn):
try:
tempbytes = resp.read()
tempbody = tempbytes.decode('utf-8')
try:
body = json.loads(tempbody)
except ValueError:
body = tempbody
except UnicodeDecodeError: # Might be binary data
body = tempbytes
conn.close()
if not body:
body = "Error " + str(resp.status)
conn.close()
raise HPEOneViewException(body)
[docs] def get_connection(self):
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
if self._sslTrustAll is False:
context.verify_mode = ssl.CERT_REQUIRED
context.load_verify_locations(self._sslTrustedBundle)
if self._doProxy is False:
conn = http.client.HTTPSConnection(self._host,
context=context,
timeout=self._timeout)
else:
conn = http.client.HTTPSConnection(self._proxyHost,
self._proxyPort,
context=context,
timeout=self._timeout)
conn.set_tunnel(self._host, 443)
else:
context.verify_mode = ssl.CERT_NONE
if self._doProxy is False:
conn = http.client.HTTPSConnection(self._host,
context=context,
timeout=self._timeout)
else:
conn = http.client.HTTPSConnection(self._proxyHost,
self._proxyPort,
context=context,
timeout=self._timeout)
conn.set_tunnel(self._host, 443)
return conn
def _open(self, name, mode):
return open(name, mode)
[docs] def post_multipart_with_response_handling(self, uri, file_path, baseName):
resp, body = self.post_multipart(uri, None, file_path, baseName)
if resp.status == 202:
task = self.__get_task_from_response(resp, body)
return task, body
if self.__body_content_is_task(body):
return body, body
return None, body
[docs] def post_multipart(self, uri, fields, files, baseName, verbose=False):
content_type = self.encode_multipart_formdata(fields, files, baseName,
verbose)
inputfile = self._open(files + '.b64', 'rb')
mappedfile = mmap.mmap(inputfile.fileno(), 0, access=mmap.ACCESS_READ)
if verbose is True:
print(('Uploading ' + files + '...'))
conn = self.get_connection()
# conn.set_debuglevel(1)
conn.connect()
conn.putrequest('POST', uri)
conn.putheader('uploadfilename', baseName)
conn.putheader('auth', self._headers['auth'])
conn.putheader('Content-Type', content_type)
totalSize = os.path.getsize(files + '.b64')
conn.putheader('Content-Length', totalSize)
conn.putheader('X-API-Version', self._apiVersion)
conn.endheaders()
while mappedfile.tell() < mappedfile.size():
# Send 1MB at a time
# NOTE: Be careful raising this value as the read chunk
# is stored in RAM
readSize = 1048576
conn.send(mappedfile.read(readSize))
if verbose is True:
print('%d bytes sent... \r' % mappedfile.tell())
mappedfile.close()
inputfile.close()
os.remove(files + '.b64')
response = conn.getresponse()
body = response.read().decode('utf-8')
if body:
try:
body = json.loads(body)
except ValueError:
body = response.read().decode('utf-8')
conn.close()
if response.status >= 400:
raise HPEOneViewException(body)
return response, body
###########################################################################
# Utility functions for making requests - the HTTP verbs
###########################################################################
[docs] def get(self, uri, custom_headers=None):
resp, body = self.do_http('GET', uri, '', custom_headers=custom_headers)
if resp.status >= 400:
raise HPEOneViewException(body)
if resp.status == 302:
body = self.get(resp.getheader('Location'))
if type(body) is dict:
if 'nextPageUri' in body:
self._nextPage = body['nextPageUri']
if 'prevPageUri' in body:
self._prevPage = body['prevPageUri']
if 'total' in body:
self._numTotalRecords = body['total']
if 'count' in body:
self._numDisplayedRecords = body['count']
return body
[docs] def getNextPage(self):
body = self.get(self._nextPage)
return get_members(body)
[docs] def getPrevPage(self):
body = self.get(self._prevPage)
return get_members(body)
[docs] def getLastPage(self):
while self._nextPage is not None:
members = self.getNextPage()
return members
[docs] def getFirstPage(self):
while self._prevPage is not None:
members = self.getPrevPage()
return members
[docs] def delete(self, uri, custom_headers=None):
return self.__do_rest_call('DELETE', uri, {}, custom_headers=custom_headers)
[docs] def put(self, uri, body, custom_headers=None):
return self.__do_rest_call('PUT', uri, body, custom_headers=custom_headers)
[docs] def post(self, uri, body, custom_headers=None):
return self.__do_rest_call('POST', uri, body, custom_headers=custom_headers)
[docs] def patch(self, uri, body, custom_headers=None):
return self.__do_rest_call('PATCH', uri, body, custom_headers=custom_headers)
def __body_content_is_task(self, body):
return isinstance(body, dict) and 'category' in body and body['category'] == 'tasks'
def __get_task_from_response(self, response, body):
location = response.getheader('Location')
if location:
task = self.get(location)
elif 'taskState' in body:
# This check is needed to handle a status response 202 without the location header,
# as is for PowerDevices. We are not sure if there are more resources with the same behavior.
task = body
else:
# For the resource Label the status is 202 but the response not contains a task.
task = None
return task
def __do_rest_call(self, http_method, uri, body, custom_headers):
resp, body = self.do_http(method=http_method,
path=uri,
body=json.dumps(body),
custom_headers=custom_headers)
if resp.status >= 400:
raise HPEOneViewException(body)
if resp.status == 304:
if body and not isinstance(body, dict):
try:
body = json.loads(body)
except Exception:
pass
elif resp.status == 202:
task = self.__get_task_from_response(resp, body)
return task, body
if self.__body_content_is_task(body):
return body, body
return None, body
###########################################################################
# EULA
###########################################################################
[docs] def get_eula_status(self):
return self.get(uri['eulaStatus'])
[docs] def set_eula(self, supportAccess='yes'):
eula = make_eula_dict(supportAccess)
self.post(uri['eulaSave'], eula)
return
###########################################################################
# Initial Setup
###########################################################################
[docs] def change_initial_password(self, newPassword):
password = make_initial_password_change_dict('Administrator',
'admin', newPassword)
# This will throw an exception if the password is already changed
self.post(uri['changePassword'], password)
###########################################################################
# Login/Logout to/from appliance
###########################################################################
[docs] def login(self, cred, sessionID=None, verbose=False):
try:
if self._validateVersion is False:
self.validateVersion()
except Exception:
raise (HPEOneViewException('Failure during login attempt.\n %s' % traceback.format_exc()))
cred['loginMsgAck'] = True # This will handle the login acknowledgement message
self._cred = cred
try:
if self._cred.get("sessionID"):
self.set_session_id(self._cred["sessionID"])
task, body = self.put(uri['loginSessions'], None)
elif sessionID is not None:
self.set_session_id(sessionID)
task, body = self.put(uri['loginSessions'], None)
else:
self._cred.pop("sessionID", None)
task, body = self.post(uri['loginSessions'], self._cred)
except HPEOneViewException:
logger.exception('Login failed')
raise
auth = body['sessionID']
# Add the auth ID to the headers dictionary
self._headers['auth'] = auth
self._session = True
if verbose is True:
print(('Session Key: ' + auth))
logger.info('Logged in successfully')
[docs] def logout(self, verbose=False):
# resp, body = self.do_http(method, uri['loginSessions'] \
# , body, self._headers)
try:
self.delete(uri['loginSessions'])
except HPEOneViewException:
logger.exception('Logout failed')
raise
if verbose is True:
print('Logged Out')
del self._headers['auth']
self._session = False
logger.info('Logged out successfully')
return None
[docs] def enable_etag_validation(self):
"""
Enable the concurrency control for the PUT and DELETE requests, in which the requests are conditionally
processed only if the provided entity tag in the body matches the latest entity tag stored for the resource.
The eTag validation is enabled by default.
"""
self._headers.pop('If-Match', None)
[docs] def disable_etag_validation(self):
"""
Disable the concurrency control for the PUT and DELETE requests. The requests will be forced without specifying
an explicit ETag. This method sets an If-Match header of "*".
"""
self._headers['If-Match'] = '*'
uri = {
# ------------------------------------
# Settings
# ------------------------------------
'globalSettings': '/rest/global-settings',
'vol-tmplate-policy': '/rest/global-settings/StorageVolumeTemplateRequired',
'eulaStatus': '/rest/appliance/eula/status',
'eulaSave': '/rest/appliance/eula/save',
'serviceAccess': '/rest/appliance/settings/enableServiceAccess',
'service': '/rest/appliance/settings/serviceaccess',
'applianceNetworkInterfaces': '/rest/appliance/network-interfaces',
'healthStatus': '/rest/appliance/health-status',
'version': '/rest/version',
'supportDump': '/rest/appliance/support-dumps',
'backups': '/rest/backups',
'archive': '/rest/backups/archive',
'dev-read-community-str': '/rest/appliance/device-read-community-string',
'licenses': '/rest/licenses',
'nodestatus': '/rest/appliance/nodeinfo/status',
'nodeversion': '/rest/appliance/nodeinfo/version',
'shutdown': '/rest/appliance/shutdown',
'trap': '/rest/appliance/trap-destinations',
'restores': '/rest/restores',
'domains': '/rest/domains',
'schema': '/rest/domains/schema',
'progress': '/rest/appliance/progress',
'appliance-firmware': '/rest/appliance/firmware/image',
'fw-pending': '/rest/appliance/firmware/pending',
# ------------------------------------
# Security
# ------------------------------------
'activeSessions': '/rest/active-user-sessions',
'loginSessions': '/rest/login-sessions',
'users': '/rest/users',
'userRole': '/rest/users/role',
'changePassword': '/rest/users/changePassword',
'roles': '/rest/roles',
'category-actions': '/rest/authz/category-actions',
'role-category-actions': '/rest/authz/role-category-actions',
'validator': '/rest/authz/validator',
# ------------------------------------
# Facilities
# ------------------------------------
'datacenters': '/rest/datacenters',
'powerDevices': '/rest/power-devices',
'powerDevicesDiscover': '/rest/power-devices/discover',
'racks': '/rest/racks',
# ------------------------------------
# Systems
# ------------------------------------
'servers': '/rest/server-hardware',
'server-hardware-types': '/rest/server-hardware-types',
'enclosures': '/rest/enclosures',
'enclosureGroups': '/rest/enclosure-groups',
'enclosurePreview': '/rest/enclosure-preview',
'fwUpload': '/rest/firmware-bundles',
'fwDrivers': '/rest/firmware-drivers',
# ------------------------------------
# Connectivity
# ------------------------------------
'conn': '/rest/connections',
'ct': '/rest/connection-templates',
'enet': '/rest/ethernet-networks',
'fcnet': '/rest/fc-networks',
'nset': '/rest/network-sets',
'li': '/rest/logical-interconnects',
'lig': '/rest/logical-interconnect-groups',
'ic': '/rest/interconnects',
'ictype': '/rest/interconnect-types',
'uplink-sets': '/rest/uplink-sets',
'ld': '/rest/logical-downlinks',
'idpool': '/rest/id-pools',
'vmac-pool': '/rest/id-pools/vmac',
'vwwn-pool': '/rest/id-pools/vwwn',
'vsn-pool': '/rest/id-pools/vsn',
# ------------------------------------
# Server Profiles
# ------------------------------------
'profiles': '/rest/server-profiles',
'profile-templates': '/rest/server-profile-templates',
'profile-networks': '/rest/server-profiles/available-networks',
'profile-networks-schema': '/rest/server-profiles/available-networks/schema',
'profile-available-servers': '/rest/server-profiles/available-servers',
'profile-available-servers-schema': '/rest/server-profiles/available-servers/schema',
'profile-available-storage-system': '/rest/server-profiles/available-storage-system',
'profile-available-storage-systems': '/rest/server-profiles/available-storage-systems',
'profile-available-targets': '/rest/server-profiles/available-targets',
'profile-messages-schema': '/rest/server-profiles/messages/schema',
'profile-ports': '/rest/server-profiles/profile-ports',
'profile-ports-schema': '/rest/server-profiles/profile-ports/schema',
'profile-schema': '/rest/server-profiles/schema',
# ------------------------------------
# Health
# ------------------------------------
'alerts': '/rest/alerts',
'events': '/rest/events',
'audit-logs': '/rest/audit-logs',
'audit-logs-download': '/rest/audit-logs/download',
# ------------------------------------
# Certificates
# ------------------------------------
'certificates': '/rest/certificates',
'ca': '/rest/certificates/ca',
'crl': '/rest/certificates/ca/crl',
'rabbitmq-kp': '/rest/certificates/client/rabbitmq/keypair',
'rabbitmq': '/rest/certificates/client/rabbitmq',
'cert-https': '/rest/certificates/https',
# ------------------------------------
# Searching and Indexing
# ------------------------------------
'resource': '/rest/index/resources',
'association': '/rest/index/associations',
'tree': '/rest/index/trees',
'search-suggestion': '/rest/index/search-suggestions',
# ------------------------------------
# Logging and Tracking
# ------------------------------------
'task': '/rest/tasks',
# ------------------------------------
# Storage
# ------------------------------------
'storage-pools': '/rest/storage-pools',
'storage-systems': '/rest/storage-systems',
'storage-volumes': '/rest/storage-volumes',
'vol-templates': '/rest/storage-volume-templates',
'connectable-vol': '/rest/storage-volume-templates/connectable-volume-templates',
'attachable-volumes': '/rest/storage-volumes/attachable-volumes',
# ------------------------------------
# FC-SANS
# ------------------------------------
'device-managers': '/rest/fc-sans/device-managers',
'managed-sans': '/rest/fc-sans/managed-sans',
'providers': '/rest/fc-sans/providers',
# ------------------------------------
# Metrcs
# ------------------------------------
'metricsCapabilities': '/rest/metrics/capability',
'metricsConfiguration': '/rest/metrics/configuration',
# ------------------------------------
# Uncategorized
# ------------------------------------
'unmanaged-devices': '/rest/unmanaged-devices',
# ------------------------------------
# Hypervisors
# ------------------------------------
'hypervisor-managers': '/rest/hypervisor-managers'
}
############################################################################
# Utility to print resource to standard output
############################################################################
[docs]def get_members(mlist):
if not mlist:
return []
if not mlist['members']:
return []
return mlist['members']
[docs]def get_member(mlist):
if not mlist:
return None
if not mlist['members']:
return None
return mlist['members'][0]
[docs]def make_eula_dict(supportAccess):
return {'supportAccess': supportAccess}
[docs]def make_initial_password_change_dict(userName, oldPassword, newPassword):
return {
'userName': userName,
'oldPassword': oldPassword,
'newPassword': newPassword}