# Copyright (c) 2014 NetApp, Inc. # 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. """ Client classes for web services. """ import json import requests import six.moves.urllib.parse as urlparse from cinder import exception from cinder.openstack.common import log as logging LOG = logging.getLogger(__name__) class WebserviceClient(object): """Base client for e-series web services.""" def __init__(self, scheme, host, port, service_path, username, password, **kwargs): self._validate_params(scheme, host, port) self._create_endpoint(scheme, host, port, service_path) self._username = username self._password = password self._init_connection() def _validate_params(self, scheme, host, port): """Does some basic validation for web service params.""" if host is None or port is None or scheme is None: msg = _("One of the required inputs from host, port" " or scheme not found.") raise exception.InvalidInput(reason=msg) if scheme not in ('http', 'https'): raise exception.InvalidInput(reason=_("Invalid transport type.")) def _create_endpoint(self, scheme, host, port, service_path): """Creates end point url for the service.""" netloc = '%s:%s' % (host, port) self._endpoint = urlparse.urlunparse((scheme, netloc, service_path, None, None, None)) def _init_connection(self): """Do client specific set up for session and connection pooling.""" self.conn = requests.Session() if self._username and self._password: self.conn.auth = (self._username, self._password) def invoke_service(self, method='GET', url=None, params=None, data=None, headers=None, timeout=None, verify=False): url = url or self._endpoint try: response = self.conn.request(method, url, params, data, headers=headers, timeout=timeout, verify=verify) # Catching error conditions other than the perceived ones. # Helps propagating only known exceptions back to the caller. except Exception as e: LOG.exception(_("Unexpected error while invoking web service." " Error - %s."), e) raise exception.NetAppDriverException( _("Invoking web service failed.")) self._eval_response(response) return response def _eval_response(self, response): """Evaluates response before passing result to invoker.""" pass class RestClient(WebserviceClient): """REST client specific to e-series storage service.""" def __init__(self, scheme, host, port, service_path, username, password, **kwargs): super(RestClient, self).__init__(scheme, host, port, service_path, username, password, **kwargs) kwargs = kwargs or {} self._system_id = kwargs.get('system_id') self._content_type = kwargs.get('content_type') or 'json' def set_system_id(self, system_id): """Set the storage system id.""" self._system_id = system_id def get_system_id(self): """Get the storage system id.""" return getattr(self, '_system_id', None) def _get_resource_url(self, path, use_system=True, **kwargs): """Creates end point url for rest service.""" kwargs = kwargs or {} if use_system: if not self._system_id: raise exception.NotFound(_('Storage system id not set.')) kwargs['system-id'] = self._system_id path = path.format(**kwargs) if not self._endpoint.endswith('/'): self._endpoint = '%s/' % self._endpoint return urlparse.urljoin(self._endpoint, path.lstrip('/')) def _invoke(self, method, path, data=None, use_system=True, timeout=None, verify=False, **kwargs): """Invokes end point for resource on path.""" params = {'m': method, 'p': path, 'd': data, 'sys': use_system, 't': timeout, 'v': verify, 'k': kwargs} LOG.debug(_("Invoking rest with method: %(m)s, path: %(p)s," " data: %(d)s, use_system: %(sys)s, timeout: %(t)s," " verify: %(v)s, kwargs: %(k)s.") % (params)) url = self._get_resource_url(path, use_system, **kwargs) if self._content_type == 'json': headers = {'Accept': 'application/json', 'Content-Type': 'application/json'} data = json.dumps(data) if data else None res = self.invoke_service(method, url, data=data, headers=headers, timeout=timeout, verify=verify) return res.json() if res.text else None else: raise exception.NetAppDriverException( _("Content type not supported.")) def _eval_response(self, response): """Evaluates response before passing result to invoker.""" super(RestClient, self)._eval_response(response) status_code = int(response.status_code) # codes >= 300 are not ok and to be treated as errors if status_code >= 300: # Response code 422 returns error code and message if status_code == 422: msg = _("Response error - %s.") % response.text else: msg = _("Response error code - %s.") % status_code raise exception.NetAppDriverException(msg) def create_volume(self, pool, label, size, unit='gb', seg_size=0): """Creates volume on array.""" path = "/storage-systems/{system-id}/volumes" data = {'poolId': pool, 'name': label, 'sizeUnit': unit, 'size': int(size), 'segSize': seg_size} return self._invoke('POST', path, data) def delete_volume(self, object_id): """Deletes given volume from array.""" path = "/storage-systems/{system-id}/volumes/{object-id}" return self._invoke('DELETE', path, **{'object-id': object_id}) def list_volumes(self): """Lists all volumes in storage array.""" path = "/storage-systems/{system-id}/volumes" return self._invoke('GET', path) def list_volume(self, object_id): """List given volume from array.""" path = "/storage-systems/{system-id}/volumes/{object-id}" return self._invoke('GET', path, **{'object-id': object_id}) def update_volume(self, object_id, label): """Renames given volume in array.""" path = "/storage-systems/{system-id}/volumes/{object-id}" data = {'name': label} return self._invoke('POST', path, data, **{'object-id': object_id}) def get_volume_mappings(self): """Creates volume mapping on array.""" path = "/storage-systems/{system-id}/volume-mappings" return self._invoke('GET', path) def create_volume_mapping(self, object_id, target_id, lun): """Creates volume mapping on array.""" path = "/storage-systems/{system-id}/volume-mappings" data = {'mappableObjectId': object_id, 'targetId': target_id, 'lun': lun} return self._invoke('POST', path, data) def delete_volume_mapping(self, map_object_id): """Deletes given volume mapping from array.""" path = "/storage-systems/{system-id}/volume-mappings/{object-id}" return self._invoke('DELETE', path, **{'object-id': map_object_id}) def list_hardware_inventory(self): """Lists objects in the hardware inventory.""" path = "/storage-systems/{system-id}/hardware-inventory" return self._invoke('GET', path) def list_hosts(self): """Lists host objects in the system.""" path = "/storage-systems/{system-id}/hosts" return self._invoke('GET', path) def create_host(self, label, host_type, ports=None, group_id=None): """Creates host on array.""" path = "/storage-systems/{system-id}/hosts" data = {'name': label, 'hostType': host_type} data.setdefault('groupId', group_id) if group_id else None data.setdefault('ports', ports) if ports else None return self._invoke('POST', path, data) def create_host_with_port(self, label, host_type, port_id, port_label, port_type='iscsi', group_id=None): """Creates host on array with given port information.""" port = {'type': port_type, 'port': port_id, 'label': port_label} return self.create_host(label, host_type, [port], group_id) def update_host_type(self, host_ref, host_type): """Updates host type for a given host.""" path = "/storage-systems/{system-id}/hosts/{object-id}" data = {'hostType': host_type} return self._invoke('POST', path, data, **{'object-id': host_ref}) def list_host_types(self): """Lists host types in storage system.""" path = "/storage-systems/{system-id}/host-types" return self._invoke('GET', path) def list_snapshot_groups(self): """Lists snapshot groups.""" path = "/storage-systems/{system-id}/snapshot-groups" return self._invoke('GET', path) def create_snapshot_group(self, label, object_id, storage_pool_id, repo_percent=99, warn_thres=99, auto_del_limit=0, full_policy='failbasewrites'): """Creates snapshot group on array.""" path = "/storage-systems/{system-id}/snapshot-groups" data = {'baseMappableObjectId': object_id, 'name': label, 'storagePoolId': storage_pool_id, 'repositoryPercentage': repo_percent, 'warningThreshold': warn_thres, 'autoDeleteLimit': auto_del_limit, 'fullPolicy': full_policy} return self._invoke('POST', path, data) def delete_snapshot_group(self, object_id): """Deletes given snapshot group from array.""" path = "/storage-systems/{system-id}/snapshot-groups/{object-id}" return self._invoke('DELETE', path, **{'object-id': object_id}) def create_snapshot_image(self, group_id): """Creates snapshot image in snapshot group.""" path = "/storage-systems/{system-id}/snapshot-images" data = {'groupId': group_id} return self._invoke('POST', path, data) def delete_snapshot_image(self, object_id): """Deletes given snapshot image in snapshot group.""" path = "/storage-systems/{system-id}/snapshot-images/{object-id}" return self._invoke('DELETE', path, **{'object-id': object_id}) def list_snapshot_images(self): """Lists snapshot images.""" path = "/storage-systems/{system-id}/snapshot-images" return self._invoke('GET', path) def create_snapshot_volume(self, image_id, label, base_object_id, storage_pool_id, repo_percent=99, full_thres=99, view_mode='readOnly'): """Creates snapshot volume.""" path = "/storage-systems/{system-id}/snapshot-volumes" data = {'snapshotImageId': image_id, 'fullThreshold': full_thres, 'storagePoolId': storage_pool_id, 'name': label, 'viewMode': view_mode, 'repositoryPercentage': repo_percent, 'baseMappableObjectId': base_object_id, 'repositoryPoolId': storage_pool_id} return self._invoke('POST', path, data) def delete_snapshot_volume(self, object_id): """Deletes given snapshot volume.""" path = "/storage-systems/{system-id}/snapshot-volumes/{object-id}" return self._invoke('DELETE', path, **{'object-id': object_id}) def list_storage_pools(self): """Lists storage pools in the array.""" path = "/storage-systems/{system-id}/storage-pools" return self._invoke('GET', path) def list_storage_systems(self): """Lists managed storage systems registered with web service.""" path = "/storage-systems" return self._invoke('GET', path, use_system=False) def list_storage_system(self): """List current storage system registered with web service.""" path = "/storage-systems/{system-id}" return self._invoke('GET', path) def register_storage_system(self, controller_addresses, password=None, wwn=None): """Registers storage system with web service.""" path = "/storage-systems" data = {'controllerAddresses': controller_addresses} data.setdefault('wwn', wwn) if wwn else None data.setdefault('password', password) if password else None return self._invoke('POST', path, data, use_system=False) def update_stored_system_password(self, password): """Update array password stored on web service.""" path = "/storage-systems/{system-id}" data = {'storedPassword': password} return self._invoke('POST', path, data) def create_volume_copy_job(self, src_id, tgt_id, priority='priority4', tgt_wrt_protected='true'): """Creates a volume copy job.""" path = "/storage-systems/{system-id}/volume-copy-jobs" data = {'sourceId': src_id, 'targetId': tgt_id, 'copyPriority': priority, 'targetWriteProtected': tgt_wrt_protected} return self._invoke('POST', path, data) def control_volume_copy_job(self, obj_id, control='start'): """Controls a volume copy job.""" path = ("/storage-systems/{system-id}/volume-copy-jobs-control" "/{object-id}?control={String}") return self._invoke('PUT', path, **{'object-id': obj_id, 'String': control}) def list_vol_copy_job(self, object_id): """List volume copy job.""" path = "/storage-systems/{system-id}/volume-copy-jobs/{object-id}" return self._invoke('GET', path, **{'object-id': object_id}) def delete_vol_copy_job(self, object_id): """Delete volume copy job.""" path = "/storage-systems/{system-id}/volume-copy-jobs/{object-id}" return self._invoke('DELETE', path, **{'object-id': object_id})