def _verify_image_volume(self, context, image_meta, image_service): # This method just verifies that IF we have a cache volume that # it's still up to date and current WRT the image in Glance # ie an image-update hasn't occurred since we grabbed it # If it's out of date, just delete it and we'll create a new one # Any other case we don't care and just return without doing anything sfaccount = self._get_sfaccount( self.configuration.sf_template_account_name) params = {'accountID': sfaccount['accountID']} sf_vol = self._get_sf_volume(image_meta['id'], params) if sf_vol is None: return # Check updated_at field, delete copy and update if needed if sf_vol['attributes']['image_info']['image_updated_at'] ==\ image_meta['updated_at'].isoformat(): return else: # Bummer, it's been updated, delete it params = {'accountID': sfaccount['accountID']} params = {'volumeID': sf_vol['volumeID']} data = self._issue_api_request('DeleteVolume', params) if 'result' not in data: msg = _("Failed to delete SolidFire Image-Volume: %s") % data raise exception.SolidFireAPIException(msg) if not self._create_image_volume(context, image_meta, image_service, image_meta['id']): msg = _("Failed to create SolidFire Image-Volume") raise exception.SolidFireAPIException(msg)
def unmanage(self, volume): """Mark SolidFire Volume as unmanaged (export from Cinder).""" LOG.debug("Enter SolidFire unmanage...") sfaccount = self._get_sfaccount(volume['project_id']) if sfaccount is None: LOG.error(_LE("Account for Volume ID %s was not found on " "the SolidFire Cluster while attempting " "unmanage operation!") % volume['id']) raise exception.SolidFireAPIException("Failed to find account " "for volume.") params = {'accountID': sfaccount['accountID']} sf_vol = self._get_sf_volume(volume['id'], params) if sf_vol is None: raise exception.VolumeNotFound(volume_id=volume['id']) export_time = timeutils.strtime() attributes = sf_vol['attributes'] attributes['os_exported_at'] = export_time params = {'volumeID': int(sf_vol['volumeID']), 'attributes': attributes} data = self._issue_api_request('ModifyVolume', params, version='5.0') if 'result' not in data: raise exception.SolidFireAPIDataException(data=data)
def _issue_api_request(self, method, params, version='1.0', endpoint=None): if params is None: params = {} if endpoint is None: endpoint = self._endpoint payload = {'method': method, 'params': params} url = '%s/json-rpc/%s/' % (endpoint['url'], version) req = requests.post(url, data=json.dumps(payload), auth=(endpoint['login'], endpoint['passwd']), verify=False, timeout=30) response = req.json() req.close() if (('error' in response) and (response['error']['name'] in self.retryable_errors)): msg = ('Retryable error (%s) encountered during ' 'SolidFire API call.' % response['error']['name']) LOG.debug(msg) raise exception.SolidFireRetryableException(message=msg) if 'error' in response: msg = _('API response: %s') % response raise exception.SolidFireAPIException(msg) return response
def _issue_api_request(self, method_name, params): """All API requests to SolidFire device go through this method Simple json-rpc web based API calls. each call takes a set of paramaters (dict) and returns results in a dict as well. """ host = FLAGS.san_ip # For now 443 is the only port our server accepts requests on port = 443 # NOTE(john-griffith): Probably don't need this, but the idea is # we provide a request_id so we can correlate # responses with requests request_id = int(uuid.uuid4()) # just generate a random number cluster_admin = FLAGS.san_login cluster_password = FLAGS.san_password command = {'method': method_name, 'id': request_id} if params is not None: command['params'] = params payload = json.dumps(command, ensure_ascii=False) payload.encode('utf-8') # we use json-rpc, webserver needs to see json-rpc in header header = {'Content-Type': 'application/json-rpc; charset=utf-8'} if cluster_password is not None: # base64.encodestring includes a newline character # in the result, make sure we strip it off auth_key = base64.encodestring( '%s:%s' % (cluster_admin, cluster_password))[:-1] header['Authorization'] = 'Basic %s' % auth_key LOG.debug(_("Payload for SolidFire API call: %s"), payload) connection = httplib.HTTPSConnection(host, port) connection.request('POST', '/json-rpc/1.0', payload, header) response = connection.getresponse() data = {} if response.status != 200: connection.close() raise exception.SolidFireAPIException(status=response.status) else: data = response.read() try: data = json.loads(data) except (TypeError, ValueError), exc: connection.close() msg = _("Call to json.loads() raised an exception: %s") % exc raise exception.SfJsonEncodeFailure(msg) connection.close()
def manage_existing(self, volume, external_ref): """Manages an existing SolidFire Volume (import to Cinder). Renames the Volume to match the expected name for the volume. Also need to consider things like QoS, Emulation, account/tenant. """ sfid = external_ref.get('source-id', None) sfname = external_ref.get('name', None) if sfid is None: raise exception.SolidFireAPIException("Manage existing volume " "requires 'source-id'.") # First get the volume on the SF cluster (MUST be active) params = {'startVolumeID': sfid, 'limit': 1} data = self._issue_api_request('ListActiveVolumes', params) if 'result' not in data: raise exception.SolidFireAPIDataException(data=data) sf_ref = data['result']['volumes'][0] sfaccount = self._create_sfaccount(volume['project_id']) attributes = {} qos = {} if (self.configuration.sf_allow_tenant_qos and volume.get('volume_metadata') is not None): qos = self._set_qos_presets(volume) ctxt = context.get_admin_context() type_id = volume.get('volume_type_id', None) if type_id is not None: qos = self._set_qos_by_volume_type(ctxt, type_id) import_time = timeutils.strtime(volume['created_at']) attributes = { 'uuid': volume['id'], 'is_clone': 'False', 'os_imported_at': import_time, 'old_name': sfname } if qos: for k, v in qos.items(): attributes[k] = str(v) params = { 'name': volume['name'], 'volumeID': sf_ref['volumeID'], 'accountID': sfaccount['accountID'], 'enable512e': self.configuration.sf_emulate_512, 'attributes': attributes, 'qos': qos } data = self._issue_api_request('ModifyVolume', params, version='5.0') if 'result' not in data: raise exception.SolidFireAPIDataException(data=data) return self._get_model_info(sfaccount, sf_ref['volumeID'])
def _get_cluster_info(self): """Query the SolidFire cluster for some property info.""" params = {} data = self._issue_api_request('GetClusterInfo', params) if 'result' not in data: msg = _("API response: %s") % data raise exception.SolidFireAPIException(msg) return data['result']
def func_retry(*args, **kwargs): _tries, _delay = tries, delay while _tries > 1: try: return f(*args, **kwargs) except exc_tuple: time.sleep(_delay) _tries -= 1 _delay *= backoff LOG.debug('Retrying %s, (%s attempts remaining)...' % (args, _tries)) # NOTE(jdg): Don't log the params passed here # some cmds like createAccount will have sensitive # info in the params, grab only the second tuple # which should be the Method msg = (_('Retry count exceeded for command: %s') % (args[1], )) LOG.error(msg) raise exception.SolidFireAPIException(message=msg)
def manage_existing_get_size(self, volume, external_ref): """Return size of an existing LV for manage_existing. existing_ref is a dictionary of the form: {'name': <name of existing volume on SF Cluster>} """ sfid = external_ref.get('source-id', None) if sfid is None: raise exception.SolidFireAPIException("Manage existing get size " "requires 'id'.") params = {'startVolumeID': int(sfid), 'limit': 1} data = self._issue_api_request('ListActiveVolumes', params) if 'result' not in data: raise exception.SolidFireAPIDataException(data=data) sf_ref = data['result']['volumes'][0] return int(sf_ref['totalSize']) / int(units.Gi)
def _get_sf_volume(self, uuid, params): # TODO(jdg): Going to fix this shortly to not iterate # but instead use the cinder UUID and our internal # mapping to get this more efficiently data = self._issue_api_request('ListVolumesForAccount', params) if 'result' not in data: msg = _("Failed to get SolidFire Volume: %s") % data raise exception.SolidFireAPIException(msg) found_count = 0 sf_volref = None for v in data['result']['volumes']: # NOTE(jdg): In the case of "name" we can't # update that on manage/import, so we use # the uuid attribute meta = v.get('attributes') alt_id = meta.get('uuid', 'empty') if uuid in v['name'] or uuid in alt_id: found_count += 1 sf_volref = v LOG.debug("Mapped SolidFire volumeID %(sfid)s " "to cinder ID %(uuid)s." % { 'sfid': v['volumeID'], 'uuid': uuid }) if found_count == 0: # NOTE(jdg): Previously we would raise here, but there are cases # where this might be a cleanup for a failed delete. # Until we get better states we'll just log an error LOG.error(_LE("Volume %s, not found on SF Cluster."), uuid) if found_count > 1: LOG.error( _LE("Found %(count)s volumes mapped to id: %(uuid)s.") % { 'count': found_count, 'uuid': uuid }) raise exception.DuplicateSfVolumeNames(vol_name=uuid) return sf_volref
def _do_clone_volume(self, src_uuid, src_project_id, v_ref): """Create a clone of an existing volume. Currently snapshots are the same as clones on the SF cluster. Due to the way the SF cluster works there's no loss in efficiency or space usage between the two. The only thing different right now is the restore snapshot functionality which has not been implemented in the pre-release version of the SolidFire Cluster. """ attributes = {} qos = {} sfaccount = self._get_sfaccount(src_project_id) params = {'accountID': sfaccount['accountID']} sf_vol = self._get_sf_volume(src_uuid, params) if sf_vol is None: raise exception.VolumeNotFound(volume_id=src_uuid) if src_project_id != v_ref['project_id']: sfaccount = self._create_sfaccount(v_ref['project_id']) params = {'volumeID': int(sf_vol['volumeID']), 'name': 'UUID-%s' % v_ref['id'], 'newSize': int(v_ref['size'] * self.GB), 'newAccountID': sfaccount['accountID']} data = self._issue_api_request('CloneVolume', params) if (('result' not in data) or ('volumeID' not in data['result'])): msg = _("API response: %s") % data raise exception.SolidFireAPIException(msg) sf_volume_id = data['result']['volumeID'] if (self.configuration.sf_allow_tenant_qos and v_ref.get('volume_metadata')is not None): qos = self._set_qos_presets(v_ref) ctxt = context.get_admin_context() type_id = v_ref.get('volume_type_id', None) if type_id is not None: qos = self._set_qos_by_volume_type(ctxt, type_id) # NOTE(jdg): all attributes are copied via clone, need to do an update # to set any that were provided params = {'volumeID': sf_volume_id} create_time = timeutils.strtime(v_ref['created_at']) attributes = {'uuid': v_ref['id'], 'is_clone': 'True', 'src_uuid': src_uuid, 'created_at': create_time} if qos: params['qos'] = qos for k, v in qos.items(): attributes[k] = str(v) params['attributes'] = attributes data = self._issue_api_request('ModifyVolume', params) model_update = self._get_model_info(sfaccount, sf_volume_id) if model_update is None: mesg = _('Failed to get model update from clone') raise exception.SolidFireAPIException(mesg) return (data, sfaccount, model_update)