Refactored NexentaStor5 driver
- Failover support for both NFS and iSCSI. - Host groups for iSCSI. - Control of ZFS parent/child dependencies causing datasets to remain on NexentaStor while being removed from cinder. - Support for iSCSI multipath. - Revert to snapshot for both NFS and iSCSI. - Fix for race condition on delete volume/snapshot. Change-Id: I1a756101dbca583584de4c478c612009fa9f4596
This commit is contained in:
parent
9660f7010e
commit
c981616d59
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
||||
# Copyright 2016 Nexenta Systems, Inc.
|
||||
# Copyright 2019 Nexenta Systems, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@ -13,188 +13,604 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import requests
|
||||
import time
|
||||
import hashlib
|
||||
import json
|
||||
import posixpath
|
||||
|
||||
from eventlet import greenthread
|
||||
from oslo_log import log as logging
|
||||
from oslo_serialization import jsonutils
|
||||
import requests
|
||||
import six
|
||||
|
||||
from cinder import exception
|
||||
from cinder.i18n import _
|
||||
from cinder.utils import retry
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
TIMEOUT = 60
|
||||
|
||||
|
||||
def check_error(response):
|
||||
code = response.status_code
|
||||
if code not in (200, 201, 202):
|
||||
reason = response.reason
|
||||
body = response.content
|
||||
try:
|
||||
content = jsonutils.loads(body) if body else None
|
||||
except ValueError:
|
||||
raise exception.VolumeBackendAPIException(
|
||||
data=_(
|
||||
'Could not parse response: %(code)s %(reason)s '
|
||||
'%(content)s') % {
|
||||
'code': code, 'reason': reason, 'content': body})
|
||||
if content and 'code' in content:
|
||||
raise exception.NexentaException(content)
|
||||
raise exception.VolumeBackendAPIException(
|
||||
data=_(
|
||||
'Got bad response: %(code)s %(reason)s %(content)s') % {
|
||||
'code': code, 'reason': reason, 'content': content})
|
||||
class NefException(exception.VolumeDriverException):
|
||||
def __init__(self, data=None, **kwargs):
|
||||
defaults = {
|
||||
'name': 'NexentaError',
|
||||
'code': 'EBADMSG',
|
||||
'source': 'CinderDriver',
|
||||
'message': 'Unknown error'
|
||||
}
|
||||
if isinstance(data, dict):
|
||||
for key in defaults:
|
||||
if key in kwargs:
|
||||
continue
|
||||
if key in data:
|
||||
kwargs[key] = data[key]
|
||||
else:
|
||||
kwargs[key] = defaults[key]
|
||||
elif isinstance(data, six.string_types):
|
||||
if 'message' not in kwargs:
|
||||
kwargs['message'] = data
|
||||
for key in defaults:
|
||||
if key not in kwargs:
|
||||
kwargs[key] = defaults[key]
|
||||
message = (_('%(message)s (source: %(source)s, '
|
||||
'name: %(name)s, code: %(code)s)')
|
||||
% kwargs)
|
||||
self.code = kwargs['code']
|
||||
del kwargs['message']
|
||||
super(NefException, self).__init__(message, **kwargs)
|
||||
|
||||
|
||||
class RESTCaller(object):
|
||||
|
||||
retry_exc_tuple = (
|
||||
requests.exceptions.ConnectionError,
|
||||
requests.exceptions.ConnectTimeout
|
||||
)
|
||||
|
||||
class NefRequest(object):
|
||||
def __init__(self, proxy, method):
|
||||
self.__proxy = proxy
|
||||
self.__method = method
|
||||
self.proxy = proxy
|
||||
self.method = method
|
||||
self.path = None
|
||||
self.lock = False
|
||||
self.time = 0
|
||||
self.data = []
|
||||
self.payload = {}
|
||||
self.stat = {}
|
||||
self.hooks = {
|
||||
'response': self.hook
|
||||
}
|
||||
self.kwargs = {
|
||||
'hooks': self.hooks,
|
||||
'timeout': self.proxy.timeout
|
||||
}
|
||||
|
||||
def get_full_url(self, path):
|
||||
return '/'.join((self.__proxy.url, path))
|
||||
|
||||
@retry(retry_exc_tuple, interval=1, retries=6)
|
||||
def __call__(self, *args):
|
||||
url = self.get_full_url(args[0])
|
||||
kwargs = {'timeout': TIMEOUT, 'verify': False}
|
||||
data = None
|
||||
if len(args) > 1:
|
||||
data = args[1]
|
||||
kwargs['json'] = data
|
||||
|
||||
LOG.debug('Sending JSON data: %s, method: %s, data: %s',
|
||||
url, self.__method, data)
|
||||
|
||||
response = getattr(self.__proxy.session, self.__method)(url, **kwargs)
|
||||
check_error(response)
|
||||
content = (jsonutils.loads(response.content)
|
||||
if response.content else None)
|
||||
LOG.debug("Got response: %(code)s %(reason)s %(content)s", {
|
||||
'code': response.status_code,
|
||||
'reason': response.reason,
|
||||
'content': content})
|
||||
|
||||
if response.status_code == 202 and content:
|
||||
url = self.get_full_url(content['links'][0]['href'])
|
||||
keep_going = True
|
||||
while keep_going:
|
||||
time.sleep(1)
|
||||
response = self.__proxy.session.get(url, verify=False)
|
||||
check_error(response)
|
||||
LOG.debug("Got response: %(code)s %(reason)s", {
|
||||
'code': response.status_code,
|
||||
'reason': response.reason})
|
||||
content = response.json() if response.content else None
|
||||
keep_going = response.status_code == 202
|
||||
def __call__(self, path, payload=None):
|
||||
LOG.debug('NEF request start: %(method)s %(path)s %(payload)s',
|
||||
{'method': self.method, 'path': path, 'payload': payload})
|
||||
if self.method not in ['get', 'delete', 'put', 'post']:
|
||||
message = (_('NEF API does not support %(method)s method')
|
||||
% {'method': self.method})
|
||||
raise NefException(code='EINVAL', message=message)
|
||||
if not path:
|
||||
message = _('NEF API call requires collection path')
|
||||
raise NefException(code='EINVAL', message=message)
|
||||
self.path = path
|
||||
if payload:
|
||||
if not isinstance(payload, dict):
|
||||
message = _('NEF API call payload must be a dictionary')
|
||||
raise NefException(code='EINVAL', message=message)
|
||||
if self.method in ['get', 'delete']:
|
||||
self.payload = {'params': payload}
|
||||
elif self.method in ['put', 'post']:
|
||||
self.payload = {'data': json.dumps(payload)}
|
||||
try:
|
||||
response = self.request(self.method, self.path, **self.payload)
|
||||
except (requests.exceptions.ConnectionError,
|
||||
requests.exceptions.Timeout) as error:
|
||||
LOG.debug('Failed to %(method)s %(path)s %(payload)s: %(error)s',
|
||||
{'method': self.method, 'path': self.path,
|
||||
'payload': self.payload, 'error': error})
|
||||
if not self.failover():
|
||||
raise
|
||||
LOG.debug('Retry initial request after failover: '
|
||||
'%(method)s %(path)s %(payload)s',
|
||||
{'method': self.method,
|
||||
'path': self.path,
|
||||
'payload': self.payload})
|
||||
response = self.request(self.method, self.path, **self.payload)
|
||||
LOG.debug('NEF request done: %(method)s %(path)s %(payload)s, '
|
||||
'total response time: %(time)s seconds, '
|
||||
'total requests count: %(count)s, '
|
||||
'requests statistics: %(stat)s',
|
||||
{'method': self.method,
|
||||
'path': self.path,
|
||||
'payload': self.payload,
|
||||
'time': self.time,
|
||||
'count': sum(self.stat.values()),
|
||||
'stat': self.stat})
|
||||
if response.ok and not response.content:
|
||||
return None
|
||||
content = json.loads(response.content)
|
||||
if not response.ok:
|
||||
raise NefException(content)
|
||||
if isinstance(content, dict) and 'data' in content:
|
||||
return self.data
|
||||
return content
|
||||
|
||||
def request(self, method, path, **kwargs):
|
||||
url = self.proxy.url(path)
|
||||
LOG.debug('Perform session request: %(method)s %(url)s %(body)s',
|
||||
{'method': method, 'url': url, 'body': kwargs})
|
||||
kwargs.update(self.kwargs)
|
||||
return self.proxy.session.request(method, url, **kwargs)
|
||||
|
||||
class HTTPSAuth(requests.auth.AuthBase):
|
||||
def hook(self, response, **kwargs):
|
||||
initial_text = (_('initial request %(method)s %(path)s %(body)s')
|
||||
% {'method': self.method,
|
||||
'path': self.path,
|
||||
'body': self.payload})
|
||||
request_text = (_('session request %(method)s %(url)s %(body)s')
|
||||
% {'method': response.request.method,
|
||||
'url': response.request.url,
|
||||
'body': response.request.body})
|
||||
response_text = (_('session response %(code)s %(content)s')
|
||||
% {'code': response.status_code,
|
||||
'content': response.content})
|
||||
text = (_('%(request_text)s and %(response_text)s')
|
||||
% {'request_text': request_text,
|
||||
'response_text': response_text})
|
||||
LOG.debug('Hook start on %(text)s', {'text': text})
|
||||
|
||||
def __init__(self, url, username, password):
|
||||
self.url = url
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.token = None
|
||||
if response.status_code not in self.stat:
|
||||
self.stat[response.status_code] = 0
|
||||
self.stat[response.status_code] += 1
|
||||
self.time += response.elapsed.total_seconds()
|
||||
|
||||
def __eq__(self, other):
|
||||
return all([
|
||||
self.url == getattr(other, 'url', None),
|
||||
self.username == getattr(other, 'username', None),
|
||||
self.password == getattr(other, 'password', None),
|
||||
self.token == getattr(other, 'token', None)
|
||||
])
|
||||
if response.ok and not response.content:
|
||||
LOG.debug('Hook done on %(text)s: '
|
||||
'empty response content',
|
||||
{'text': text})
|
||||
return response
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
if not response.content:
|
||||
message = (_('There is no response content '
|
||||
'is available for %(text)s')
|
||||
% {'text': text})
|
||||
raise NefException(code='ENODATA', message=message)
|
||||
|
||||
def handle_401(self, r, **kwargs):
|
||||
if r.status_code == 401:
|
||||
LOG.debug('Got 401. Trying to reauth...')
|
||||
self.token = self.https_auth()
|
||||
# Consume content and release the original connection
|
||||
# to allow our new request to reuse the same one.
|
||||
r.content
|
||||
r.close()
|
||||
prep = r.request.copy()
|
||||
requests.cookies.extract_cookies_to_jar(
|
||||
prep._cookies, r.request, r.raw)
|
||||
prep.prepare_cookies(prep._cookies)
|
||||
try:
|
||||
content = json.loads(response.content)
|
||||
except (TypeError, ValueError) as error:
|
||||
message = (_('Failed to decode JSON for %(text)s: %(error)s')
|
||||
% {'text': text, 'error': error})
|
||||
raise NefException(code='ENOMSG', message=message)
|
||||
|
||||
prep.headers['Authorization'] = 'Bearer %s' % self.token
|
||||
_r = r.connection.send(prep, **kwargs)
|
||||
_r.history.append(r)
|
||||
_r.request = prep
|
||||
method = 'get'
|
||||
if response.status_code == requests.codes.unauthorized:
|
||||
if self.stat[response.status_code] > self.proxy.retries:
|
||||
raise NefException(content)
|
||||
self.auth()
|
||||
LOG.debug('Retry %(text)s after authentication',
|
||||
{'text': request_text})
|
||||
request = response.request.copy()
|
||||
request.headers.update(self.proxy.session.headers)
|
||||
return self.proxy.session.send(request, **kwargs)
|
||||
elif response.status_code == requests.codes.not_found:
|
||||
if self.lock:
|
||||
LOG.debug('Hook done on %(text)s: '
|
||||
'nested failover is detected',
|
||||
{'text': text})
|
||||
return response
|
||||
if self.stat[response.status_code] > self.proxy.retries:
|
||||
raise NefException(content)
|
||||
if not self.failover():
|
||||
LOG.debug('Hook done on %(text)s: '
|
||||
'no valid hosts found',
|
||||
{'text': text})
|
||||
return response
|
||||
LOG.debug('Retry %(text)s after failover',
|
||||
{'text': initial_text})
|
||||
self.data = []
|
||||
return self.request(self.method, self.path, **self.payload)
|
||||
elif response.status_code == requests.codes.server_error:
|
||||
if not (isinstance(content, dict) and
|
||||
'code' in content and
|
||||
content['code'] == 'EBUSY'):
|
||||
raise NefException(content)
|
||||
if self.stat[response.status_code] > self.proxy.retries:
|
||||
raise NefException(content)
|
||||
self.proxy.delay(self.stat[response.status_code])
|
||||
LOG.debug('Retry %(text)s after delay',
|
||||
{'text': initial_text})
|
||||
self.data = []
|
||||
return self.request(self.method, self.path, **self.payload)
|
||||
elif response.status_code == requests.codes.accepted:
|
||||
path = self.getpath(content, 'monitor')
|
||||
if not path:
|
||||
message = (_('There is no monitor path '
|
||||
'available for %(text)s')
|
||||
% {'text': text})
|
||||
raise NefException(code='ENOMSG', message=message)
|
||||
self.proxy.delay(self.stat[response.status_code])
|
||||
return self.request(method, path)
|
||||
elif response.status_code == requests.codes.ok:
|
||||
if not (isinstance(content, dict) and 'data' in content):
|
||||
LOG.debug('Hook done on %(text)s: there '
|
||||
'is no JSON data available',
|
||||
{'text': text})
|
||||
return response
|
||||
LOG.debug('Append %(count)s data items to response',
|
||||
{'count': len(content['data'])})
|
||||
self.data += content['data']
|
||||
path = self.getpath(content, 'next')
|
||||
if not path:
|
||||
LOG.debug('Hook done on %(text)s: there '
|
||||
'is no next path available',
|
||||
{'text': text})
|
||||
return response
|
||||
LOG.debug('Perform next session request %(method)s %(path)s',
|
||||
{'method': method, 'path': path})
|
||||
return self.request(method, path)
|
||||
LOG.debug('Hook done on %(text)s and '
|
||||
'returned original response',
|
||||
{'text': text})
|
||||
return response
|
||||
|
||||
return _r
|
||||
return r
|
||||
def auth(self):
|
||||
method = 'post'
|
||||
path = 'auth/login'
|
||||
payload = {
|
||||
'username': self.proxy.username,
|
||||
'password': self.proxy.password
|
||||
}
|
||||
data = json.dumps(payload)
|
||||
kwargs = {'data': data}
|
||||
self.proxy.delete_bearer()
|
||||
response = self.request(method, path, **kwargs)
|
||||
content = json.loads(response.content)
|
||||
if not (isinstance(content, dict) and 'token' in content):
|
||||
message = (_('There is no authentication token available '
|
||||
'for authentication request %(method)s %(url)s '
|
||||
'%(body)s and response %(code)s %(content)s')
|
||||
% {'method': response.request.method,
|
||||
'url': response.request.url,
|
||||
'body': response.request.body,
|
||||
'code': response.status_code,
|
||||
'content': response.content})
|
||||
raise NefException(code='ENODATA', message=message)
|
||||
token = content['token']
|
||||
self.proxy.update_token(token)
|
||||
|
||||
def __call__(self, r):
|
||||
if not self.token:
|
||||
self.token = self.https_auth()
|
||||
r.headers['Authorization'] = 'Bearer %s' % self.token
|
||||
r.register_hook('response', self.handle_401)
|
||||
return r
|
||||
def failover(self):
|
||||
result = False
|
||||
self.lock = True
|
||||
method = 'get'
|
||||
root = self.proxy.root
|
||||
for host in self.proxy.hosts:
|
||||
self.proxy.update_host(host)
|
||||
LOG.debug('Try to failover path '
|
||||
'%(root)s to host %(host)s',
|
||||
{'root': root, 'host': host})
|
||||
try:
|
||||
response = self.request(method, root)
|
||||
except (requests.exceptions.ConnectionError,
|
||||
requests.exceptions.Timeout) as error:
|
||||
LOG.debug('Skip unavailable host %(host)s '
|
||||
'due to error: %(error)s',
|
||||
{'host': host, 'error': error})
|
||||
continue
|
||||
LOG.debug('Failover result: %(code)s %(content)s',
|
||||
{'code': response.status_code,
|
||||
'content': response.content})
|
||||
if response.status_code == requests.codes.ok:
|
||||
LOG.debug('Successful failover path '
|
||||
'%(root)s to host %(host)s',
|
||||
{'root': root, 'host': host})
|
||||
self.proxy.update_lock()
|
||||
result = True
|
||||
break
|
||||
else:
|
||||
LOG.debug('Skip unsuitable host %(host)s: '
|
||||
'there is no %(root)s path found',
|
||||
{'host': host, 'root': root})
|
||||
self.lock = False
|
||||
return result
|
||||
|
||||
def https_auth(self):
|
||||
LOG.debug('Sending auth request...')
|
||||
url = '/'.join((self.url, 'auth/login'))
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
data = {'username': self.username, 'password': self.password}
|
||||
response = requests.post(url, json=data, verify=False,
|
||||
headers=headers, timeout=TIMEOUT)
|
||||
check_error(response)
|
||||
response.close()
|
||||
if response.content:
|
||||
content = jsonutils.loads(response.content)
|
||||
token = content['token']
|
||||
del content['token']
|
||||
LOG.debug("Got response: %(code)s %(reason)s %(content)s", {
|
||||
'code': response.status_code,
|
||||
'reason': response.reason,
|
||||
'content': content})
|
||||
return token
|
||||
raise exception.VolumeBackendAPIException(
|
||||
data=_(
|
||||
'Got bad response: %(code)s %(reason)s') % {
|
||||
'code': response.status_code, 'reason': response.reason})
|
||||
@staticmethod
|
||||
def getpath(content, name):
|
||||
if isinstance(content, dict) and 'links' in content:
|
||||
for link in content['links']:
|
||||
if not isinstance(link, dict):
|
||||
continue
|
||||
if 'rel' in link and 'href' in link:
|
||||
if link['rel'] == name:
|
||||
return link['href']
|
||||
return None
|
||||
|
||||
|
||||
class NexentaJSONProxy(object):
|
||||
class NefCollections(object):
|
||||
subj = 'collection'
|
||||
root = '/collections'
|
||||
|
||||
def __init__(self, host, port, user, password, use_https):
|
||||
def __init__(self, proxy):
|
||||
self.proxy = proxy
|
||||
|
||||
def path(self, name):
|
||||
quoted_name = six.moves.urllib.parse.quote_plus(name)
|
||||
return posixpath.join(self.root, quoted_name)
|
||||
|
||||
def get(self, name, payload=None):
|
||||
LOG.debug('Get properties of %(subj)s %(name)s: %(payload)s',
|
||||
{'subj': self.subj, 'name': name, 'payload': payload})
|
||||
path = self.path(name)
|
||||
return self.proxy.get(path, payload)
|
||||
|
||||
def set(self, name, payload=None):
|
||||
LOG.debug('Modify properties of %(subj)s %(name)s: %(payload)s',
|
||||
{'subj': self.subj, 'name': name, 'payload': payload})
|
||||
path = self.path(name)
|
||||
return self.proxy.put(path, payload)
|
||||
|
||||
def list(self, payload=None):
|
||||
LOG.debug('List of %(subj)ss: %(payload)s',
|
||||
{'subj': self.subj, 'payload': payload})
|
||||
return self.proxy.get(self.root, payload)
|
||||
|
||||
def create(self, payload=None):
|
||||
LOG.debug('Create %(subj)s: %(payload)s',
|
||||
{'subj': self.subj, 'payload': payload})
|
||||
try:
|
||||
return self.proxy.post(self.root, payload)
|
||||
except NefException as error:
|
||||
if error.code != 'EEXIST':
|
||||
raise
|
||||
|
||||
def delete(self, name, payload=None):
|
||||
LOG.debug('Delete %(subj)s %(name)s: %(payload)s',
|
||||
{'subj': self.subj, 'name': name, 'payload': payload})
|
||||
path = self.path(name)
|
||||
try:
|
||||
return self.proxy.delete(path, payload)
|
||||
except NefException as error:
|
||||
if error.code != 'ENOENT':
|
||||
raise
|
||||
|
||||
|
||||
class NefSettings(NefCollections):
|
||||
subj = 'setting'
|
||||
root = '/settings/properties'
|
||||
|
||||
def create(self, payload=None):
|
||||
return NotImplemented
|
||||
|
||||
def delete(self, name, payload=None):
|
||||
return NotImplemented
|
||||
|
||||
|
||||
class NefDatasets(NefCollections):
|
||||
subj = 'dataset'
|
||||
root = '/storage/datasets'
|
||||
|
||||
def rename(self, name, payload=None):
|
||||
LOG.debug('Rename %(subj)s %(name)s: %(payload)s',
|
||||
{'subj': self.subj, 'name': name, 'payload': payload})
|
||||
path = posixpath.join(self.path(name), 'rename')
|
||||
return self.proxy.post(path, payload)
|
||||
|
||||
|
||||
class NefSnapshots(NefDatasets, NefCollections):
|
||||
subj = 'snapshot'
|
||||
root = '/storage/snapshots'
|
||||
|
||||
def clone(self, name, payload=None):
|
||||
LOG.debug('Clone %(subj)s %(name)s: %(payload)s',
|
||||
{'subj': self.subj, 'name': name, 'payload': payload})
|
||||
path = posixpath.join(self.path(name), 'clone')
|
||||
return self.proxy.post(path, payload)
|
||||
|
||||
|
||||
class NefVolumeGroups(NefDatasets, NefCollections):
|
||||
subj = 'volume group'
|
||||
root = 'storage/volumeGroups'
|
||||
|
||||
def rollback(self, name, payload=None):
|
||||
LOG.debug('Rollback %(subj)s %(name)s: %(payload)s',
|
||||
{'subj': self.subj, 'name': name, 'payload': payload})
|
||||
path = posixpath.join(self.path(name), 'rollback')
|
||||
return self.proxy.post(path, payload)
|
||||
|
||||
|
||||
class NefVolumes(NefVolumeGroups, NefDatasets, NefCollections):
|
||||
subj = 'volume'
|
||||
root = '/storage/volumes'
|
||||
|
||||
def promote(self, name, payload=None):
|
||||
LOG.debug('Promote %(subj)s %(name)s: %(payload)s',
|
||||
{'subj': self.subj, 'name': name, 'payload': payload})
|
||||
path = posixpath.join(self.path(name), 'promote')
|
||||
return self.proxy.post(path, payload)
|
||||
|
||||
|
||||
class NefFilesystems(NefVolumes, NefVolumeGroups, NefDatasets, NefCollections):
|
||||
subj = 'filesystem'
|
||||
root = '/storage/filesystems'
|
||||
|
||||
def mount(self, name, payload=None):
|
||||
LOG.debug('Mount %(subj)s %(name)s: %(payload)s',
|
||||
{'subj': self.subj, 'name': name, 'payload': payload})
|
||||
path = posixpath.join(self.path(name), 'mount')
|
||||
return self.proxy.post(path, payload)
|
||||
|
||||
def unmount(self, name, payload=None):
|
||||
LOG.debug('Unmount %(subj)s %(name)s: %(payload)s',
|
||||
{'subj': self.subj, 'name': name, 'payload': payload})
|
||||
path = posixpath.join(self.path(name), 'unmount')
|
||||
return self.proxy.post(path, payload)
|
||||
|
||||
def acl(self, name, payload=None):
|
||||
LOG.debug('Set %(subj)s %(name)s ACL: %(payload)s',
|
||||
{'subj': self.subj, 'name': name, 'payload': payload})
|
||||
path = posixpath.join(self.path(name), 'acl')
|
||||
return self.proxy.post(path, payload)
|
||||
|
||||
|
||||
class NefHpr(NefCollections):
|
||||
subj = 'HPR service'
|
||||
root = '/hpr'
|
||||
|
||||
def activate(self, payload=None):
|
||||
LOG.debug('Activate %(payload)s',
|
||||
{'payload': payload})
|
||||
path = posixpath.join(self.root, 'activate')
|
||||
return self.proxy.post(path, payload)
|
||||
|
||||
def start(self, name, payload=None):
|
||||
LOG.debug('Start %(subj)s %(name)s: %(payload)s',
|
||||
{'subj': self.subj, 'name': name, 'payload': payload})
|
||||
path = posixpath.join(self.path(name), 'start')
|
||||
return self.proxy.post(path, payload)
|
||||
|
||||
|
||||
class NefServices(NefCollections):
|
||||
subj = 'service'
|
||||
root = '/services'
|
||||
|
||||
|
||||
class NefNfs(NefCollections):
|
||||
subj = 'NFS'
|
||||
root = '/nas/nfs'
|
||||
|
||||
|
||||
class NefTargets(NefCollections):
|
||||
subj = 'iSCSI target'
|
||||
root = '/san/iscsi/targets'
|
||||
|
||||
|
||||
class NefHostGroups(NefCollections):
|
||||
subj = 'host group'
|
||||
root = '/san/hostgroups'
|
||||
|
||||
|
||||
class NefTargetsGroups(NefCollections):
|
||||
subj = 'target group'
|
||||
root = '/san/targetgroups'
|
||||
|
||||
|
||||
class NefLunMappings(NefCollections):
|
||||
subj = 'LUN mapping'
|
||||
root = '/san/lunMappings'
|
||||
|
||||
|
||||
class NefLogicalUnits(NefCollections):
|
||||
subj = 'LU'
|
||||
root = 'san/logicalUnits'
|
||||
|
||||
|
||||
class NefNetAddresses(NefCollections):
|
||||
subj = 'network address'
|
||||
root = '/network/addresses'
|
||||
|
||||
|
||||
class NefProxy(object):
|
||||
def __init__(self, proto, path, conf):
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({'Content-Type': 'application/json'})
|
||||
self.host = host
|
||||
if use_https:
|
||||
self.settings = NefSettings(self)
|
||||
self.filesystems = NefFilesystems(self)
|
||||
self.volumegroups = NefVolumeGroups(self)
|
||||
self.volumes = NefVolumes(self)
|
||||
self.snapshots = NefSnapshots(self)
|
||||
self.services = NefServices(self)
|
||||
self.hpr = NefHpr(self)
|
||||
self.nfs = NefNfs(self)
|
||||
self.targets = NefTargets(self)
|
||||
self.hostgroups = NefHostGroups(self)
|
||||
self.targetgroups = NefTargetsGroups(self)
|
||||
self.mappings = NefLunMappings(self)
|
||||
self.logicalunits = NefLogicalUnits(self)
|
||||
self.netaddrs = NefNetAddresses(self)
|
||||
self.lock = None
|
||||
self.tokens = {}
|
||||
self.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-XSS-Protection': '1'
|
||||
}
|
||||
if conf.nexenta_use_https:
|
||||
self.scheme = 'https'
|
||||
self.port = port if port else 8443
|
||||
self.session.auth = HTTPSAuth(self.url, user, password)
|
||||
else:
|
||||
self.scheme = 'http'
|
||||
self.port = port if port else 8080
|
||||
self.session.auth = (user, password)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return '%(scheme)s://%(host)s:%(port)s' % {
|
||||
'scheme': self.scheme,
|
||||
'host': self.host,
|
||||
'port': self.port}
|
||||
self.username = conf.nexenta_user
|
||||
self.password = conf.nexenta_password
|
||||
self.hosts = []
|
||||
if conf.nexenta_rest_address:
|
||||
for host in conf.nexenta_rest_address.split(','):
|
||||
self.hosts.append(host.strip())
|
||||
if proto == 'nfs':
|
||||
self.root = self.filesystems.path(path)
|
||||
if not self.hosts:
|
||||
self.hosts.append(conf.nas_host)
|
||||
elif proto == 'iscsi':
|
||||
self.root = self.volumegroups.path(path)
|
||||
if not self.hosts:
|
||||
self.hosts.append(conf.nexenta_host)
|
||||
else:
|
||||
message = (_('Storage protocol %(proto)s not supported')
|
||||
% {'proto': proto})
|
||||
raise NefException(code='EPROTO', message=message)
|
||||
self.host = self.hosts[0]
|
||||
if conf.nexenta_rest_port:
|
||||
self.port = conf.nexenta_rest_port
|
||||
else:
|
||||
if conf.nexenta_use_https:
|
||||
self.port = 8443
|
||||
else:
|
||||
self.port = 8080
|
||||
self.proto = proto
|
||||
self.path = path
|
||||
self.backoff_factor = conf.nexenta_rest_backoff_factor
|
||||
self.retries = len(self.hosts) * conf.nexenta_rest_retry_count
|
||||
self.timeout = requests.packages.urllib3.util.timeout.Timeout(
|
||||
connect=conf.nexenta_rest_connect_timeout,
|
||||
read=conf.nexenta_rest_read_timeout)
|
||||
max_retries = requests.packages.urllib3.util.retry.Retry(
|
||||
total=conf.nexenta_rest_retry_count,
|
||||
backoff_factor=conf.nexenta_rest_backoff_factor)
|
||||
adapter = requests.adapters.HTTPAdapter(max_retries=max_retries)
|
||||
self.session.verify = conf.driver_ssl_cert_verify
|
||||
self.session.headers.update(self.headers)
|
||||
self.session.mount('%s://' % self.scheme, adapter)
|
||||
if not conf.driver_ssl_cert_verify:
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
self.update_lock()
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name in ('get', 'post', 'put', 'delete'):
|
||||
return RESTCaller(self, name)
|
||||
return super(NexentaJSONProxy, self).__getattribute__(name)
|
||||
return NefRequest(self, name)
|
||||
|
||||
def __repr__(self):
|
||||
return 'HTTP JSON proxy: %s' % self.url
|
||||
def delete_bearer(self):
|
||||
if 'Authorization' in self.session.headers:
|
||||
del self.session.headers['Authorization']
|
||||
|
||||
def update_bearer(self, token):
|
||||
bearer = 'Bearer %s' % token
|
||||
self.session.headers['Authorization'] = bearer
|
||||
|
||||
def update_token(self, token):
|
||||
self.tokens[self.host] = token
|
||||
self.update_bearer(token)
|
||||
|
||||
def update_host(self, host):
|
||||
self.host = host
|
||||
if host in self.tokens:
|
||||
token = self.tokens[host]
|
||||
self.update_bearer(token)
|
||||
|
||||
def update_lock(self):
|
||||
prop = self.settings.get('system.guid')
|
||||
guid = prop.get('value')
|
||||
path = '%s:%s' % (guid, self.path)
|
||||
if isinstance(path, six.text_type):
|
||||
path = path.encode('utf-8')
|
||||
self.lock = hashlib.md5(path).hexdigest()
|
||||
|
||||
def url(self, path):
|
||||
netloc = '%s:%d' % (self.host, int(self.port))
|
||||
components = (self.scheme, netloc, str(path), None, None)
|
||||
url = six.moves.urllib.parse.urlunsplit(components)
|
||||
return url
|
||||
|
||||
def delay(self, attempt):
|
||||
interval = int(self.backoff_factor * (2 ** (attempt - 1)))
|
||||
LOG.debug('Waiting for %(interval)s seconds',
|
||||
{'interval': interval})
|
||||
greenthread.sleep(interval)
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
||||
# Copyright 2016 Nexenta Systems, Inc.
|
||||
# Copyright 2019 Nexenta Systems, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@ -17,7 +17,6 @@ from oslo_config import cfg
|
||||
|
||||
from cinder.volume import configuration as conf
|
||||
|
||||
POLL_RETRIES = 5
|
||||
DEFAULT_ISCSI_PORT = 3260
|
||||
DEFAULT_HOST_GROUP = 'all'
|
||||
DEFAULT_TARGET_GROUP = 'all'
|
||||
@ -63,31 +62,53 @@ NEXENTA_EDGE_OPTS = [
|
||||
]
|
||||
|
||||
NEXENTA_CONNECTION_OPTS = [
|
||||
cfg.StrOpt('nexenta_host',
|
||||
default='',
|
||||
help='IP address of NexentaStor Appliance'),
|
||||
cfg.StrOpt('nexenta_rest_address',
|
||||
deprecated_for_removal=True,
|
||||
deprecated_reason='Rest address should now be set using '
|
||||
'the common param depending on driver type, '
|
||||
'san_ip or nas_host',
|
||||
default='',
|
||||
help='IP address of NexentaEdge management REST API endpoint'),
|
||||
cfg.StrOpt('nexenta_host',
|
||||
default='',
|
||||
help='IP address of Nexenta SA'),
|
||||
help='IP address of NexentaStor management REST API endpoint'),
|
||||
cfg.IntOpt('nexenta_rest_port',
|
||||
deprecated_for_removal=True,
|
||||
deprecated_reason='Rest address should now be set using '
|
||||
'the common param san_api_port.',
|
||||
default=0,
|
||||
help='HTTP(S) port to connect to Nexenta REST API server. '
|
||||
'If it is equal zero, 8443 for HTTPS and 8080 for HTTP '
|
||||
'is used'),
|
||||
help='HTTP(S) port to connect to NexentaStor management '
|
||||
'REST API server. If it is equal zero, 8443 for '
|
||||
'HTTPS and 8080 for HTTP is used'),
|
||||
cfg.StrOpt('nexenta_rest_protocol',
|
||||
default='auto',
|
||||
choices=['http', 'https', 'auto'],
|
||||
help='Use http or https for REST connection (default auto)'),
|
||||
help='Use http or https for NexentaStor management '
|
||||
'REST API connection (default auto)'),
|
||||
cfg.FloatOpt('nexenta_rest_connect_timeout',
|
||||
default=30,
|
||||
help='Specifies the time limit (in seconds), within '
|
||||
'which the connection to NexentaStor management '
|
||||
'REST API server must be established'),
|
||||
cfg.FloatOpt('nexenta_rest_read_timeout',
|
||||
default=300,
|
||||
help='Specifies the time limit (in seconds), '
|
||||
'within which NexentaStor management '
|
||||
'REST API server must send a response'),
|
||||
cfg.FloatOpt('nexenta_rest_backoff_factor',
|
||||
default=0.5,
|
||||
help='Specifies the backoff factor to apply '
|
||||
'between connection attempts to NexentaStor '
|
||||
'management REST API server'),
|
||||
cfg.IntOpt('nexenta_rest_retry_count',
|
||||
default=3,
|
||||
help='Specifies the number of times to repeat NexentaStor '
|
||||
'management REST API call in case of connection errors '
|
||||
'and NexentaStor appliance EBUSY or ENOENT errors'),
|
||||
cfg.BoolOpt('nexenta_use_https',
|
||||
default=True,
|
||||
help='Use secure HTTP for REST connection (default True)'),
|
||||
help='Use HTTP secure protocol for NexentaStor '
|
||||
'management REST API connections'),
|
||||
cfg.BoolOpt('nexenta_lu_writebackcache_disabled',
|
||||
default=False,
|
||||
help='Postponed write to backing store or not'),
|
||||
@ -97,24 +118,26 @@ NEXENTA_CONNECTION_OPTS = [
|
||||
'depending on the driver type: '
|
||||
'san_login or nas_login',
|
||||
default='admin',
|
||||
help='User name to connect to Nexenta SA'),
|
||||
help='User name to connect to NexentaStor '
|
||||
'management REST API server'),
|
||||
cfg.StrOpt('nexenta_password',
|
||||
deprecated_for_removal=True,
|
||||
deprecated_reason='Common password parameters should be used '
|
||||
'depending on the driver type: '
|
||||
'san_password or nas_password',
|
||||
default='nexenta',
|
||||
help='Password to connect to Nexenta SA',
|
||||
secret=True),
|
||||
help='Password to connect to NexentaStor '
|
||||
'management REST API server',
|
||||
secret=True)
|
||||
]
|
||||
|
||||
NEXENTA_ISCSI_OPTS = [
|
||||
cfg.StrOpt('nexenta_iscsi_target_portal_groups',
|
||||
default='',
|
||||
help='Nexenta target portal groups'),
|
||||
help='NexentaStor target portal groups'),
|
||||
cfg.StrOpt('nexenta_iscsi_target_portals',
|
||||
default='',
|
||||
help='Comma separated list of portals for NexentaStor5, in '
|
||||
help='Comma separated list of portals for NexentaStor5, in'
|
||||
'format of IP1:port1,IP2:port2. Port is optional, '
|
||||
'default=3260. Example: 10.10.10.1:3267,10.10.1.2'),
|
||||
cfg.StrOpt('nexenta_iscsi_target_host_group',
|
||||
@ -122,22 +145,22 @@ NEXENTA_ISCSI_OPTS = [
|
||||
help='Group of hosts which are allowed to access volumes'),
|
||||
cfg.IntOpt('nexenta_iscsi_target_portal_port',
|
||||
default=3260,
|
||||
help='Nexenta target portal port'),
|
||||
help='Nexenta appliance iSCSI target portal port'),
|
||||
cfg.IntOpt('nexenta_luns_per_target',
|
||||
default=100,
|
||||
help='Amount of iSCSI LUNs per each target'),
|
||||
help='Amount of LUNs per iSCSI target'),
|
||||
cfg.StrOpt('nexenta_volume',
|
||||
default='cinder',
|
||||
help='SA Pool that holds all volumes'),
|
||||
help='NexentaStor pool name that holds all volumes'),
|
||||
cfg.StrOpt('nexenta_target_prefix',
|
||||
default='iqn.1986-03.com.sun:02:cinder',
|
||||
help='IQN prefix for iSCSI targets'),
|
||||
help='iqn prefix for NexentaStor iSCSI targets'),
|
||||
cfg.StrOpt('nexenta_target_group_prefix',
|
||||
default='cinder',
|
||||
help='Prefix for iSCSI target groups on SA'),
|
||||
help='Prefix for iSCSI target groups on NexentaStor'),
|
||||
cfg.StrOpt('nexenta_host_group_prefix',
|
||||
default='cinder',
|
||||
help='Prefix for iSCSI host groups on SA'),
|
||||
help='Prefix for iSCSI host groups on NexentaStor'),
|
||||
cfg.StrOpt('nexenta_volume_group',
|
||||
default='iscsi',
|
||||
help='Volume group for NexentaStor5 iSCSI'),
|
||||
@ -156,6 +179,9 @@ NEXENTA_NFS_OPTS = [
|
||||
'sparsed files that take no space. If disabled '
|
||||
'(False), volume is created as a regular file, '
|
||||
'which takes a long time.'),
|
||||
cfg.BoolOpt('nexenta_qcow2_volumes',
|
||||
default=False,
|
||||
help='Create volumes as QCOW2 files rather than raw files'),
|
||||
cfg.BoolOpt('nexenta_nms_cache_volroot',
|
||||
default=True,
|
||||
help=('If set True cache NexentaStor appliance volroot option '
|
||||
@ -188,6 +214,12 @@ NEXENTA_DATASET_OPTS = [
|
||||
cfg.BoolOpt('nexenta_sparse',
|
||||
default=False,
|
||||
help='Enables or disables the creation of sparse datasets'),
|
||||
cfg.StrOpt('nexenta_origin_snapshot_template',
|
||||
default='origin-snapshot-%s',
|
||||
help='Template string to generate origin name of clone'),
|
||||
cfg.StrOpt('nexenta_group_snapshot_template',
|
||||
default='group-snapshot-%s',
|
||||
help='Template string to generate group snapshot name')
|
||||
]
|
||||
|
||||
NEXENTA_RRMGR_OPTS = [
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright 2013 Nexenta Systems, Inc.
|
||||
# Copyright 2018 Nexenta Systems, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@ -38,11 +38,12 @@ def str2size(s, scale=1024):
|
||||
|
||||
match = re.match(r'^([\.\d]+)\s*([BbKkMmGgTtPpEeZzYy]?)', s)
|
||||
if match is None:
|
||||
raise ValueError(_('Invalid value: "%s"') % s)
|
||||
raise ValueError(_('Invalid value: %(value)s')
|
||||
% {'value': s})
|
||||
|
||||
groups = match.groups()
|
||||
value = float(groups[0])
|
||||
suffix = groups[1].upper() or 'B'
|
||||
suffix = groups[1].upper() if groups[1] else 'B'
|
||||
|
||||
types = ('B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
|
||||
for i, t in enumerate(types):
|
||||
@ -61,7 +62,7 @@ def get_rrmgr_cmd(src, dst, compression=None, tcp_buf_size=None,
|
||||
"""Returns rrmgr command for source and destination."""
|
||||
cmd = ['rrmgr', '-s', 'zfs']
|
||||
if compression:
|
||||
cmd.extend(['-c', '%s' % compression])
|
||||
cmd.extend(['-c', six.text_type(compression)])
|
||||
cmd.append('-q')
|
||||
cmd.append('-e')
|
||||
if tcp_buf_size:
|
||||
@ -116,50 +117,11 @@ def parse_nms_url(url):
|
||||
return auto, scheme, user, password, host, port, '/rest/nms/'
|
||||
|
||||
|
||||
def parse_nef_url(url):
|
||||
"""Parse NMS url into normalized parts like scheme, user, host and others.
|
||||
|
||||
Example NMS URL:
|
||||
auto://admin:nexenta@192.168.1.1:8080/
|
||||
|
||||
NMS URL parts:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
auto True if url starts with auto://, protocol
|
||||
will be automatically switched to https
|
||||
if http not supported;
|
||||
scheme (auto) connection protocol (http or https);
|
||||
user (admin) NMS user;
|
||||
password (nexenta) NMS password;
|
||||
host (192.168.1.1) NMS host;
|
||||
port (8080) NMS port.
|
||||
|
||||
:param url: url string
|
||||
:return: tuple (auto, scheme, user, password, host, port)
|
||||
"""
|
||||
pr = urlparse.urlparse(url)
|
||||
scheme = pr.scheme
|
||||
auto = scheme == 'auto'
|
||||
if auto:
|
||||
scheme = 'http'
|
||||
user = 'admin'
|
||||
password = 'nexenta'
|
||||
if '@' not in pr.netloc:
|
||||
host_and_port = pr.netloc
|
||||
else:
|
||||
user_and_password, host_and_port = pr.netloc.split('@', 1)
|
||||
if ':' in user_and_password:
|
||||
user, password = user_and_password.split(':')
|
||||
else:
|
||||
user = user_and_password
|
||||
if ':' in host_and_port:
|
||||
host, port = host_and_port.split(':', 1)
|
||||
else:
|
||||
host, port = host_and_port, '8080'
|
||||
return auto, scheme, user, password, host, port
|
||||
|
||||
|
||||
def get_migrate_snapshot_name(volume):
|
||||
"""Return name for snapshot that will be used to migrate the volume."""
|
||||
return 'cinder-migrate-snapshot-%(id)s' % volume
|
||||
|
||||
|
||||
def ex2err(ex):
|
||||
"""Convert a Cinder Exception to a Nexenta Error."""
|
||||
return ex.msg
|
||||
|
@ -0,0 +1,34 @@
|
||||
---
|
||||
features:
|
||||
- Added revert to snapshot support for NexentaStor5 iSCSI and NFS drivers.
|
||||
- NexentaStor5 iSCSI and NFS drivers multiattach capability enabled.
|
||||
- Added support for creating, deleting, and updating consistency groups
|
||||
for NexentaStor5 iSCSI and NFS drivers.
|
||||
- Added support for taking, deleting, and restoring consistency group
|
||||
snapshots for NexentaStor5 iSCSI and NFS drivers.
|
||||
- Added consistency group capability to generic volume groups for
|
||||
NexentaStor5 iSCSI and NFS drivers.
|
||||
- Added volume manage/unmanage support for NexentaStor5 iSCSI and NFS
|
||||
drivers.
|
||||
- Added snapshot manage/unmanage support for NexentaStor5 iSCSI and NFS
|
||||
drivers.
|
||||
- Added the ability to list manageable volumes and snapshots for
|
||||
NexentaStor5 iSCSI and NFS drivers.
|
||||
upgrade:
|
||||
- Added a new config option ``nexenta_rest_connect_timeout``. This option
|
||||
specifies the time limit (in seconds), within which the connection to
|
||||
NexentaStor management REST API server must be established.
|
||||
- Added a new config option ``nexenta_rest_read_timeout``. This option
|
||||
specifies the time limit (in seconds), within which NexentaStor
|
||||
management REST API server must send a response.
|
||||
- Added a new config option ``nexenta_rest_backoff_factor``. This option
|
||||
specifies the backoff factor to apply between connection attempts to
|
||||
NexentaStor management REST API server.
|
||||
- Added a new config option ``nexenta_rest_retry_count``. This option
|
||||
specifies the number of times to repeat NexentaStor management REST
|
||||
API call in case of connection errors and NexentaStor appliance EBUSY
|
||||
or ENOENT errors.
|
||||
- Added a new config option ``nexenta_origin_snapshot_template``.
|
||||
This option specifies template string to generate origin name of clone.
|
||||
- Added a new config option ``nexenta_group_snapshot_template``.
|
||||
This option specifies template string to generate group snapshot name.
|
Loading…
Reference in New Issue
Block a user