Merge "Refactored NexentaStor5 driver"

This commit is contained in:
Zuul 2019-04-12 07:10:43 +00:00 committed by Gerrit Code Review
commit b33af28ebc
9 changed files with 6307 additions and 1467 deletions

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

View File

@ -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

View File

@ -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 = [

View File

@ -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

View File

@ -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.