Skip to content
This repository has been archived by the owner on Oct 15, 2020. It is now read-only.

Allow connection reuse #361

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# 4.5.0 (Unreleased)
#### Notes
Added the capability to set a connection timeout when connecting to the HPE OneView Appliance
Added the capability to set a connection timeout when connecting to the HPE OneView Appliance.

Extends support of the SDK to OneView Rest API version 600 (OneView v4.0).

Added the capability to reuse https connections to the HPE OneView Appliance.

#### Features supported with current release:
- Connection template
- Enclosure
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export ONEVIEWSDK_AUTH_LOGIN_DOMAIN='authdomain'
export ONEVIEWSDK_SSL_CERTIFICATE='<path_to_cert.crt_file>'
export ONEVIEWSDK_PROXY='<proxy_host>:<proxy_port>'
export ONEVIEWSDK_CONNECTION_TIMEOUT='<connection time-out in seconds>'
export ONEVIEWSDK_REUSE_CONNECTION='<string>'
```

:lock: Tip: Make sure no unauthorized person has access to the environment variables, since the password is stored in clear-text.
Expand Down Expand Up @@ -259,6 +260,22 @@ export ONEVIEWSDK_CONNECTION_TIMEOUT='<connection time-out in seconds>'
"timeout": <timeout in seconds>
```


### Reuse https connections
By default a new https connection is made and subsequently closed for each SDK transaction. To
change this so that the https_connection is reused then either:

1. Set the appropriate environment variable:
```bash
export ONEVIEWSDK_REUSE_CONNECTION='<any non-null string, eg Yes>'
```

2. Set the reuse_connection flag in the JSON configuration file:
```bash
"reuse_connection": true
```


## Exception handling

All exceptions raised by the OneView Python SDK inherit from HPOneViewException.
Expand Down
3 changes: 2 additions & 1 deletion examples/config-rename.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@
"storage_system_password": "",
"power_device_hostname": "",
"power_device_username": "",
"power_device_password": ""
"power_device_password": "",
"reuse_connection": true
}
59 changes: 43 additions & 16 deletions hpOneView/connection.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*
###
# (C) Copyright (2012-2017) Hewlett Packard Enterprise Development LP
# (C) Copyright (2012-2018) 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
Expand Down Expand Up @@ -54,7 +54,7 @@


class connection(object):
def __init__(self, applianceIp, api_version=300, sslBundle=False, timeout=None):
def __init__(self, applianceIp, api_version=300, sslBundle=False, timeout=None, reuse_connection=False):
self._session = None
self._host = applianceIp
self._cred = None
Expand All @@ -75,6 +75,10 @@ def __init__(self, applianceIp, api_version=300, sslBundle=False, timeout=None):
self._numDisplayedRecords = 0
self._validateVersion = False
self._timeout = timeout
self._reuse_connection = reuse_connection
if self._reuse_connection:
self._headers['Connection'] = 'keep-alive'
self._conn = None

def validateVersion(self):
version = self.get(uri['version'])
Expand Down Expand Up @@ -120,11 +124,11 @@ def do_http(self, method, path, body, custom_headers=None):
if custom_headers:
http_headers.update(custom_headers)

bConnected = False
conn = None
bConnected = False
while bConnected is False:
try:
conn = self.get_connection()
conn = self.get_reusable_connection()
conn.request(method, path, body, http_headers)
resp = conn.getresponse()
tempbytes = ''
Expand All @@ -133,23 +137,25 @@ def do_http(self, method, path, body, custom_headers=None):
tempbody = tempbytes.decode('utf-8')
except UnicodeDecodeError: # Might be binary data
tempbody = tempbytes
conn.close()
if not self._reuse_connection:
self.close_reusable_connection(conn)
bConnected = True
return resp, tempbody
if tempbody:
try:
body = json.loads(tempbody)
except ValueError:
body = tempbody
conn.close()
if not self._reuse_connection:
self.close_reusable_connection(conn)
bConnected = True
except http.client.BadStatusLine:
logger.warning('Bad Status Line. Trying again...')
if conn:
conn.close()
self.close_reusable_connection(conn)
time.sleep(1)
continue
except http.client.HTTPException:
self.close_reusable_connection(conn)
raise HPOneViewException('Failure during login attempt.\n %s' % traceback.format_exc())

return resp, body
Expand All @@ -165,7 +171,7 @@ def download_to_stream(self, stream_writer, url, body='', method='GET', custom_h
successful_connected = False
while not successful_connected:
try:
conn = self.get_connection()
conn = self.get_reusable_connection()
conn.request(method, url, body, http_headers)
resp = conn.getresponse()

Expand All @@ -178,15 +184,16 @@ def download_to_stream(self, stream_writer, url, body='', method='GET', custom_h
if tempbytes: # filter out keep-alive new chunks
stream_writer.write(tempbytes)

conn.close()
if not self._reuse_connection:
self.close_reusable_connection(conn)
successful_connected = True
except http.client.BadStatusLine:
logger.warning('Bad Status Line. Trying again...')
if conn:
conn.close()
self.close_reusable_connection(conn)
time.sleep(1)
continue
except http.client.HTTPException:
self.close_reusable_connection(conn)
raise HPOneViewException('Failure during login attempt.\n %s' % traceback.format_exc())

return successful_connected
Expand All @@ -201,13 +208,28 @@ def __handle_download_error(self, resp, conn):
body = tempbody
except UnicodeDecodeError: # Might be binary data
body = tempbytes
conn.close()
self.close_reusable_connection(conn)
if not body:
body = "Error " + str(resp.status)

conn.close()
self.close_reusable_connection(conn)
raise HPOneViewException(body)

def get_reusable_connection(self):
if self._reuse_connection:
if not self._conn:
logger.debug('Creating new connection')
self._conn = self.get_connection()
conn = self._conn
else:
conn = self.get_connection()
return conn

def close_reusable_connection(self, conn):
if conn:
conn.close()
self._conn = None

def get_connection(self):
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
if self._sslTrustAll is False:
Expand Down Expand Up @@ -290,7 +312,7 @@ def post_multipart(self, uri, fields, files, baseName, verbose=False):
mappedfile = mmap.mmap(inputfile.fileno(), 0, access=mmap.ACCESS_READ)
if verbose is True:
print(('Uploading ' + files + '...'))
conn = self.get_connection()
conn = self.get_reusable_connection()
# conn.set_debuglevel(1)
conn.connect()
conn.putrequest('POST', uri)
Expand All @@ -300,6 +322,8 @@ def post_multipart(self, uri, fields, files, baseName, verbose=False):
totalSize = os.path.getsize(files + '.b64')
conn.putheader('Content-Length', totalSize)
conn.putheader('X-API-Version', self._apiVersion)
if self._reuse_connection:
conn.putheader('Connection', 'keep-alive')
conn.endheaders()

while mappedfile.tell() < mappedfile.size():
Expand All @@ -313,6 +337,7 @@ def post_multipart(self, uri, fields, files, baseName, verbose=False):
mappedfile.close()
inputfile.close()
os.remove(files + '.b64')

response = conn.getresponse()
body = response.read().decode('utf-8')

Expand All @@ -322,9 +347,11 @@ def post_multipart(self, uri, fields, files, baseName, verbose=False):
except ValueError:
body = response.read().decode('utf-8')

conn.close()
if not self._reuse_connection:
self.close_reusable_connection(conn)

if response.status >= 400:
self.close_reusable_connection(conn)
raise HPOneViewException(body)

return response, body
Expand Down
7 changes: 3 additions & 4 deletions hpOneView/image_streamer/image_streamer_client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
###
# (C) Copyright (2012-2017) Hewlett Packard Enterprise Development LP
# (C) Copyright (2012-2018) 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
Expand Down Expand Up @@ -32,7 +32,6 @@

standard_library.install_aliases()


from hpOneView.connection import connection
from hpOneView.image_streamer.resources.golden_images import GoldenImages
from hpOneView.image_streamer.resources.plan_scripts import PlanScripts
Expand All @@ -44,8 +43,8 @@


class ImageStreamerClient(object):
def __init__(self, ip, session_id, api_version, sslBundle=False):
self.__connection = connection(ip, api_version, sslBundle)
def __init__(self, ip, session_id, api_version, sslBundle=False, timeout=None, reuse_connection=False):
self.__connection = connection(ip, api_version, sslBundle, timeout, reuse_connection)
self.__connection.set_session_id(session_id)
self.__golden_images = None
self.__plan_scripts = None
Expand Down
13 changes: 8 additions & 5 deletions hpOneView/oneview_client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
###
# (C) Copyright (2012-2017) Hewlett Packard Enterprise Development LP
# (C) Copyright (2012-2018) 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
Expand Down Expand Up @@ -117,7 +117,7 @@ class OneViewClient(object):

def __init__(self, config):
self.__connection = connection(config["ip"], config.get('api_version', self.DEFAULT_API_VERSION), config.get('ssl_certificate', False),
config.get('timeout'))
config.get('timeout'), config.get('reuse_connection', False))
self.__image_streamer_ip = config.get("image_streamer_ip")
self.__set_proxy(config)
self.__connection.login(config["credentials"])
Expand Down Expand Up @@ -218,7 +218,7 @@ def from_environment_variables(cls):

Allowed variables: ONEVIEWSDK_IP (required), ONEVIEWSDK_USERNAME (required), ONEVIEWSDK_PASSWORD (required),
ONEVIEWSDK_AUTH_LOGIN_DOMAIN, ONEVIEWSDK_API_VERSION, ONEVIEWSDK_IMAGE_STREAMER_IP, ONEVIEWSDK_SESSIONID, ONEVIEWSDK_SSL_CERTIFICATE,
ONEVIEWSDK_CONNECTION_TIMEOUT and ONEVIEWSDK_PROXY.
ONEVIEWSDK_CONNECTION_TIMEOUT, ONEVIEWSDK_PROXY and ONEVIEWSDK_REUSE_CONNECTION.

Returns:
OneViewClient:
Expand All @@ -233,13 +233,14 @@ def from_environment_variables(cls):
proxy = os.environ.get('ONEVIEWSDK_PROXY', '')
sessionID = os.environ.get('ONEVIEWSDK_SESSIONID', '')
timeout = os.environ.get('ONEVIEWSDK_CONNECTION_TIMEOUT')
reuse_connection = bool(os.environ.get('ONEVIEWSDK_REUSE_CONNECTION', ''))

config = dict(ip=ip,
image_streamer_ip=image_streamer_ip,
api_version=api_version,
ssl_certificate=ssl_certificate,
credentials=dict(userName=username, authLoginDomain=auth_login_domain, password=password, sessionID=sessionID),
proxy=proxy, timeout=timeout)
proxy=proxy, timeout=timeout, reuse_connection=reuse_connection)

return cls(config)

Expand Down Expand Up @@ -289,7 +290,9 @@ def create_image_streamer_client(self):
image_streamer = ImageStreamerClient(self.__image_streamer_ip,
self.__connection.get_session_id(),
self.__connection._apiVersion,
self.__connection._sslBundle)
self.__connection._sslBundle,
self.__connection._timeout,
self.__connection._reuse_connection)

return image_streamer

Expand Down
21 changes: 20 additions & 1 deletion tests/unit/test_connection.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
###
# (C) Copyright (2016-2017) Hewlett Packard Enterprise Development LP
# (C) Copyright (2016-2018) 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
Expand Down Expand Up @@ -604,6 +604,25 @@ def test_download_to_stream_when_error_status_with_empty_body(self, mock_get_con
else:
self.fail()

@patch.object(connection, 'get_connection')
def test_reuse_connection(self, mock_get_conn):
mock_conn = Mock()
mock_get_conn.return_value = mock_conn

mock_response = mock_conn.getresponse.return_value
mock_response.read.return_value = json.dumps('').encode('utf-8')
mock_response.status = 202

try:
self.connection._reuse_connection = True
self.connection.do_http('GET', '/rest', None)
self.assertEqual(self.connection._conn, mock_conn)
self.connection.close_reusable_connection(mock_conn)
self.assertEqual(self.connection._conn, None)
finally:
self.connection._reuse_connection = False
self.connection._conn = None

@patch.object(connection, 'get_connection')
def test_download_to_stream_with_timeout_error(self, mock_get_connection):

Expand Down
12 changes: 8 additions & 4 deletions tests/unit/test_oneview_client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
###
# (C) Copyright (2012-2017) Hewlett Packard Enterprise Development LP
# (C) Copyright (2012-2018) 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
Expand Down Expand Up @@ -100,7 +100,8 @@
'ONEVIEWSDK_API_VERSION': '201',
'ONEVIEWSDK_AUTH_LOGIN_DOMAIN': 'authdomain',
'ONEVIEWSDK_PROXY': '172.16.100.195:9999',
'ONEVIEWSDK_CONNECTION_TIMEOUT': '20'
'ONEVIEWSDK_CONNECTION_TIMEOUT': '20',
'ONEVIEWSDK_REUSE_CONNECTION': 'Yes'
}

OS_ENVIRON_CONFIG_FULL_WITH_SESSIONID = {
Expand All @@ -111,8 +112,8 @@
'ONEVIEWSDK_SESSIONID': '123',
'ONEVIEWSDK_API_VERSION': '201',
'ONEVIEWSDK_PROXY': '172.16.100.195:9999',
'ONEVIEWSDK_CONNECTION_TIMEOUT': '20'

'ONEVIEWSDK_CONNECTION_TIMEOUT': '20',
'ONEVIEWSDK_REUSE_CONNECTION': 'Yes'
}


Expand Down Expand Up @@ -300,6 +301,7 @@ def test_from_environment_variables_is_passing_right_arguments_to_the_constructo
mock_cls.assert_called_once_with({'api_version': 201,
'proxy': '172.16.100.195:9999',
'timeout': '20',
'reuse_connection': True,
'ip': '172.16.100.199',
'ssl_certificate': '',
'image_streamer_ip': '172.172.172.172',
Expand All @@ -317,6 +319,7 @@ def test_from_environment_variables_is_passing_right_arguments_to_the_constructo
mock_cls.assert_called_once_with({'api_version': 201,
'proxy': '172.16.100.195:9999',
'timeout': '20',
'reuse_connection': True,
'ip': '172.16.100.199',
'image_streamer_ip': '172.172.172.172',
'ssl_certificate': '',
Expand All @@ -334,6 +337,7 @@ def test_from_environment_variables_is_passing_right_arguments_to_the_constructo
mock_cls.assert_called_once_with({'api_version': 300,
'proxy': '',
'timeout': None,
'reuse_connection': False,
'ip': '172.16.100.199',
'image_streamer_ip': '',
'ssl_certificate': '',
Expand Down