# -*- coding: utf-8 -*
###
# (C) Copyright (2012-2019) Hewlett Packard Enterprise Development LP
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
###
"""
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 hpOneView.exceptions import HPOneViewException
logger = logging.getLogger(__name__)
[docs]class connection(object):
def __init__(self, applianceIp, api_version=300, sslBundle=False, timeout=None):
self._session = None
self._host = applianceIp
self._cred = None
self._apiVersion = int(api_version)
self._headers = {
'X-API-Version': self._apiVersion,
'Accept': 'application/json',
'Content-Type': 'application/json'}
self._proxyHost = None
self._proxyPort = None
self._doProxy = False
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
[docs] def validateVersion(self):
version = self.get(uri['version'])
if 'minimumVersion' in version:
if self._apiVersion < version['minimumVersion']:
raise HPOneViewException('Unsupported API Version')
if 'currentVersion' in version:
if self._apiVersion > version['currentVersion']:
raise HPOneViewException('Unsupported API Version')
self._validateVersion = True
[docs] def set_proxy(self, proxyHost, proxyPort):
self._proxyHost = proxyHost
self._proxyPort = proxyPort
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 HPOneViewException('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)
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 HPOneViewException('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 HPOneViewException(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 HPOneViewException(body)
return response, body
###########################################################################
# Utility functions for making requests - the HTTP verbs
###########################################################################
[docs] def get(self, uri):
resp, body = self.do_http('GET', uri, '')
if resp.status >= 400:
raise HPOneViewException(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 HPOneViewException(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, verbose=False):
try:
if self._validateVersion is False:
self.validateVersion()
except Exception:
raise(HPOneViewException('Failure during login attempt.\n %s' % traceback.format_exc()))
self._cred = cred
try:
if self._cred.get("sessionID"):
self.set_session_id(self._cred["sessionID"])
task, body = self.put(uri['loginSessions'], None)
else:
self._cred.pop("sessionID", None)
task, body = self.post(uri['loginSessions'], self._cred)
except HPOneViewException:
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 HPOneViewException:
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'
}
############################################################################
# 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}