Source code for hplefthandclient.client

# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2012 Hewlett Packard Development Company, L.P.
# All Rights Reserved.
#
#    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.
"""
HPLeftHand REST Client

.. module: HPLeftHandClient
.. moduleauthor: Kurt Martin

:Author: Kurt Martin
:Description: This is the LeftHand/StoreVirtual Client that talks to the
LeftHand OS REST Service.

This client requires and works with version 11.5 of the LeftHand firmware

"""

try:
    # For Python 3.0 and later
    from urllib.parse import quote
except ImportError:
    # Fall back to Python 2's urllib2
    from urllib2 import quote

from hplefthandclient import exceptions, http


[docs]class HPLeftHandClient: # Minimum API version needed for consistency group support MIN_CG_API_VERSION = '1.2' def __init__(self, api_url, debug=False, secure=False): self.api_url = api_url self.http = http.HTTPJSONRESTClient(self.api_url, secure=secure) self.api_version = None self.debug_rest(debug)
[docs] def debug_rest(self, flag): """ This is useful for debugging requests to LeftHand :param flag: set to True to enable debugging :type flag: bool """ self.http.set_debug_flag(flag)
[docs] def login(self, username, password): """ This authenticates against the LH OS REST server and creates a session. :param username: The username :type username: str :param password: The password :type password: str :returns: None """ try: resp = self.http.authenticate(username, password) self.api_version = resp['x-api-version'] except Exception as ex: ex_desc = ex.get_description() if (ex_desc and ("Unable to find the server at" in ex_desc or "Only absolute URIs are allowed" in ex_desc)): raise exceptions.HTTPBadRequest(ex_desc) if (ex_desc and "SSL Certificate Verification Failed" in ex_desc): raise exceptions.SSLCertFailed() else: msg = ('Error: \'%s\' - Error communicating with the LeftHand ' 'API. Check proxy settings. If error persists, either ' 'the LeftHand API is not running or the version of the ' 'API is not supported.') % ex_desc raise exceptions.UnsupportedVersion(msg)
[docs] def logout(self): """ This destroys the session and logs out from the LH OS server :returns: None """ self.http.unauthenticate()
[docs] def getApiVersion(self): """ This retrieves the API version of the backend. :returns: REST API Version """ return self.api_version
[docs] def getClusters(self): """ Get the list of Clusters :returns: list of Clusters """ response, body = self.http.get('/clusters') return body
[docs] def getCluster(self, cluster_id): """ Get information about a Cluster :param cluster_id: The id of the cluster to find :type cluster_id: str :returns: cluster """ response, body = self.http.get('/clusters/%s' % cluster_id) return body
[docs] def getClusterByName(self, name): """ Get information about a cluster by name :param name: The name of the cluster to find :type name: str :returns: cluster :raises: :class:`~hplefthandclient.exceptions.HTTPNotFound` - NON_EXISTENT_CLUSTER - cluster doesn't exist """ response, body = self.http.get('/clusters?name=%s' % name) return body
[docs] def getServers(self): """ Get the list of Servers :returns: list of Servers """ response, body = self.http.get('/servers') return body
[docs] def getServer(self, server_id): """ Get information about a server :param server_id: The id of the server to find :type server_id: str :returns: server :raises: :class:`~hplefthandclient.exceptions.HTTPServerError` """ response, body = self.http.get('/servers/%s' % server_id) return body
[docs] def getServerByName(self, name): """ Get information about a server by name :param name: The name of the server to find :type name: str :returns: server :raises: :class:`~hplefthandclient.exceptions.HTTPNotFound` - NON_EXISTENT_SERVER - server doesn't exist """ response, body = self.http.get('/servers?name=%s' % name) return body
[docs] def createServer(self, name, iqn, optional=None): """ Create a server by name :param name: The name of the server to create :type name: str :param iqn: The iSCSI qualified name :type name: str :param optional: Dictionary of optional params :type optional: dict .. code-block:: python optional = { 'description' : "some comment", 'iscsiEnabled' : True, 'chapName': "some chap name", 'chapAuthenticationRequired': False, 'chapInitiatorSecret': "initiator secret", 'chapTargetSecret': "target secret", 'iscsiLoadBalancingEnabled': True, 'controllingServerName': "server name", 'fibreChannelEnabled': False, 'inServerCluster": True } :returns: server :raises: :class:`~hplefthandclient.exceptions.HTTPNotFound` - NON_EXISTENT_SERVER - server doesn't exist """ info = {'name': name, 'iscsiIQN': iqn} if optional: info = self._mergeDict(info, optional) response, body = self.http.post('/servers', body=info) return body
[docs] def deleteServer(self, server_id): """ Delete a Server :param server_id: the server ID to delete :raises: :class:`~hplefthandclient.exceptions.HTTPNotFound` - NON_EXISTENT_SERVER - The server does not exist """ response, body = self.http.delete('/servers/%s' % server_id) return body
[docs] def getSnapshots(self): """ Get the list of Snapshots :returns: list of Snapshots """ response, body = self.http.get('/snapshots') return body
[docs] def getSnapshot(self, snapshot_id): """ Get information about a Snapshot :returns: snapshot :raises: :class:`~hplefthandclient.exceptions.HTTPServerError` """ response, body = self.http.get('/snapshots/%s' % snapshot_id) return body
[docs] def getSnapshotByName(self, name): """ Get information about a snapshot by name :param name: The name of the snapshot to find :returns: volume :raises: :class:`~hplefthandclient.exceptions.HTTPNotFound` - NON_EXISTENT_SNAP - shapshot doesn't exist """ response, body = self.http.get('/snapshots?name=%s' % name) return body
[docs] def createSnapshot(self, name, source_volume_id, optional=None): """ Create a snapshot of an existing Volume :param name: Name of the Snapshot :type name: str :param source_volume_id: The volume you want to snapshot :type source_volume_id: int :param optional: Dictionary of optional params :type optional: dict .. code-block:: python optional = { 'description' : "some comment", 'inheritAccess' : false } """ parameters = {'name': name} if optional: parameters = self._mergeDict(parameters, optional) info = {'action': 'createSnapshot', 'parameters': parameters} response, body = self.http.post('/volumes/%s' % source_volume_id, body=info) return body
[docs] def createSnapshotSet(self, source_volume_id, snapshot_set, optional=None): """ Create a snapshot of multiple existing volumes :param source_volume_id: The base volume you want to snapshot. NOTE: Must be the ID of the first volume listed in snapshot_set. :type source_volume_id: int :param snapshot_set: Array of SnapshotSet entities. The 1st entry of the array will always be the current volume. :type snapshot_set: Array :param optional: Dictionary of optional params :type optional: dict .. code-block:: python snapshotSet = [ { "volumeName": "myVol1", "volumeId": 48, "snapshotName": "myVolSnapshot-0" }, { "volumeName": "myVol2", "volumeId": 58, "snapshotName": "myVolSnapshot-1" } ] optional = { 'description' : "some comment", 'inheritAccess' : false } """ # we need to be on LeftHand API version 1.2 to create a snapshot set if self.api_version < self.MIN_CG_API_VERSION: ex_msg = ('Invalid LeftHand API version found (%(found)s).' 'Version %(minimum)s or greater required.') % { 'found': self.api_version, 'minimum': self.MIN_CG_API_VERSION} raise exceptions.UnsupportedVersion(ex_msg) parameters = {'snapshotSet': snapshot_set} if optional: parameters = self._mergeDict(parameters, optional) info = {'action': 'createSnapshotSet', 'parameters': parameters} response, body = self.http.post('/volumes/%s' % source_volume_id, body=info) return body
[docs] def deleteSnapshot(self, snapshot_id): """ Delete a Snapshot :param snapshot_id: the snapshot ID to delete :raises: :class:`~hplefthandclient.exceptions.HTTPNotFound` - NON_EXISTENT_SNAPSHOT - The snapshot does not exist """ response, body = self.http.delete('/snapshots/%s' % snapshot_id) return body
[docs] def cloneSnapshot(self, name, source_snapshot_id, optional=None): """ Create a clone of an existing Shapshot :param name: Name of the Snapshot clone :type name: str :param source_snapshot_id: The snapshot you want to clone :type source_snapshot_id: int :param optional: Dictionary of optional params :type optional: dict .. code-block:: python optional = { 'description' : "some comment" } """ parameters = {'name': name} if optional: parameters = self._mergeDict(parameters, optional) info = {'action': 'createSmartClone', 'parameters': parameters} response, body = self.http.post('/snapshots/%s' % source_snapshot_id, body=info) return body
[docs] def getVolumes(self, cluster=None, fields=None): """ Get the list of Volumes :param cluster: a cluster name :type cluster: str :param fields: specific fields of the returning data :type fields: list :returns: list of Volumes """ fieldsQuery = [] query = None if fields: tmpFields = [] for field in fields: tmpFields.append(field) fieldsQuery = ('%s' % ','.join(tmpFields)) if cluster and fieldsQuery: query = ('clusterName=%(cluster)s&fields=%(fieldsQuery)s' % ({'cluster': quote(cluster.encode('utf8')), 'fieldsQuery': fieldsQuery})) elif cluster: # clusterName is documented, but not working, at this # point will get everything query = 'clusterName=%s' % quote(cluster.encode('utf8')) elif fieldsQuery: query = 'fields=%s' % fieldsQuery url = '/volumes' if query: url = '/volumes?%s' % query response, body = self.http.get(url) # Workaround for clusterName doesn't work in current API if cluster and fields and 'members[clusterName]' in fields: all_members = body['members'] cluster_members = [member for member in all_members if member['clusterName'] == cluster] body['members'] = cluster_members body['total'] = len(cluster_members) return body
[docs] def getVolume(self, volume_id, query=None): """ Get information about a volume :param volume_id: The id of the volume to find :param query: Optional query parameter, e.g. fields, expand-links :type volume_id: str :returns: volume :raises: :class:`~hplefthandclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - volume doesn't exist """ uri = '/volumes/%s' % volume_id if query: uri = uri + '?' + query response, body = self.http.get(uri) return body
[docs] def getVolumeByName(self, name): """ Get information about a volume by name :param name: The name of the volume to find :type name: str :returns: volume :raises: :class:`~hplefthandclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - volume doesn't exist """ response, body = self.http.get('/volumes?name=%s' % name) return body
[docs] def findServerVolumes(self, server_name): """Find volumes that are exported to a server. :param server_name: The name of the server to search. :type server_name: str :returns: A list of volumes that are on the specified server. """ # The only mechanism we have in 1.0 of the REST API # is to fetch all volumes and filter through them. # So we limit the return fields to cut down on the # n/w usage. # TODO(walter-boring) response, body = self.http.get( '/volumes?fields=members[name],members[volumeACL]') # Creates a list of volumes that have read/write access to the # specified server. volumes = [ self.getVolumeByName(volume['name']) # Filter out volumes that do not have the volumeACL property set. for volume in body['members'] if ('volumeACL' in volume and volume['volumeACL'] is not None) # Filter out volumes that do not have access to the target server. for entry in volume['volumeACL'] if ('server' in entry and entry['server']['name'] == server_name) ] return volumes
[docs] def createVolume(self, name, cluster_id, size, optional=None): """ Create a new volume :param name: the name of the volume :type name: str :param cluster_id: the cluster Id :type cluster_id: int :param sizeKB: size in KB for the volume :type sizeKB: int :param optional: dict of other optional items :type optional: dict .. code-block:: python optional = { 'description': 'some comment', 'isThinProvisioned': 'true', 'autogrowSeconds': 200, 'clusterName': 'somename', 'isAdaptiveOptimizationEnabled': 'true', 'dataProtectionLevel': 2, } :returns: List of Volumes :raises: :class:`~hplefthandclient.exceptions.HTTPConflict` - EXISTENT_SV - Volume Exists already """ info = {'name': name, 'clusterId': cluster_id, 'size': size} if optional: info = self._mergeDict(info, optional) response, body = self.http.post('/volumes', body=info) return body
[docs] def deleteVolume(self, volume_id): """ Delete a volume :param name: the name of the volume :type name: str :raises: :class:`~hplefthandclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - The volume does not exist """ response, body = self.http.delete('/volumes/%s' % volume_id) return body
[docs] def modifyVolume(self, volume_id, optional): """Modify an existing volume. :param volume_id: The id of the volume to find :type volume_id: str :returns: volume :raises: :class:`~hplefthandclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - volume doesn't exist """ info = {'volume_id': volume_id} info = self._mergeDict(info, optional) response, body = self.http.put('/volumes/%s' % volume_id, body=info) return body
[docs] def cloneVolume(self, name, source_volume_id, optional=None): """ Create a clone of an existing Volume :param name: Name of the Volume clone :type name: str :param source_volume_id: The Volume you want to clone :type source_volume_id: int :param optional: Dictionary of optional params :type optional: dict .. code-block:: python optional = { 'description' : "some comment" } """ parameters = {'name': name} if optional: parameters = self._mergeDict(parameters, optional) info = {'action': 'createSmartClone', 'parameters': parameters} response, body = self.http.post('/volumes/%s' % source_volume_id, body=info) return body
[docs] def addServerAccess(self, volume_id, server_id, optional=None): """ Assign a Volume to a Server :param volume_id: Volume ID of the volume :type name: int :param server_id: Server ID of the server to add the volume to :type source_volume_id: int :param optional: Dictionary of optional params :type optional: dict .. code-block:: python optional = { 'Transport' : 0, 'Lun' : 1, } """ parameters = {'serverID': server_id, 'exclusiveAccess': True, 'readAccess': True, 'writeAccess': True} if optional: parameters = self._mergeDict(parameters, optional) info = {'action': 'addServerAccess', 'parameters': parameters} response, body = self.http.post('/volumes/%s' % volume_id, body=info) return body
[docs] def removeServerAccess(self, volume_id, server_id): """ Unassign a Volume from a Server :param volume_id: Volume ID of the volume :type name: int :param server_id: Server ID of the server to remove the volume fom :type source_volume_id: int """ parameters = {'serverID': server_id} info = {'action': 'removeServerAccess', 'parameters': parameters} response, body = self.http.post('/volumes/%s' % volume_id, body=info) return body
def _mergeDict(self, dict1, dict2): """ Safely merge 2 dictionaries together :param dict1: The first dictionary :type dict1: dict :param dict2: The second dictionary :type dict2: dict :returns: dict :raises Exception: dict1, dict2 is not a dictionary """ if type(dict1) is not dict: raise Exception("dict1 is not a dictionary") if type(dict2) is not dict: raise Exception("dict2 is not a dictionary") dict3 = dict1.copy() dict3.update(dict2) return dict3