diff --git a/doc/source/admin/index.rst b/doc/source/admin/index.rst index 204ddfd06e..8d1414059d 100644 --- a/doc/source/admin/index.rst +++ b/doc/source/admin/index.rst @@ -88,4 +88,5 @@ each back end. hitachi_hnas_driver hpe_3par_driver tegile_driver + nexentastor5_driver ../configuration/shared-file-systems/drivers/windows-smb-driver diff --git a/doc/source/admin/nexentastor5_driver.rst b/doc/source/admin/nexentastor5_driver.rst new file mode 100644 index 0000000000..29eb354dda --- /dev/null +++ b/doc/source/admin/nexentastor5_driver.rst @@ -0,0 +1,104 @@ +.. + Copyright 2019 Nexenta by DDN, 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. + +NexentaStor5 Driver for OpenStack Manila +======================================== + +The `NexentaStor5 `__ Manila driver +provides NFS shared file systems to OpenStack. + +Requirements +------------ + +- The NexentaStor 5.1 or newer + +Supported shared filesystems and operations +------------------------------------------- + +This driver supports NFS shares. + +The following operations are supported: + +- Create NFS Share +- Delete NFS Share +- Allow NFS Share access + + * Only IP access type is supported for NFS (ro/rw). +- Deny NFS Share access +- Manage a share. +- Unmanage a share. +- Extend a share. +- Shrink a share. +- Create snapshot +- Revert to snapshot +- Delete snapshot +- Create share from snapshot + +Backend Configuration +--------------------- + +The following parameters need to be configured in the manila configuration +file for the NexentaStor5 driver: + +- `share_backend_name` = +- `share_driver` = manila.share.drivers.nexenta.ns5.nexenta_nas.NexentaNasDriver +- `driver_handles_share_servers` = False +- `nexenta_nas_host` = +- `nexenta_user` = +- `nexenta_password` = +- `nexenta_pool` = +- `nexenta_rest_addresses` = +- `nexenta_folder` = +- `nexenta_nfs` = True + +Share Types +----------- + +When creating a share, a share type can be specified to determine where and +how the share will be created. If a share type is not specified, the +`default_share_type` set in the manila configuration file is used. + +Manila requires that the share type includes the +`driver_handles_share_servers` extra-spec. This ensures that the share +will be created on a backend that supports the requested +driver_handles_share_servers (share networks) capability. +For the NexentaStor driver, this extra-spec's value must be set to False. + +Restrictions +------------ +- Only IP share access control is allowed for NFS shares. + + +Back-end configuration example +------------------------------ + +.. code-block:: ini + + [DEFAULT] + enabled_share_backends = NexentaStor5 + + [NexentaStor5] + share_backend_name = NexentaStor5 + driver_handles_share_servers = False + nexenta_folder = manila + share_driver = manila.share.drivers.nexenta.ns5.nexenta_nas.NexentaNasDriver + nexenta_rest_addresses = 10.3.1.1,10.3.1.2 + nexenta_nas_host = 10.3.1.10 + nexenta_rest_port = 8443 + nexenta_pool = pool1 + nexenta_nfs = True + nexenta_user = admin + nexenta_password = secret_password + nexenta_thin_provisioning = True diff --git a/doc/source/admin/share_back_ends_feature_support_mapping.rst b/doc/source/admin/share_back_ends_feature_support_mapping.rst index 306e273620..1c3f4b667c 100644 --- a/doc/source/admin/share_back_ends_feature_support_mapping.rst +++ b/doc/source/admin/share_back_ends_feature_support_mapping.rst @@ -87,7 +87,7 @@ Mapping of share drivers and share features support +----------------------------------------+-----------------------+-----------------------+--------------------------+--------------------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+ | NexentaStor4 | N | \- | N | \- | N | N | \- | \- | \- | +----------------------------------------+-----------------------+-----------------------+--------------------------+--------------------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+ -| NexentaStor5 | N | \- | N | N | N | N | \- | \- | \- | +| NexentaStor5 | N | T | N | N | N | N | \- | T | \- | +----------------------------------------+-----------------------+-----------------------+--------------------------+--------------------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+ | MapRFS | O | O | O | O | O | O | O | \- | \- | +----------------------------------------+-----------------------+-----------------------+--------------------------+--------------------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+ @@ -156,7 +156,7 @@ Mapping of share drivers and share access rules support +----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+ | NexentaStor4 | NFS (N) | \- | \- | \- | \- | NFS (N) | \- | \- | \- | \- | +----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+ -| NexentaStor5 | NFS (N) | \- | \- | \- | \- | NFS (N) | \- | \- | \- | \- | +| NexentaStor5 | NFS (N) | T | \- | \- | \- | NFS (N) | T | \- | \- | \- | +----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+ | MapRFS | \- | \- | MapRFS(O) | \- | \- | \- | \- | MapRFS(O) | \- | \- | +----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+ @@ -288,7 +288,7 @@ More information: :ref:`capabilities_and_extra_specs` +----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+ | NexentaStor4 | \- | N | N | N | N | N | \- | N | \- | \- | P | \- | +----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+ -| NexentaStor5 | \- | N | N | N | N | N | \- | N | \- | \- | P | \- | +| NexentaStor5 | \- | N | \- | N | N | N | \- | N | T | \- | P | \- | +----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+ | MapRFS | \- | N | \- | \- | \- | N | \- | O | \- | \- | P | \- | +----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+ diff --git a/doc/source/configuration/shared-file-systems/drivers.rst b/doc/source/configuration/shared-file-systems/drivers.rst index 8cd24c52f2..24dfabbacf 100644 --- a/doc/source/configuration/shared-file-systems/drivers.rst +++ b/doc/source/configuration/shared-file-systems/drivers.rst @@ -30,6 +30,7 @@ Share drivers drivers/netapp-cluster-mode-driver.rst drivers/quobyte-driver.rst drivers/windows-smb-driver.rst + drivers/nexentastor5-driver.rst To use different share drivers for the Shared File Systems service, use the diff --git a/doc/source/configuration/shared-file-systems/drivers/nexentastor5-driver.rst b/doc/source/configuration/shared-file-systems/drivers/nexentastor5-driver.rst new file mode 100644 index 0000000000..e7726f8dbe --- /dev/null +++ b/doc/source/configuration/shared-file-systems/drivers/nexentastor5-driver.rst @@ -0,0 +1,100 @@ +=================== +NexentaStor5 Driver +=================== + +Nexentastor5 can be used as a storage back end for the OpenStack Shared File +System service. Shares in the Shared File System service are mapped 1:1 +to Nexentastor5 filesystems. Access is provided via NFS protocol and IP-based +authentication. + +Network approach +~~~~~~~~~~~~~~~~ + +L3 connectivity between the storage back end and the host running the +Shared File Systems share service should exist. + + +Supported shared filesystems and operations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The drivers supports NFS shares. + +The following operations are supported: + +- Create NFS share + +- Delete share + +- Extend share + +- Shrink share + +- Allow share access + + Note the following limitation: + + * Only IP based access is supported (ro/rw). + +- Deny share access + +- Create snapshot + +- Revert to snapshot + +- Delete snapshot + +- Create share from snapshot + +- Manage share + +- Unmanage share + +Requirements +~~~~~~~~~~~~ + +- NexentaStor 5.x Appliance pre-provisioned and licensed + +- Pool and parent filesystem configured (this filesystem will contain + all manila shares) + +Restrictions +~~~~~~~~~~~~ +- Only IP share access control is allowed for NFS shares. + +Configuration +~~~~~~~~~~~~~~ + +.. code-block:: ini + + enabled_share_backends = NexentaStor5 + +Create the new back end configuration section, in this case named +``NexentaStor5``: + +.. code-block:: ini + + [NexentaStor5] + + share_backend_name = NexentaStor5 + driver_handles_share_servers = False + nexenta_folder = manila + share_driver = manila.share.drivers.nexenta.ns5.nexenta_nas.NexentaNasDriver + nexenta_rest_addresses = 10.3.1.1,10.3.1.2 + nexenta_nas_host = 10.3.1.10 + nexenta_rest_port = 8443 + nexenta_pool = pool1 + nexenta_nfs = True + nexenta_user = admin + nexenta_password = secret_password + nexenta_thin_provisioning = True + +More information can be found at the `Nexenta documentation webpage +`. + +Driver options +~~~~~~~~~~~~~~ + +The following table contains the configuration options specific to the +share driver. + +.. include:: ../../tables/manila-nexentastor5.inc diff --git a/doc/source/configuration/tables/manila-nexentastor5.inc b/doc/source/configuration/tables/manila-nexentastor5.inc new file mode 100644 index 0000000000..a1df0d28cd --- /dev/null +++ b/doc/source/configuration/tables/manila-nexentastor5.inc @@ -0,0 +1,48 @@ +.. _manila-nexentastor5: + +.. list-table:: Description of NexentaStor5 configuration options + :header-rows: 1 + :class: config-ref-table + + * - Configuration option = Default value + - Description + * - **[DEFAULT]** + - + * - ``nexenta_rest_addresses`` = ``None`` + - (List) One or more comma delimited IP addresses for management communication with NexentaStor appliance. + * - ``nexenta_rest_port`` = ``8443`` + - (Integer) Port to connect to Nexenta REST API server. + * - ``nexenta_use_https`` = ``True`` + - (Boolean) Use HTTP secure protocol for NexentaStor management REST API connections. + * - ``nexenta_user`` = ``admin`` + - (String) User name to connect to Nexenta SA. + * - ``nexenta_password`` = ``None`` + - (String) Password to connect to Nexenta SA. + * - ``nexenta_pool`` = ``pool1`` + - (String) Pool name on NexentaStor. + * - ``nexenta_nfs`` = ``True`` + - (Boolean) Defines whether share over NFS is enabled. + * - ``nexenta_ssl_cert_verify`` = ``False`` + - (Boolean) Defines whether the driver should check ssl cert. + * - ``nexenta_rest_connect_timeout`` = ``30`` + - (Float) Specifies the time limit (in seconds), within which the connection to NexentaStor management REST API server must be established. + * - ``nexenta_rest_read_timeout`` = ``300`` + - (Float) Specifies the time limit (in seconds), within which NexentaStor management REST API server must send a response. + * - ``nexenta_rest_backoff_factor`` = ``1`` + - (Float) Specifies the backoff factor to apply between connection attempts to NexentaStor management REST API server. + * - ``nexenta_rest_retry_count`` = ``5`` + - (Integer) Specifies the number of times to repeat NexentaStor management REST API call in case of connection errors and NexentaStor appliance EBUSY or ENOENT errors. + * - ``nexenta_nas_host`` = ``None`` + - (Hostname) Data IP address of Nexenta storage appliance. + * - ``nexenta_mount_point_base`` = ``$state_path/mnt`` + - (String) Base directory that contains NFS share mount points. + * - ``nexenta_share_name_prefix`` = ``share-`` + - (String) Nexenta share name prefix. + * - ``nexenta_folder`` = ``folder`` + - (String) Parent folder on NexentaStor. + * - ``nexenta_dataset_compression`` = ``on`` + - (String) Compression value for new ZFS folders. + * - ``nexenta_thin_provisioning`` = ``True`` + - (Boolean) If True shares will not be space guaranteed and overprovisioning will be enabled. + * - ``nexenta_dataset_record_size`` = ``131072`` + - (Integer) Specifies a suggested block size in for files in a file system. (bytes) diff --git a/manila/share/drivers/nexenta/ns5/jsonrpc.py b/manila/share/drivers/nexenta/ns5/jsonrpc.py index 1918543ea8..37d1354bb1 100644 --- a/manila/share/drivers/nexenta/ns5/jsonrpc.py +++ b/manila/share/drivers/nexenta/ns5/jsonrpc.py @@ -1,4 +1,4 @@ -# Copyright 2016 Nexenta Systems, Inc. +# Copyright 2019 Nexenta by DDN, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -12,137 +12,555 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -""" -:mod:`nexenta.jsonrpc` -- Nexenta-specific JSON RPC client -===================================================================== -.. automodule:: nexenta.jsonrpc -""" - -import base64 +import hashlib import json -import time +import posixpath -from oslo_log import log -from oslo_serialization import jsonutils +from eventlet import greenthread +from oslo_log import log as logging import requests -# pylint: disable=no-member,import-error -from requests.packages.urllib3 import exceptions -requests.packages.urllib3.disable_warnings(exceptions.InsecureRequestWarning) -requests.packages.urllib3.disable_warnings( - exceptions.InsecurePlatformWarning) -# pylint: enable=no-member,import-error +import six from manila import exception from manila.i18n import _ - -LOG = log.getLogger(__name__) -session = requests.Session() +LOG = logging.getLogger(__name__) -class NexentaJSONProxy(object): - def __init__(self, scheme, host, port, user, - password, method='get'): - self.scheme = scheme - self.host = host - self.port = port - self.user = user - self.password = password +class NefException(exception.ManilaException): + def __init__(self, data=None, **kwargs): + defaults = { + 'name': 'NexentaError', + 'code': 'EBADMSG', + 'source': 'ManilaDriver', + '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 NefRequest(object): + def __init__(self, proxy, 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 + } - @property - def url(self): - return '%s://%s:%s/' % (self.scheme, self.host, self.port) - - def __getattr__(self, method='get'): - if method: - return NexentaJSONProxy( - self.scheme, self.host, self.port, - self.user, self.password, method) - - def __hash__(self): - return self.url.__hash__() - - def __repr__(self): - return 'NEF proxy: %s' % self.url - - def __call__(self, path, data=None): - auth = base64.b64encode( - ('%s:%s' % (self.user, self.password)).encode('utf-8')) - url = self.url + path - - if data: - data = jsonutils.dumps(data) - - LOG.debug('Sending JSON to url: %s, data: %s, method: %s', - path, data, self.method) - session.headers.update({'Content-Type': 'application/json'}) - - response = getattr(session, self.method)( - url, data=data, verify=False) - if response.status_code in (401, 403): - LOG.debug('Login requested by NexentaStor') - if self.scheme == 'http': - session.headers.update({'Authorization': 'Basic %s' % auth}) - else: - session.headers.update( - {'Authorization': 'Bearer %s' % self.https_auth()}) - LOG.debug('Re-sending JSON to url: %s, data: %s, method: %s', - path, data, self.method) - response = getattr(session, self.method)( - url, data=data, verify=False) - self.check_error(response) - content = json.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}) - response.close() - - if response.status_code == 202 and content: - url = self.url + content['links'][0]['href'] - keep_going = True - while keep_going: - time.sleep(1) - response = session.get(url, verify=False) - self.check_error(response) - LOG.debug("Got response: %(code)s %(reason)s", { - 'code': response.status_code, - 'reason': response.reason}) - content = json.loads( - response.content) if response.content else None - keep_going = response.status_code == 202 - response.close() + 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 error + 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 https_auth(self): - url = self.url + 'auth/login' - data = jsonutils.dumps( - {'username': self.user, 'password': self.password}) - response = session.post( - url, data=data, verify=False) - content = json.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}) - response.close() - return content['token'] + 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) - def check_error(self, response): - code = response.status_code - if code not in (200, 201, 202): - reason = response.reason - content = json.loads( - response.content) if response.content else None - response.close() - if content and 'code' in content: - message = content.get( - 'message', 'Message is not specified by Nexenta REST') - raise exception.NexentaException( - reason=message, code=content['code']) - raise exception.NexentaException( - reason=_( - 'Got bad response: %(code)s %(reason)s %(content)s') % { - 'code': code, 'reason': reason, 'content': content}) + 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}) + + 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() + + if response.ok and not response.content: + LOG.debug('Hook done on %(text)s: ' + 'empty response content', + {'text': text}) + return response + + if not response.content: + message = (_('There is no response content ' + 'is available for %(text)s') + % {'text': text}) + raise NefException(code='ENODATA', message=message) + + 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) + + method = 'get' + # pylint: disable=no-member + if response.status_code == requests.codes.unauthorized: + if self.stat[response.status_code] > self.proxy.retries: + raise NefException(content) + self.auth() + request = response.request.copy() + request.headers.update(self.proxy.session.headers) + LOG.debug('Retry last %(text)s after authentication', + {'text': request_text}) + 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}) + 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}) + 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 + + 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 failover(self): + result = False + self.lock = True + method = 'get' + host = self.proxy.host + root = self.proxy.root + for item in self.proxy.hosts: + if item == host: + continue + self.proxy.update_host(item) + LOG.debug('Try to failover path ' + '%(root)s to host %(host)s', + {'root': root, 'host': item}) + 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': item, 'error': error}) + continue + LOG.debug('Failover result: %(code)s %(content)s', + {'code': response.status_code, + 'content': response.content}) + # pylint: disable=no-member + if response.status_code == requests.codes.ok: + LOG.debug('Successful failover path ' + '%(root)s to host %(host)s', + {'root': root, 'host': item}) + self.proxy.update_lock() + result = True + break + else: + LOG.debug('Skip unsuitable host %(host)s: ' + 'there is no %(root)s path found', + {'host': item, 'root': root}) + self.lock = False + return result + + @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 NefCollections(object): + subj = 'collection' + root = '/collections' + + 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 error + + 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 error + + +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 NefFilesystems(NefDatasets, NefCollections): + subj = 'filesystem' + root = '/storage/filesystems' + + 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) + + 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) + + 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 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 NefNetAddresses(NefCollections): + subj = 'network address' + root = '/network/addresses' + + +class NefProxy(object): + def __init__(self, proto, path, conf): + self.session = requests.Session() + self.settings = NefSettings(self) + self.filesystems = NefFilesystems(self) + self.snapshots = NefSnapshots(self) + self.services = NefServices(self) + self.hpr = NefHpr(self) + self.nfs = NefNfs(self) + self.netaddrs = NefNetAddresses(self) + self.proto = proto + self.path = path + self.lock = None + self.tokens = {} + self.headers = { + 'Content-Type': 'application/json', + 'X-XSS-Protection': '1' + } + if conf.nexenta_use_https: + self.scheme = 'https' + else: + self.scheme = 'http' + self.username = conf.nexenta_user + self.password = conf.nexenta_password + self.hosts = [] + if conf.nexenta_rest_addresses: + for host in conf.nexenta_rest_addresses: + self.hosts.append(host.strip()) + self.root = self.filesystems.path(path) + if not self.hosts: + self.hosts.append(conf.nexenta_nas_host) + 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.backoff_factor = conf.nexenta_rest_backoff_factor + self.retries = len(self.hosts) * conf.nexenta_rest_retry_count + self.timeout = ( + conf.nexenta_rest_connect_timeout, conf.nexenta_rest_read_timeout) + # pylint: disable=no-member + 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.nexenta_ssl_cert_verify + self.session.headers.update(self.headers) + self.session.mount('%s://' % self.scheme, adapter) + if not conf.nexenta_ssl_cert_verify: + requests.packages.urllib3.disable_warnings() + self.update_lock() + + def __getattr__(self, name): + return NefRequest(self, name) + + 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) diff --git a/manila/share/drivers/nexenta/ns5/nexenta_nas.py b/manila/share/drivers/nexenta/ns5/nexenta_nas.py index f36b819dc2..782c64d977 100644 --- a/manila/share/drivers/nexenta/ns5/nexenta_nas.py +++ b/manila/share/drivers/nexenta/ns5/nexenta_nas.py @@ -1,4 +1,4 @@ -# Copyright 2016 Nexenta Systems, Inc. +# Copyright 2019 Nexenta by DDN, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +import posixpath + from oslo_log import log from oslo_utils import units @@ -24,9 +26,9 @@ from manila.share.drivers.nexenta.ns5 import jsonrpc from manila.share.drivers.nexenta import options from manila.share.drivers.nexenta import utils -PATH_DELIMITER = '%2F' -VERSION = '1.0' +VERSION = '1.1' LOG = log.getLogger(__name__) +ZFS_MULTIPLIER = 1.1 # ZFS quotas do not take metadata into account. class NexentaNasDriver(driver.ShareDriver): @@ -35,6 +37,12 @@ class NexentaNasDriver(driver.ShareDriver): Executes commands relating to Shares. API version history: 1.0 - Initial version. + 1.1 - Failover support. + - Unshare filesystem completely after last securityContext + is removed. + - Moved all http/url code to jsonrpc. + - Manage existing support. + - Revert to snapshot support. """ driver_prefix = 'nexenta' @@ -56,21 +64,34 @@ class NexentaNasDriver(driver.ShareDriver): reason=_('Nexenta configuration missing.')) self.nef = None - self.nef_protocol = self.configuration.nexenta_rest_protocol - self.nef_host = self.configuration.nexenta_host + self.verify_ssl = self.configuration.nexenta_ssl_cert_verify + self.nas_host = self.configuration.nexenta_nas_host self.nef_port = self.configuration.nexenta_rest_port self.nef_user = self.configuration.nexenta_user self.nef_password = self.configuration.nexenta_password self.pool_name = self.configuration.nexenta_pool - self.fs_prefix = self.configuration.nexenta_nfs_share + self.parent_fs = self.configuration.nexenta_folder - self.storage_protocol = 'NFS' self.nfs_mount_point_base = self.configuration.nexenta_mount_point_base self.dataset_compression = ( self.configuration.nexenta_dataset_compression) self.provisioned_capacity = 0 + @property + def storage_protocol(self): + protocol = '' + if self.configuration.nexenta_nfs: + protocol = 'NFS' + else: + msg = _('At least 1 storage protocol must be enabled.') + raise exception.NexentaException(msg) + return protocol + + @property + def root_path(self): + return posixpath.join(self.pool_name, self.parent_fs) + @property def share_backend_name(self): if not hasattr(self, '_share_backend_name'): @@ -83,196 +104,292 @@ class NexentaNasDriver(driver.ShareDriver): return self._share_backend_name def do_setup(self, context): - """Any initialization the nexenta nas driver does while starting.""" - if self.nef_protocol == 'auto': - protocol = 'https' - else: - protocol = self.nef_protocol - self.nef = jsonrpc.NexentaJSONProxy( - protocol, self.nef_host, self.nef_port, self.nef_user, - self.nef_password) + self.nef = jsonrpc.NefProxy(self.storage_protocol, + self.root_path, + self.configuration) def check_for_setup_error(self): - """Verify that the volume for our folder exists. - - :raise: :py:exc:`LookupError` - """ - url = 'storage/pools/{}'.format(self.pool_name) - if not self.nef.get(url): - raise LookupError( - _("Pool {} does not exist in Nexenta Store appliance").format( - self.pool_name)) - url = 'storage/pools/{}/filesystems/{}'.format(self.pool_name, - self.fs_prefix) - if not self.nef.get(url): - raise LookupError( - _("filesystem {} does not exist in Nexenta Store " - "appliance").format(self.fs_prefix)) - - path = '/'.join((self.pool_name, self.fs_prefix)) - shared = False - response = self.nef.get('nas/nfs') - for share in response['data']: - if share.get('filesystem') == path: - shared = True - break - if not shared: - raise LookupError(_( - "Dataset {} is not shared in Nexenta Store appliance").format( - path)) + """Check root filesystem, NFS service and NFS share.""" + filesystem = self.nef.filesystems.get(self.root_path) + if filesystem['mountPoint'] == 'none': + message = (_('NFS root filesystem %(path)s is not writable') + % {'path': filesystem['mountPoint']}) + raise jsonrpc.NefException(code='ENOENT', message=message) + if not filesystem['isMounted']: + message = (_('NFS root filesystem %(path)s is not mounted') + % {'path': filesystem['mountPoint']}) + raise jsonrpc.NefException(code='ENOTDIR', message=message) + payload = {} + if filesystem['nonBlockingMandatoryMode']: + payload['nonBlockingMandatoryMode'] = False + if filesystem['smartCompression']: + payload['smartCompression'] = False + if payload: + self.nef.filesystems.set(self.root_path, payload) + service = self.nef.services.get('nfs') + if service['state'] != 'online': + message = (_('NFS server service is not online: %(state)s') + % {'state': service['state']}) + raise jsonrpc.NefException(code='ESRCH', message=message) self._get_provisioned_capacity() def _get_provisioned_capacity(self): - path = '%(pool)s/%(fs)s' % { - 'pool': self.pool_name, 'fs': self.fs_prefix} - url = 'storage/filesystems?parent=%s' % path - fs_list = self.nef.get(url)['data'] - for fs in fs_list: - if fs['path'] != path: - self.provisioned_capacity += fs['quotaSize'] / units.Gi + payload = {'fields': 'referencedQuotaSize'} + self.provisioned_capacity += self.nef.filesystems.get( + self.root_path, payload)['referencedQuotaSize'] + + def ensure_share(self, context, share, share_server=None): + pass def create_share(self, context, share, share_server=None): """Create a share.""" - LOG.debug('Creating share: %s.', share['name']) - data = { - 'recordSize': 4 * units.Ki, + LOG.debug('Creating share: %s.', self._get_share_name(share)) + dataset_path = self._get_dataset_path(share) + size = int(share['size'] * units.Gi * ZFS_MULTIPLIER) + payload = { + 'recordSize': self.configuration.nexenta_dataset_record_size, 'compressionMode': self.dataset_compression, - 'name': '/'.join((self.fs_prefix, share['name'])), - 'quotaSize': share['size'] * units.Gi, + 'path': dataset_path, + 'referencedQuotaSize': size, + 'nonBlockingMandatoryMode': False } if not self.configuration.nexenta_thin_provisioning: - data['reservationSize'] = share['size'] * units.Gi - - url = 'storage/pools/{}/filesystems'.format(self.pool_name) - self.nef.post(url, data) - location = { - 'path': '{}:/{}/{}/{}'.format(self.nef_host, self.pool_name, - self.fs_prefix, share['name']) - } + payload['referencedReservationSize'] = size + self.nef.filesystems.create(payload) try: - self._add_permission(share['name']) - except exception.NexentaException: + mount_path = self._mount_filesystem(share) + except jsonrpc.NefException as create_error: try: - self.delete_share(None, share) - except exception.NexentaException as exc: - LOG.warning( - "Cannot destroy created filesystem: %(vol)s/%(folder)s, " - "exception: %(exc)s", - {'vol': self.pool_name, 'folder': '/'.join( - (self.fs_prefix, share['name'])), 'exc': exc}) - raise + payload = {'force': True} + self.nef.filesystems.delete(dataset_path, payload) + except jsonrpc.NefException as delete_error: + LOG.debug('Failed to delete share %(path)s: %(error)s', + {'path': dataset_path, 'error': delete_error}) + raise create_error + self.provisioned_capacity += share['size'] + location = { + 'path': mount_path, + 'id': self._get_share_name(share) + } return [location] + def _mount_filesystem(self, share): + """Ensure that filesystem is activated and mounted on the host.""" + dataset_path = self._get_dataset_path(share) + payload = {'fields': 'mountPoint,isMounted'} + filesystem = self.nef.filesystems.get(dataset_path, payload) + if filesystem['mountPoint'] == 'none': + payload = {'datasetName': dataset_path} + self.nef.hpr.activate(payload) + filesystem = self.nef.filesystems.get(dataset_path, payload) + elif not filesystem['isMounted']: + self.nef.filesystems.mount(dataset_path) + return '%s:%s' % (self.nas_host, filesystem['mountPoint']) + def create_share_from_snapshot(self, context, share, snapshot, share_server=None): """Is called to create share from snapshot.""" - LOG.debug('Creating share from snapshot %s.', snapshot['name']) - url = ('storage/pools/%(pool)s/' - 'filesystems/%(fs)s/snapshots/%(snap)s/clone') % { - 'pool': self.pool_name, - 'fs': PATH_DELIMITER.join( - (self.fs_prefix, snapshot['share_name'])), - 'snap': snapshot['name']} - location = { - 'path': '{}:/{}/{}/{}'.format(self.nef_host, self.pool_name, - self.fs_prefix, share['name']) - } - path = '/'.join((self.pool_name, self.fs_prefix, share['name'])) - data = { - 'targetPath': path, - 'quotaSize': share['size'] * units.Gi, - 'recordSize': 4 * units.Ki, + snapshot_path = self._get_snapshot_path(snapshot) + LOG.debug('Creating share from snapshot %s.', snapshot_path) + clone_path = self._get_dataset_path(share) + size = int(share['size'] * units.Gi * ZFS_MULTIPLIER) + payload = { + 'targetPath': clone_path, + 'referencedQuotaSize': size, + 'recordSize': self.configuration.nexenta_dataset_record_size, 'compressionMode': self.dataset_compression, + 'nonBlockingMandatoryMode': False } if not self.configuration.nexenta_thin_provisioning: - data['reservationSize'] = share['size'] * units.Gi - self.nef.post(url, data) - - try: - self._add_permission(share['name']) - except exception.NexentaException: - LOG.exception( - ('Failed to add permissions for %s'), share['name']) - try: - self.delete_share(None, share) - except exception.NexentaException: - LOG.warning("Cannot destroy cloned filesystem: " - "%(vol)s/%(filesystem)s", - {'vol': self.pool_name, - 'filesystem': '/'.join( - (self.fs_prefix, share['name']))}) - raise - + payload['referencedReservationSize'] = size + self.nef.snapshots.clone(snapshot_path, payload) + self._remount_filesystem(clone_path) self.provisioned_capacity += share['size'] + try: + mount_path = self._mount_filesystem(share) + except jsonrpc.NefException as create_error: + try: + payload = {'force': True} + self.nef.filesystems.delete(clone_path, payload) + except jsonrpc.NefException as delete_error: + LOG.debug('Failed to delete share %(path)s: %(error)s', + {'path': clone_path, 'error': delete_error}) + raise create_error + + location = { + 'path': mount_path, + 'id': self._get_share_name(share) + } return [location] + def _remount_filesystem(self, clone_path): + """Workaround for NEF bug: cloned share has offline NFS status""" + self.nef.filesystems.unmount(clone_path) + self.nef.filesystems.mount(clone_path) + + def _get_dataset_path(self, share): + share_name = self._get_share_name(share) + return posixpath.join(self.root_path, share_name) + + def _get_share_name(self, share): + """Get share name with share name prefix.""" + return ('%(prefix)s%(share_id)s' % { + 'prefix': self.configuration.nexenta_share_name_prefix, + 'share_id': share['share_id']}) + + def _get_snapshot_path(self, snapshot): + """Return ZFS snapshot path for the snapshot.""" + snapshot_id = ( + snapshot['snapshot_id'] or snapshot['share_group_snapshot_id']) + share = snapshot.get('share') or snapshot.get('share_instance') + fs_path = self._get_dataset_path(share) + return '%s@snapshot-%s' % (fs_path, snapshot_id) + def delete_share(self, context, share, share_server=None): """Delete a share.""" - LOG.debug('Deleting share: %s.', share['name']) - - url = 'storage/pools/%(pool)s/filesystems/%(fs)s' % { - 'pool': self.pool_name, - 'fs': PATH_DELIMITER.join([self.fs_prefix, share['name']]), - } - self.nef.delete(url) + LOG.debug('Deleting share: %s.', self._get_share_name(share)) + share_path = self._get_dataset_path(share) + delete_payload = {'force': True, 'snapshots': True} + try: + self.nef.filesystems.delete(share_path, delete_payload) + except jsonrpc.NefException as error: + if error.code != 'EEXIST': + raise error + snapshots_tree = {} + snapshots_payload = {'parent': share_path, 'fields': 'path'} + snapshots = self.nef.snapshots.list(snapshots_payload) + for snapshot in snapshots: + clones_payload = {'fields': 'clones,creationTxg'} + data = self.nef.snapshots.get(snapshot['path'], clones_payload) + if data['clones']: + snapshots_tree[data['creationTxg']] = data['clones'][0] + if snapshots_tree: + clone_path = snapshots_tree[max(snapshots_tree)] + self.nef.filesystems.promote(clone_path) + self.nef.filesystems.delete(share_path, delete_payload) self.provisioned_capacity -= share['size'] def extend_share(self, share, new_size, share_server=None): """Extends a share.""" LOG.debug( 'Extending share: %(name)s to %(size)sG.', ( - {'name': share['name'], 'size': new_size})) - self._set_quota(share['name'], new_size) + {'name': self._get_share_name(share), 'size': new_size})) + self._set_quota(share, new_size) + if not self.configuration.nexenta_thin_provisioning: + self._set_reservation(share, new_size) self.provisioned_capacity += (new_size - share['size']) def shrink_share(self, share, new_size, share_server=None): """Shrinks size of existing share.""" LOG.debug( 'Shrinking share: %(name)s to %(size)sG.', { - 'name': share['name'], 'size': new_size}) - url = 'storage/pools/{}/filesystems/{}%2F{}'.format(self.pool_name, - self.fs_prefix, - share['name']) - used = self.nef.get(url)['bytesUsed'] / units.Gi + 'name': self._get_share_name(share), 'size': new_size}) + share_path = self._get_dataset_path(share) + share_data = self.nef.filesystems.get(share_path) + used = share_data['bytesUsedBySelf'] / units.Gi if used > new_size: raise exception.ShareShrinkingPossibleDataLoss( - share_id=share['id']) - self._set_quota(share['name'], new_size) + share_id=self._get_share_name(share)) + if not self.configuration.nexenta_thin_provisioning: + self._set_reservation(share, new_size) + self._set_quota(share, new_size) self.provisioned_capacity += (share['size'] - new_size) def create_snapshot(self, context, snapshot, share_server=None): """Create a snapshot.""" - LOG.debug('Creating a snapshot of share: %s.', snapshot['share_name']) - url = 'storage/pools/%(pool)s/filesystems/%(fs)s/snapshots' % { - 'pool': self.pool_name, - 'fs': PATH_DELIMITER.join( - (self.fs_prefix, snapshot['share_name'])), - } - data = {'name': snapshot['name']} - self.nef.post(url, data) + snapshot_path = self._get_snapshot_path(snapshot) + LOG.debug('Creating snapshot: %s.', snapshot_path) + payload = {'path': snapshot_path} + self.nef.snapshots.create(payload) def delete_snapshot(self, context, snapshot, share_server=None): - """Delete a snapshot.""" - LOG.debug('Deleting a snapshot: %(shr_name)s@%(snap_name)s.', { - 'shr_name': snapshot['share_name'], - 'snap_name': snapshot['name']}) + """Deletes a snapshot. - url = ('storage/pools/%(pool)s/filesystems/%(fs)s/snapshots/' - '%(snap)s') % {'pool': self.pool_name, - 'fs': PATH_DELIMITER.join( - (self.fs_prefix, snapshot['share_name'])), - 'snap': snapshot['name']} - try: - self.nef.delete(url) - except exception.NexentaException as e: - if e.kwargs['code'] == 'ENOENT': - LOG.warning( - 'snapshot %(name)s not found, response: %(msg)s', { - 'name': snapshot['name'], 'msg': e.msg}) - else: - raise + :param snapshot: snapshot reference + """ + snapshot_path = self._get_snapshot_path(snapshot) + LOG.debug('Deleting snapshot: %s.', snapshot_path) + payload = {'defer': True} + self.nef.snapshots.delete(snapshot_path, payload) + + def revert_to_snapshot(self, context, snapshot, share_access_rules, + snapshot_access_rules, share_server=None): + """Reverts a share (in place) to the specified snapshot. + + Does not delete the share snapshot. The share and snapshot must both + be 'available' for the restore to be attempted. The snapshot must be + the most recent one taken by Manila; the API layer performs this check + so the driver doesn't have to. + + The share must be reverted in place to the contents of the snapshot. + Application admins should quiesce or otherwise prepare the application + for the shared file system contents to change suddenly. + + :param context: Current context + :param snapshot: The snapshot to be restored + :param share_access_rules: List of all access rules for the affected + share + :param snapshot_access_rules: List of all access rules for the affected + snapshot + :param share_server: Optional -- Share server model or None + """ + snapshot_path = self._get_snapshot_path(snapshot).split('@')[1] + LOG.debug('Reverting to snapshot: %s.', snapshot_path) + share_path = self._get_dataset_path(snapshot['share']) + payload = {'snapshot': snapshot_path} + self.nef.filesystems.rollback(share_path, payload) + + def manage_existing(self, share, driver_options): + """Brings an existing share under Manila management. + + If the provided share is not valid, then raise a + ManageInvalidShare exception, specifying a reason for the failure. + + If the provided share is not in a state that can be managed, such as + being replicated on the backend, the driver *MUST* raise + ManageInvalidShare exception with an appropriate message. + + The share has a share_type, and the driver can inspect that and + compare against the properties of the referenced backend share. + If they are incompatible, raise a + ManageExistingShareTypeMismatch, specifying a reason for the failure. + + :param share: Share model + :param driver_options: Driver-specific options provided by admin. + :return: share_update dictionary with required key 'size', + which should contain size of the share. + """ + LOG.debug('Manage share %s.', self._get_share_name(share)) + export_path = share['export_locations'][0]['path'] + + # check that filesystem with provided export exists. + fs_path = export_path.split(':/')[1] + fs_data = self.nef.filesystems.get(fs_path) + + if not fs_data: + # wrong export path, raise exception. + msg = _('Share %s does not exist on Nexenta Store appliance, ' + 'cannot manage.') % export_path + raise exception.NexentaException(msg) + + # get dataset properties. + if fs_data['referencedQuotaSize']: + size = (fs_data['referencedQuotaSize'] / units.Gi) + 1 + else: + size = fs_data['bytesReferenced'] / units.Gi + 1 + # rename filesystem on appliance to correlate with manila ID. + new_path = '%s/%s' % (self.root_path, self._get_share_name(share)) + self.nef.filesystems.rename(fs_path, {'newPath': new_path}) + # make sure quotas and reservations are correct. + if not self.configuration.nexenta_thin_provisioning: + self._set_reservation(share, size) + self._set_quota(share, size) + + return {'size': size, 'export_locations': [{ + 'path': '%s:/%s' % (self.nas_host, new_path) + }]} def update_access(self, context, share, access_rules, add_rules, delete_rules, share_server=None): @@ -292,68 +409,112 @@ class NexentaNasDriver(driver.ShareDriver): :param share_server: Data structure with share server information. Not used by this driver. """ - LOG.debug('Updating access to share %s.', share) + LOG.debug('Updating access to share %(id)s with following access ' + 'rules: %(rules)s', { + 'id': self._get_share_name(share), + 'rules': [( + rule.get('access_type'), rule.get('access_level'), + rule.get('access_to')) for rule in access_rules]}) rw_list = [] ro_list = [] - security_contexts = [] - for rule in access_rules: - if rule['access_type'].lower() != 'ip': - msg = _('Only IP access type is supported.') - raise exception.InvalidShareAccess(reason=msg) - else: + update_dict = {} + if share['share_proto'] == 'NFS': + for rule in access_rules: + if rule['access_type'].lower() != 'ip': + msg = _( + 'Only IP access control type is supported for NFS.') + LOG.warning(msg) + update_dict[rule['access_id']] = { + 'state': 'error', + } + else: + update_dict[rule['access_id']] = { + 'state': 'active', + } if rule['access_level'] == common.ACCESS_LEVEL_RW: rw_list.append(rule['access_to']) else: ro_list.append(rule['access_to']) + self._update_nfs_access(share, rw_list, ro_list) + return update_dict + + def _update_nfs_access(self, share, rw_list, ro_list): + # Define allowed security context types to be able to tell whether + # the 'security_contexts' dict contains any rules at all + context_types = {'none', 'root', 'readOnlyList', 'readWriteList'} + + security_contexts = {'securityModes': ['sys']} + + def add_sc(addr_list, sc_type): + if sc_type not in context_types: + return + + rule_list = [] - def append_sc(addr_list, sc_type): for addr in addr_list: address_mask = addr.strip().split('/', 1) address = address_mask[0] - ls = [{"allow": True, "etype": "network", "entity": address}] + ls = {"allow": True, "etype": "fqdn", "entity": address} if len(address_mask) == 2: - try: - mask = int(address_mask[1]) - if mask != 32: - ls[0]['mask'] = mask - except Exception: - raise exception.InvalidInput( - reason=_( - '<{}> is not a valid access parameter').format( - addr)) - new_sc = {"securityModes": ["sys"]} - new_sc[sc_type] = ls - security_contexts.append(new_sc) + mask = int(address_mask[1]) + if 0 <= mask < 31: + ls['mask'] = mask + ls['etype'] = 'network' + rule_list.append(ls) - append_sc(rw_list, 'readWriteList') - append_sc(ro_list, 'readOnlyList') - data = {"securityContexts": security_contexts} - url = 'nas/nfs/' + PATH_DELIMITER.join( - (self.pool_name, self.fs_prefix, share['name'])) - self.nef.put(url, data) + # Context type with no addresses will result in an API error + if rule_list: + security_contexts[sc_type] = rule_list - def _set_quota(self, share_name, new_size): - quota = new_size * units.Gi - data = {'quotaSize': quota} - if not self.configuration.nexenta_thin_provisioning: - data['reservationSize'] = quota - url = 'storage/pools/{}/filesystems/{}%2F{}'.format(self.pool_name, - self.fs_prefix, - share_name) - self.nef.put(url, data) + add_sc(rw_list, 'readWriteList') + add_sc(ro_list, 'readOnlyList') + payload = {'securityContexts': [security_contexts]} + share_path = self._get_dataset_path(share) + if self.nef.nfs.list({'filesystem': share_path}): + if not set(security_contexts.keys()) & context_types: + self.nef.nfs.delete(share_path) + else: + self.nef.nfs.set(share_path, payload) + else: + payload['filesystem'] = share_path + self.nef.nfs.create(payload) + payload = { + 'flags': ['file_inherit', 'dir_inherit'], + 'permissions': ['full_set'], + 'principal': 'everyone@', + 'type': 'allow' + } + self.nef.filesystems.acl(share_path, payload) + + def _set_quota(self, share, new_size): + quota = int(new_size * units.Gi * ZFS_MULTIPLIER) + share_path = self._get_dataset_path(share) + payload = {'referencedQuotaSize': quota} + LOG.debug('Setting quota for dataset %s.', share_path) + self.nef.filesystems.set(share_path, payload) + + def _set_reservation(self, share, new_size): + res_size = int(new_size * units.Gi * ZFS_MULTIPLIER) + share_path = self._get_dataset_path(share) + payload = {'referencedReservationSize': res_size} + self.nef.filesystems.set(share_path, payload) def _update_share_stats(self, data=None): super(NexentaNasDriver, self)._update_share_stats() total, free, allocated = self._get_capacity_info() - + compression = not self.dataset_compression == 'off' data = { 'vendor_name': 'Nexenta', 'storage_protocol': self.storage_protocol, 'share_backend_name': self.share_backend_name, 'nfs_mount_point_base': self.nfs_mount_point_base, 'driver_version': VERSION, + 'snapshot_support': True, + 'create_share_from_snapshot_support': True, + 'revert_to_snapshot_support': True, 'pools': [{ 'pool_name': self.pool_name, + 'compression': compression, 'total_capacity_gb': total, 'free_capacity_gb': free, 'reserved_percentage': ( @@ -370,53 +531,8 @@ class NexentaNasDriver(driver.ShareDriver): def _get_capacity_info(self): """Calculate available space on the NFS share.""" - url = 'storage/pools/{}/filesystems/{}'.format(self.pool_name, - self.fs_prefix) - data = self.nef.get(url) - total = utils.bytes_to_gb(data['bytesAvailable']) - allocated = utils.bytes_to_gb(data['bytesUsed']) - free = total - allocated + data = self.nef.filesystems.get(self.root_path) + free = int(utils.bytes_to_gb(data['bytesAvailable'])) + allocated = int(utils.bytes_to_gb(data['bytesUsed'])) + total = free + allocated return total, free, allocated - - def _add_permission(self, share_name): - """Share NFS filesystem on NexentaStor Appliance. - - :param share_name: relative filesystem name to be shared - """ - LOG.debug( - 'Creating RW ACE for filesystem everyone on Nexenta Store ' - 'for <%s> filesystem.', share_name) - url = 'storage/pools/{}/filesystems/{}/acl'.format( - self.pool_name, PATH_DELIMITER.join((self.fs_prefix, share_name))) - data = { - "type": "allow", - "principal": "everyone@", - "permissions": [ - "list_directory", - "read_data", - "add_file", - "write_data", - "add_subdirectory", - "append_data", - "read_xattr", - "write_xattr", - "execute", - "delete_child", - "read_attributes", - "write_attributes", - "delete", - "read_acl", - "write_acl", - "write_owner", - "synchronize", - ], - "flags": [ - "file_inherit", - "dir_inherit", - ], - } - self.nef.post(url, data) - - LOG.debug( - 'RW ACE for filesystem <%s> on Nexenta Store has been ' - 'successfully created.', share_name) diff --git a/manila/share/drivers/nexenta/options.py b/manila/share/drivers/nexenta/options.py index a5ac758983..44f11156f1 100644 --- a/manila/share/drivers/nexenta/options.py +++ b/manila/share/drivers/nexenta/options.py @@ -1,4 +1,4 @@ -# Copyright 2016 Nexenta Systems, Inc. +# Copyright 2019 Nexenta by DDN, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -23,36 +23,68 @@ from oslo_config import cfg nexenta_connection_opts = [ - cfg.HostAddressOpt('nexenta_host', - help='IP address of Nexenta storage appliance.'), + cfg.ListOpt('nexenta_rest_addresses', + help='One or more comma delimited IP addresses for management ' + 'communication with NexentaStor appliance.'), cfg.IntOpt('nexenta_rest_port', - default=8457, + default=8443, help='Port to connect to Nexenta REST API server.'), - cfg.IntOpt('nexenta_retry_count', - default=6, - help='Number of retries for unsuccessful API calls.'), cfg.StrOpt('nexenta_rest_protocol', default='auto', choices=['http', 'https', 'auto'], help='Use http or https for REST connection (default auto).'), + cfg.BoolOpt('nexenta_use_https', + default=True, + help='Use HTTP secure protocol for NexentaStor ' + 'management REST API connections'), cfg.StrOpt('nexenta_user', default='admin', - help='User name to connect to Nexenta SA.'), + help='User name to connect to Nexenta SA.', + required=True), cfg.StrOpt('nexenta_password', help='Password to connect to Nexenta SA.', + required=True, secret=True), cfg.StrOpt('nexenta_volume', default='volume1', help='Volume name on NexentaStor.'), cfg.StrOpt('nexenta_pool', default='pool1', + required=True, help='Pool name on NexentaStor.'), cfg.BoolOpt('nexenta_nfs', default=True, - help='On if share over NFS is enabled.'), + help='Defines whether share over NFS is enabled.'), + cfg.BoolOpt('nexenta_ssl_cert_verify', + default=False, + help='Defines whether the driver should check ssl cert.'), + 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=1, + help='Specifies the backoff factor to apply ' + 'between connection attempts to NexentaStor ' + 'management REST API server'), + cfg.IntOpt('nexenta_rest_retry_count', + default=5, + 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'), ] nexenta_nfs_opts = [ + cfg.HostAddressOpt('nexenta_nas_host', + deprecated_name='nexenta_host', + help='Data IP address of Nexenta storage appliance.', + required=True), cfg.StrOpt('nexenta_mount_point_base', default='$state_path/mnt', help='Base directory that contains NFS share mount points.'), @@ -61,6 +93,14 @@ nexenta_nfs_opts = [ nexenta_dataset_opts = [ cfg.StrOpt('nexenta_nfs_share', default='nfs_share', + help='Parent filesystem where all the shares will be created. ' + 'This parameter is only used by NexentaStor4 driver.'), + cfg.StrOpt('nexenta_share_name_prefix', + help='Nexenta share name prefix.', + default='share-'), + cfg.StrOpt('nexenta_folder', + default='folder', + required=True, help='Parent folder on NexentaStor.'), cfg.StrOpt('nexenta_dataset_compression', default='on', @@ -71,9 +111,14 @@ nexenta_dataset_opts = [ cfg.StrOpt('nexenta_dataset_dedupe', default='off', choices=['on', 'off', 'sha256', 'verify', 'sha256, verify'], - help='Deduplication value for new ZFS folders.'), + help='Deduplication value for new ZFS folders. ' + 'Only used by NexentaStor4 driver.'), cfg.BoolOpt('nexenta_thin_provisioning', default=True, help=('If True shares will not be space guaranteed and ' 'overprovisioning will be enabled.')), + cfg.IntOpt('nexenta_dataset_record_size', + default=131072, + help='Specifies a suggested block size in for files in a file ' + 'system. (bytes)'), ] diff --git a/manila/share/drivers/nexenta/utils.py b/manila/share/drivers/nexenta/utils.py index 55f0dcbdcc..af4ee8c690 100644 --- a/manila/share/drivers/nexenta/utils.py +++ b/manila/share/drivers/nexenta/utils.py @@ -1,4 +1,4 @@ -# Copyright 2016 Nexenta Systems, Inc. +# Copyright 2019 Nexenta by DDN, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/manila/tests/share/drivers/nexenta/ns5/test_jsonrpc.py b/manila/tests/share/drivers/nexenta/ns5/test_jsonrpc.py index 0bdb79b091..01cdfee93a 100644 --- a/manila/tests/share/drivers/nexenta/ns5/test_jsonrpc.py +++ b/manila/tests/share/drivers/nexenta/ns5/test_jsonrpc.py @@ -1,4 +1,4 @@ -# Copyright 2016 Nexenta Systems, Inc. +# Copyright 2019 Nexenta by DDN, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -12,118 +12,1167 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +""" +Unit tests for NexentaStor 5 REST API helper +""" -from mock import patch -from oslo_serialization import jsonutils +import copy +import hashlib +import json +import posixpath +import uuid + +import mock import requests +import six -from manila import exception +from manila.share import configuration as conf from manila.share.drivers.nexenta.ns5 import jsonrpc from manila import test -PATH_TO_RPC = 'manila.share.drivers.nexenta.ns5.jsonrpc.NexentaJSONProxy' + +class FakeNefProxy(object): + + def __init__(self): + self.scheme = 'https' + self.port = 8443 + self.hosts = ['1.1.1.1', '2.2.2.2'] + self.host = self.hosts[0] + self.root = 'pool/share' + self.username = 'username' + self.password = 'password' + self.retries = 3 + self.timeout = 5 + self.session = mock.Mock() + self.session.headers = {} + + def __getattr__(self, name): + pass + + def delay(self, interval): + pass + + def delete_bearer(self): + pass + + def update_lock(self): + pass + + def update_token(self, token): + pass + + def update_host(self, host): + pass + + def url(self, path): + return '%s://%s:%s/%s' % (self.scheme, self.host, self.port, path) -class TestNexentaJSONProxy(test.TestCase): +class TestNefException(test.TestCase): - def __init__(self, method): - super(TestNexentaJSONProxy, self).__init__(method) + def test_message(self): + message = 'test message 1' + result = jsonrpc.NefException(message) + self.assertIn(message, result.msg) + + def test_message_kwargs(self): + code = 'EAGAIN' + message = 'test message 2' + result = jsonrpc.NefException(message, code=code) + self.assertEqual(code, result.code) + self.assertIn(message, result.msg) + + def test_no_message_kwargs(self): + code = 'ESRCH' + message = 'test message 3' + result = jsonrpc.NefException(None, code=code, message=message) + self.assertEqual(code, result.code) + self.assertIn(message, result.msg) + + def test_message_plus_kwargs(self): + code = 'ENODEV' + message1 = 'test message 4' + message2 = 'test message 5' + result = jsonrpc.NefException(message1, code=code, message=message2) + self.assertEqual(code, result.code) + self.assertIn(message2, result.msg) + + def test_dict(self): + code = 'ENOENT' + message = 'test message 4' + result = jsonrpc.NefException({'code': code, 'message': message}) + self.assertEqual(code, result.code) + self.assertIn(message, result.msg) + + def test_kwargs(self): + code = 'EPERM' + message = 'test message 5' + result = jsonrpc.NefException(code=code, message=message) + self.assertEqual(code, result.code) + self.assertIn(message, result.msg) + + def test_dict_kwargs(self): + code = 'EINVAL' + message = 'test message 6' + result = jsonrpc.NefException({'code': code}, message=message) + self.assertEqual(code, result.code) + self.assertIn(message, result.msg) + + def test_defaults(self): + code = 'EBADMSG' + message = 'NexentaError' + result = jsonrpc.NefException() + self.assertEqual(code, result.code) + self.assertIn(message, result.msg) + + +class TestNefRequest(test.TestCase): def setUp(self): - super(TestNexentaJSONProxy, self).setUp() - self.nef_get = jsonrpc.NexentaJSONProxy( - 'http', '1.1.1.1', '8080', 'user', 'pass', 'get') - self.nef_post = jsonrpc.NexentaJSONProxy( - 'https', '1.1.1.1', '8080', 'user', 'pass', 'post') + super(TestNefRequest, self).setUp() + self.proxy = FakeNefProxy() - @patch('requests.Response.close') - @patch('requests.Session.get') - def test_call_get_data(self, get, close): - data = {'key': 'value'} - get.return_value = requests.Response() - get.return_value.__setstate__( - {'status_code': 200, '_content': jsonutils.dumps(data)}) + def fake_response(self, method, path, payload, code, content): + request = requests.PreparedRequest() + request.method = method + request.url = self.proxy.url(path) + request.headers = {'Content-Type': 'application/json'} + request.body = None + if method in ['get', 'delete']: + request.params = payload + elif method in ['put', 'post']: + request.data = json.dumps(payload) + response = requests.Response() + response.request = request + response.status_code = code + response._content = json.dumps(content) if content else '' + return response - self.assertEqual({'key': 'value'}, self.nef_get('url')) + def test___call___invalid_method(self): + method = 'unsupported' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + self.assertRaises(jsonrpc.NefException, instance, path) - @patch('requests.Response.close') - @patch('requests.Session.get') - def test_call_get_created(self, get, close): - get.return_value = requests.Response() - get.return_value.__setstate__({ - 'status_code': 201, '_content': ''}) + def test___call___none_path(self): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + self.assertRaises(jsonrpc.NefException, instance, None) - self.assertIsNone(self.nef_get('url')) + def test___call___empty_path(self): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + self.assertRaises(jsonrpc.NefException, instance, '') - @patch('requests.Response.close') - @patch('requests.Session.post') - def test_call_post_success(self, post, close): - data = {'key': 'value'} - post.return_value = requests.Response() - post.return_value.__setstate__({ - 'status_code': 200, '_content': ''}) - self.assertIsNone(self.nef_post('url', data)) + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.request') + def test___call___get(self, request): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {} + content = {'name': 'snapshot'} + response = self.fake_response(method, path, payload, 200, content) + request.return_value = response + result = instance(path, payload) + request.assert_called_with(method, path) + self.assertEqual(content, result) - @patch('time.sleep') - @patch('requests.Response.close') - @patch('requests.Session.get') - @patch('requests.Session.post') - def test_call_post_202(self, post, get, close, sleep): - data = {'key': 'value'} - data2 = {'links': [{'href': 'redirect_url'}]} + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.request') + def test___call___get_payload(self, request): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {'key': 'value'} + content = {'name': 'snapshot'} + response = self.fake_response(method, path, payload, 200, content) + request.return_value = response + result = instance(path, payload) + params = {'params': payload} + request.assert_called_with(method, path, **params) + self.assertEqual(content, result) - get.return_value = requests.Response() - post.return_value = requests.Response() - post.return_value.__setstate__({ - 'status_code': 202, '_content': jsonutils.dumps(data2)}) - get.return_value.__setstate__({ - 'status_code': 200, '_content': jsonutils.dumps(data)}) + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.request') + def test___call___get_data_payload(self, request): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {'key': 'value'} + data = [ + { + 'name': 'fs1', + 'path': 'pool/fs1' + }, + { + 'name': 'fs2', + 'path': 'pool/fs2' + } + ] + content = {'data': data} + response = self.fake_response(method, path, payload, 200, content) + request.return_value = response + instance.data = data + result = instance(path, payload) + params = {'params': payload} + request.assert_called_with(method, path, **params) + self.assertEqual(data, result) - self.assertEqual({'key': 'value'}, self.nef_post('url')) + def test___call___get_invalid_payload(self): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = 'bad data' + self.assertRaises(jsonrpc.NefException, instance, path, payload) - @patch('requests.Response.close') - @patch('requests.Session.get') - def test_call_get_not_exist(self, get, close): - get.return_value = requests.Response() - get.return_value.__setstate__({ - 'status_code': 400, - '_content': jsonutils.dumps({'code': 'ENOENT'})}) + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.request') + def test___call___delete(self, request): + method = 'delete' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {} + content = {'name': 'snapshot'} + response = self.fake_response(method, path, payload, 200, content) + request.return_value = response + result = instance(path, payload) + request.assert_called_with(method, path) + self.assertEqual(content, result) - self.assertRaises( - exception.NexentaException, lambda: self.nef_get('url')) + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.request') + def test___call___delete_payload(self, request): + method = 'delete' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {'key': 'value'} + content = {'name': 'snapshot'} + response = self.fake_response(method, path, payload, 200, content) + request.return_value = response + result = instance(path, payload) + params = {'params': payload} + request.assert_called_with(method, path, **params) + self.assertEqual(content, result) - @patch('requests.Response.close') - @patch('requests.Session.get') - def test_call_get_unauthorized(self, get, close): - get.return_value = requests.Response() - get.return_value.__setstate__({ - 'status_code': 401, - '_content': jsonutils.dumps({'code': 'unauthorized'})}) + def test___call___delete_invalid_payload(self): + method = 'delete' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = 'bad data' + self.assertRaises(jsonrpc.NefException, instance, path, payload) - self.assertRaises( - exception.NexentaException, lambda: self.nef_get('url')) + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.request') + def test___call___post(self, request): + method = 'post' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {} + content = None + response = self.fake_response(method, path, payload, 200, content) + request.return_value = response + result = instance(path, payload) + request.assert_called_with(method, path) + self.assertEqual(content, result) - @patch('%s.https_auth' % PATH_TO_RPC) - @patch('requests.Response.close') - @patch('requests.Session.post') - def test_call_post_bad_token(self, post, close, auth): - post.return_value = requests.Response() - auth.return_value = {'token': 'tok'} - post.return_value.__setstate__({ - 'status_code': 401, - '_content': jsonutils.dumps({'code': 'unauthorized'})}) + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.request') + def test___call___post_payload(self, request): + method = 'post' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {'key': 'value'} + content = None + response = self.fake_response(method, path, payload, 200, content) + request.return_value = response + result = instance(path, payload) + params = {'data': json.dumps(payload)} + request.assert_called_with(method, path, **params) + self.assertEqual(content, result) - self.assertRaises( - exception.NexentaException, lambda: self.nef_post('url')) + def test___call___post_invalid_payload(self): + method = 'post' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = 'bad data' + self.assertRaises(jsonrpc.NefException, instance, path, payload) - @patch('requests.Response.close') - @patch('requests.Session.post') - def test_auth(self, post, close): - httpsdata = {'token': 'tok'} - post.return_value = requests.Response() - post.return_value.__setstate__({ - 'status_code': 200, '_content': jsonutils.dumps(httpsdata)}) - nef_get = jsonrpc.NexentaJSONProxy( - 'http', '1.1.1.1', '8080', 'user', 'pass', method='get') - https_auth = nef_get.https_auth() - self.assertEqual('tok', https_auth) + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.request') + def test___call___put(self, request): + method = 'put' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {} + content = None + response = self.fake_response(method, path, payload, 200, content) + request.return_value = response + result = instance(path, payload) + request.assert_called_with(method, path) + self.assertEqual(content, result) + + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.request') + def test___call___put_payload(self, request): + method = 'put' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {'key': 'value'} + content = None + response = self.fake_response(method, path, payload, 200, content) + request.return_value = response + result = instance(path, payload) + params = {'data': json.dumps(payload)} + request.assert_called_with(method, path, **params) + self.assertEqual(content, result) + + def test___call___put_invalid_payload(self): + method = 'put' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = 'bad data' + self.assertRaises(jsonrpc.NefException, instance, path, payload) + + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.request') + def test___call___non_ok_response(self, request): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {'key': 'value'} + content = {'code': 'ENOENT', 'message': 'error'} + response = self.fake_response(method, path, payload, 500, content) + request.return_value = response + self.assertRaises(jsonrpc.NefException, instance, path, payload) + + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.failover') + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.request') + def test___call___request_after_failover(self, request, failover): + method = 'post' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {'key': 'value'} + content = None + response = self.fake_response(method, path, payload, 200, content) + request.side_effect = [requests.exceptions.Timeout, response] + failover.return_value = True + result = instance(path, payload) + params = {'data': json.dumps(payload)} + request.assert_called_with(method, path, **params) + self.assertEqual(content, result) + + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.failover') + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.request') + def test___call___request_failover_error(self, request, failover): + method = 'put' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {'key': 'value'} + request.side_effect = requests.exceptions.Timeout + failover.return_value = False + self.assertRaises(requests.exceptions.Timeout, instance, path, payload) + + def test_hook_default(self): + method = 'post' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {'key': 'value'} + content = {'name': 'dataset'} + response = self.fake_response(method, path, payload, 303, content) + result = instance.hook(response) + self.assertEqual(response, result) + + def test_hook_200_empty(self): + method = 'delete' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'storage/filesystems' + payload = {'force': True} + content = None + response = self.fake_response(method, path, payload, 200, content) + result = instance.hook(response) + self.assertEqual(response, result) + + def test_hook_201_empty(self): + method = 'post' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'storage/snapshots' + payload = {'path': 'parent/child@name'} + content = None + response = self.fake_response(method, path, payload, 201, content) + result = instance.hook(response) + self.assertEqual(response, result) + + def test_hook_500_empty(self): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'storage/pools' + payload = {'poolName': 'tank'} + content = None + response = self.fake_response(method, path, payload, 500, content) + self.assertRaises(jsonrpc.NefException, instance.hook, response) + + def test_hook_200_bad_content(self): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'storage/volumes' + payload = {'name': 'test'} + content = None + response = self.fake_response(method, path, payload, 200, content) + response._content = 'bad_content' + self.assertRaises(jsonrpc.NefException, instance.hook, response) + + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.request') + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.auth') + def test_hook_401(self, auth, request): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {'key': 'value'} + content = {'code': 'EAUTH'} + response = self.fake_response(method, path, payload, 401, content) + auth.return_value = True + content2 = {'name': 'test'} + response2 = self.fake_response(method, path, payload, 200, content2) + request.return_value = response2 + self.proxy.session.send.return_value = content2 + result = instance.hook(response) + self.assertEqual(content2, result) + + def test_hook_401_max_retries(self): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + instance.stat[401] = self.proxy.retries + path = 'parent/child' + payload = {'key': 'value'} + content = {'code': 'EAUTH'} + response = self.fake_response(method, path, payload, 401, content) + self.assertRaises(jsonrpc.NefException, instance.hook, response) + + def test_hook_404_nested(self): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + instance.lock = True + path = 'parent/child' + payload = {'key': 'value'} + content = {'code': 'ENOENT'} + response = self.fake_response(method, path, payload, 404, content) + result = instance.hook(response) + self.assertEqual(response, result) + + def test_hook_404_max_retries(self): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + instance.stat[404] = self.proxy.retries + path = 'parent/child' + payload = {'key': 'value'} + content = {'code': 'ENOENT'} + response = self.fake_response(method, path, payload, 404, content) + self.assertRaises(jsonrpc.NefException, instance.hook, response) + + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.failover') + def test_hook_404_failover_error(self, failover): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {'key': 'value'} + content = {'code': 'ENOENT'} + response = self.fake_response(method, path, payload, 404, content) + failover.return_value = False + result = instance.hook(response) + self.assertEqual(response, result) + + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.request') + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.failover') + def test_hook_404_failover_ok(self, failover, request): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {'key': 'value'} + content = {'code': 'ENOENT'} + response = self.fake_response(method, path, payload, 404, content) + failover.return_value = True + content2 = {'name': 'test'} + response2 = self.fake_response(method, path, payload, 200, content2) + request.return_value = response2 + result = instance.hook(response) + self.assertEqual(response2, result) + + def test_hook_500_permanent(self): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {'key': 'value'} + content = {'code': 'EINVAL'} + response = self.fake_response(method, path, payload, 500, content) + self.assertRaises(jsonrpc.NefException, instance.hook, response) + + def test_hook_500_busy_max_retries(self): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + instance.stat[500] = self.proxy.retries + path = 'parent/child' + payload = {'key': 'value'} + content = {'code': 'EBUSY'} + response = self.fake_response(method, path, payload, 500, content) + self.assertRaises(jsonrpc.NefException, instance.hook, response) + + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.request') + def test_hook_500_busy_ok(self, request): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {'key': 'value'} + content = {'code': 'EBUSY'} + response = self.fake_response(method, path, payload, 500, content) + content2 = {'name': 'test'} + response2 = self.fake_response(method, path, payload, 200, content2) + request.return_value = response2 + result = instance.hook(response) + self.assertEqual(response2, result) + + def test_hook_201_no_monitor(self): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {'key': 'value'} + content = {'monitor': 'unknown'} + response = self.fake_response(method, path, payload, 202, content) + self.assertRaises(jsonrpc.NefException, instance.hook, response) + + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.request') + def test_hook_201_ok(self, request): + method = 'delete' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {'key': 'value'} + content = { + 'links': [{ + 'rel': 'monitor', + 'href': '/jobStatus/jobID' + }] + } + response = self.fake_response(method, path, payload, 202, content) + content2 = None + response2 = self.fake_response(method, path, payload, 201, content2) + request.return_value = response2 + result = instance.hook(response) + self.assertEqual(response2, result) + + def test_200_no_data(self): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {'key': 'value'} + content = {'name': 'test'} + response = self.fake_response(method, path, payload, 200, content) + result = instance.hook(response) + self.assertEqual(response, result) + + def test_200_pagination_end(self): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {'key': 'value'} + content = {'data': 'value'} + response = self.fake_response(method, path, payload, 200, content) + result = instance.hook(response) + self.assertEqual(response, result) + + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.request') + def test_200_pagination_next(self, request): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {'key': 'value'} + content = { + 'data': [{ + 'name': 'test' + }], + 'links': [{ + 'rel': 'next', + 'href': path + }] + } + response = self.fake_response(method, path, payload, 200, content) + response2 = self.fake_response(method, path, payload, 200, content) + request.return_value = response2 + result = instance.hook(response) + self.assertEqual(response2, result) + + def test_request(self): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + path = 'parent/child' + payload = {'key': 'value'} + expected = {'name': 'dataset'} + url = self.proxy.url(path) + kwargs = payload.copy() + kwargs['timeout'] = self.proxy.timeout + kwargs['hooks'] = {'response': instance.hook} + self.proxy.session.request.return_value = expected + result = instance.request(method, path, **payload) + self.proxy.session.request.assert_called_with(method, url, **kwargs) + self.assertEqual(expected, result) + + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.request') + def test_auth(self, request): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + method = 'post' + path = 'auth/login' + payload = { + 'data': json.dumps({ + 'username': self.proxy.username, + 'password': self.proxy.password + }) + } + content = {'token': 'test'} + response = self.fake_response(method, path, payload, 200, content) + request.return_value = response + instance.auth() + request.assert_called_once_with(method, path, **payload) + + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.request') + def test_auth_error(self, request): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + method = 'post' + path = 'auth/login' + payload = { + 'data': json.dumps({ + 'username': self.proxy.username, + 'password': self.proxy.password + }) + } + content = {'data': 'noauth'} + response = self.fake_response(method, path, payload, 200, content) + request.return_value = response + self.assertRaises(jsonrpc.NefException, instance.auth) + + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.request') + def test_failover(self, request): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + path = self.proxy.root + payload = {} + content = {'path': path} + response = self.fake_response(method, path, payload, 200, content) + request.return_value = response + result = instance.failover() + request.assert_called_once_with(method, path) + expected = True + self.assertEqual(expected, result) + + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.request') + def test_failover_timeout(self, request): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + path = self.proxy.root + payload = {} + content = {'path': path} + response = self.fake_response(method, path, payload, 200, content) + request.side_effect = [requests.exceptions.Timeout, response] + result = instance.failover() + request.assert_called_once_with(method, path) + expected = False + self.assertEqual(expected, result) + + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.request') + def test_failover_404(self, request): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + path = self.proxy.root + payload = {} + content = {} + response = self.fake_response(method, path, payload, 404, content) + request.side_effect = [response, response] + result = instance.failover() + request.assert_called_once_with(method, path) + expected = False + self.assertEqual(expected, result) + + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefRequest.request') + def test_failover_error(self, request): + method = 'get' + instance = jsonrpc.NefRequest(self.proxy, method) + path = self.proxy.root + request.side_effect = [ + requests.exceptions.Timeout, + requests.exceptions.ConnectionError + ] + result = instance.failover() + request.assert_called_with(method, path) + expected = False + self.assertEqual(expected, result) + + def test_getpath(self): + method = 'get' + rel = 'monitor' + href = 'jobStatus/jobID' + content = { + 'links': [ + [1, 2], + 'bad link', + { + 'rel': 'next', + 'href': href + }, + { + 'rel': rel, + 'href': href + } + ] + } + instance = jsonrpc.NefRequest(self.proxy, method) + result = instance.getpath(content, rel) + expected = href + self.assertEqual(expected, result) + + def test_getpath_no_content(self): + method = 'get' + rel = 'next' + content = None + instance = jsonrpc.NefRequest(self.proxy, method) + result = instance.getpath(content, rel) + self.assertIsNone(result) + + def test_getpath_no_links(self): + method = 'get' + rel = 'next' + content = {'a': 'b'} + instance = jsonrpc.NefRequest(self.proxy, method) + result = instance.getpath(content, rel) + self.assertIsNone(result) + + def test_getpath_no_rel(self): + method = 'get' + rel = 'next' + content = { + 'links': [ + { + 'rel': 'monitor', + 'href': '/jobs/jobID' + } + ] + } + instance = jsonrpc.NefRequest(self.proxy, method) + result = instance.getpath(content, rel) + self.assertIsNone(result) + + def test_getpath_no_href(self): + method = 'get' + rel = 'next' + content = { + 'links': [ + { + 'rel': rel + } + ] + } + instance = jsonrpc.NefRequest(self.proxy, method) + result = instance.getpath(content, rel) + self.assertIsNone(result) + + +class TestNefCollections(test.TestCase): + + def setUp(self): + super(TestNefCollections, self).setUp() + self.proxy = mock.Mock() + self.instance = jsonrpc.NefCollections(self.proxy) + + def test_path(self): + path = 'path/to/item name + - & # $ = 0' + result = self.instance.path(path) + quoted_path = six.moves.urllib.parse.quote_plus(path) + expected = posixpath.join(self.instance.root, quoted_path) + self.assertEqual(expected, result) + + def test_get(self): + name = 'parent/child' + payload = {'key': 'value'} + expected = {'name': 'dataset'} + path = self.instance.path(name) + self.proxy.get.return_value = expected + result = self.instance.get(name, payload) + self.proxy.get.assert_called_with(path, payload) + self.assertEqual(expected, result) + + def test_set(self): + name = 'parent/child' + payload = {'key': 'value'} + expected = None + path = self.instance.path(name) + self.proxy.put.return_value = expected + result = self.instance.set(name, payload) + self.proxy.put.assert_called_with(path, payload) + self.assertIsNone(result) + + def test_list(self): + payload = {'key': 'value'} + expected = [{'name': 'dataset'}] + self.proxy.get.return_value = expected + result = self.instance.list(payload) + self.proxy.get.assert_called_with(self.instance.root, payload) + self.assertEqual(expected, result) + + def test_create(self): + payload = {'key': 'value'} + expected = None + self.proxy.post.return_value = expected + result = self.instance.create(payload) + self.proxy.post.assert_called_with(self.instance.root, payload) + self.assertIsNone(result) + + def test_create_exist(self): + payload = {'key': 'value'} + self.proxy.post.side_effect = jsonrpc.NefException(code='EEXIST') + result = self.instance.create(payload) + self.proxy.post.assert_called_with(self.instance.root, payload) + self.assertIsNone(result) + + def test_create_error(self): + payload = {'key': 'value'} + self.proxy.post.side_effect = jsonrpc.NefException(code='EBUSY') + self.assertRaises(jsonrpc.NefException, self.instance.create, payload) + self.proxy.post.assert_called_with(self.instance.root, payload) + + def test_delete(self): + name = 'parent/child' + payload = {'key': 'value'} + expected = None + path = self.instance.path(name) + self.proxy.delete.return_value = expected + result = self.instance.delete(name, payload) + self.proxy.delete.assert_called_with(path, payload) + self.assertIsNone(result) + + def test_delete_not_found(self): + name = 'parent/child' + payload = {'key': 'value'} + path = self.instance.path(name) + self.proxy.delete.side_effect = jsonrpc.NefException(code='ENOENT') + result = self.instance.delete(name, payload) + self.proxy.delete.assert_called_with(path, payload) + self.assertIsNone(result) + + def test_delete_error(self): + name = 'parent/child' + payload = {'key': 'value'} + path = self.instance.path(name) + self.proxy.delete.side_effect = jsonrpc.NefException(code='EINVAL') + self.assertRaises(jsonrpc.NefException, self.instance.delete, name, + payload) + self.proxy.delete.assert_called_with(path, payload) + + +class TestNefSettings(test.TestCase): + + def setUp(self): + super(TestNefSettings, self).setUp() + self.proxy = mock.Mock() + self.instance = jsonrpc.NefSettings(self.proxy) + + def test_create(self): + payload = {'key': 'value'} + result = self.instance.create(payload) + expected = NotImplemented + self.assertEqual(expected, result) + + def test_delete(self): + name = 'parent/child' + payload = {'key': 'value'} + result = self.instance.delete(name, payload) + expected = NotImplemented + self.assertEqual(expected, result) + + +class TestNefDatasets(test.TestCase): + + def setUp(self): + super(TestNefDatasets, self).setUp() + self.proxy = mock.Mock() + self.instance = jsonrpc.NefDatasets(self.proxy) + + def test_rename(self): + name = 'parent/child' + payload = {'key': 'value'} + expected = None + path = self.instance.path(name) + path = posixpath.join(path, 'rename') + self.proxy.post.return_value = expected + result = self.instance.rename(name, payload) + self.proxy.post.assert_called_with(path, payload) + self.assertIsNone(result) + + +class TestNefSnapshots(test.TestCase): + + def setUp(self): + super(TestNefSnapshots, self).setUp() + self.proxy = mock.Mock() + self.instance = jsonrpc.NefSnapshots(self.proxy) + + def test_clone(self): + name = 'parent/child' + payload = {'key': 'value'} + expected = None + path = self.instance.path(name) + path = posixpath.join(path, 'clone') + self.proxy.post.return_value = expected + result = self.instance.clone(name, payload) + self.proxy.post.assert_called_with(path, payload) + self.assertIsNone(result) + + +class TestNefFilesystems(test.TestCase): + + def setUp(self): + super(TestNefFilesystems, self).setUp() + self.proxy = mock.Mock() + self.instance = jsonrpc.NefFilesystems(self.proxy) + + def test_mount(self): + name = 'parent/child' + payload = {'key': 'value'} + expected = None + path = self.instance.path(name) + path = posixpath.join(path, 'mount') + self.proxy.post.return_value = expected + result = self.instance.mount(name, payload) + self.proxy.post.assert_called_with(path, payload) + self.assertIsNone(result) + + def test_unmount(self): + name = 'parent/child' + payload = {'key': 'value'} + expected = None + path = self.instance.path(name) + path = posixpath.join(path, 'unmount') + self.proxy.post.return_value = expected + result = self.instance.unmount(name, payload) + self.proxy.post.assert_called_with(path, payload) + self.assertIsNone(result) + + def test_acl(self): + name = 'parent/child' + payload = {'key': 'value'} + expected = None + path = self.instance.path(name) + path = posixpath.join(path, 'acl') + self.proxy.post.return_value = expected + result = self.instance.acl(name, payload) + self.proxy.post.assert_called_with(path, payload) + self.assertIsNone(result) + + def test_promote(self): + name = 'parent/child' + payload = {'key': 'value'} + expected = None + path = self.instance.path(name) + path = posixpath.join(path, 'promote') + self.proxy.post.return_value = expected + result = self.instance.promote(name, payload) + self.proxy.post.assert_called_with(path, payload) + self.assertIsNone(result) + + def test_rollback(self): + name = 'parent/child' + payload = {'key': 'value'} + expected = None + path = self.instance.path(name) + path = posixpath.join(path, 'rollback') + self.proxy.post.return_value = expected + result = self.instance.rollback(name, payload) + self.proxy.post.assert_called_with(path, payload) + self.assertIsNone(result) + + +class TestNefHpr(test.TestCase): + + def setUp(self): + super(TestNefHpr, self).setUp() + self.proxy = mock.Mock() + self.instance = jsonrpc.NefHpr(self.proxy) + + def test_activate(self): + payload = {'key': 'value'} + expected = None + path = posixpath.join(self.instance.root, 'activate') + self.proxy.post.return_value = expected + result = self.instance.activate(payload) + self.proxy.post.assert_called_with(path, payload) + self.assertIsNone(result) + + def test_start(self): + name = 'parent/child' + payload = {'key': 'value'} + expected = None + path = posixpath.join(self.instance.path(name), 'start') + self.proxy.post.return_value = expected + result = self.instance.start(name, payload) + self.proxy.post.assert_called_with(path, payload) + self.assertIsNone(result) + + +class TestNefProxy(test.TestCase): + + def setUp(self): + super(TestNefProxy, self).setUp() + self.cfg = mock.Mock(spec=conf.Configuration) + self.cfg.nexenta_use_https = True + self.cfg.nexenta_ssl_cert_verify = True + self.cfg.nexenta_user = 'user' + self.cfg.nexenta_password = 'pass' + self.cfg.nexenta_rest_addresses = ['1.1.1.1', '2.2.2.2'] + self.cfg.nexenta_rest_port = 8443 + self.cfg.nexenta_rest_backoff_factor = 1 + self.cfg.nexenta_rest_retry_count = 3 + self.cfg.nexenta_rest_connect_timeout = 1 + self.cfg.nexenta_rest_read_timeout = 1 + self.cfg.nexenta_nas_host = '3.3.3.3' + self.cfg.nexenta_folder = 'pool/path/to/share' + self.nef_mock = mock.Mock() + self.mock_object(jsonrpc, 'NefRequest') + + self.proto = 'nfs' + self.proxy = jsonrpc.NefProxy(self.proto, + self.cfg.nexenta_folder, + self.cfg) + + def test___init___http(self): + proto = 'nfs' + cfg = copy.copy(self.cfg) + cfg.nexenta_use_https = False + result = jsonrpc.NefProxy(proto, cfg.nexenta_folder, cfg) + self.assertIsInstance(result, jsonrpc.NefProxy) + + def test___init___no_rest_port_http(self): + proto = 'nfs' + cfg = copy.copy(self.cfg) + cfg.nexenta_rest_port = 0 + cfg.nexenta_use_https = False + result = jsonrpc.NefProxy(proto, cfg.nexenta_folder, cfg) + self.assertIsInstance(result, jsonrpc.NefProxy) + + def test___init___no_rest_port_https(self): + proto = 'nfs' + cfg = copy.copy(self.cfg) + cfg.nexenta_rest_port = 0 + cfg.nexenta_use_https = True + result = jsonrpc.NefProxy(proto, cfg.nexenta_folder, cfg) + self.assertIsInstance(result, jsonrpc.NefProxy) + + def test___init___iscsi(self): + proto = 'iscsi' + cfg = copy.copy(self.cfg) + result = jsonrpc.NefProxy(proto, cfg.nexenta_folder, cfg) + self.assertIsInstance(result, jsonrpc.NefProxy) + + def test___init___nfs_no_rest_address(self): + proto = 'nfs' + cfg = copy.copy(self.cfg) + cfg.nexenta_rest_addresses = '' + result = jsonrpc.NefProxy(proto, cfg.nexenta_folder, cfg) + self.assertIsInstance(result, jsonrpc.NefProxy) + + def test___init___iscsi_no_rest_address(self): + proto = 'iscsi' + cfg = copy.copy(self.cfg) + cfg.nexenta_rest_addresses = '' + cfg.nexenta_host = '4.4.4.4' + result = jsonrpc.NefProxy(proto, cfg.nexenta_folder, cfg) + self.assertIsInstance(result, jsonrpc.NefProxy) + + @mock.patch('requests.packages.urllib3.disable_warnings') + def test___init___no_ssl_cert_verify(self, disable_warnings): + proto = 'nfs' + cfg = copy.copy(self.cfg) + cfg.nexenta_ssl_cert_verify = False + disable_warnings.return_value = None + result = jsonrpc.NefProxy(proto, cfg.nexenta_folder, cfg) + disable_warnings.assert_called() + self.assertIsInstance(result, jsonrpc.NefProxy) + + def test_delete_bearer(self): + self.assertIsNone(self.proxy.delete_bearer()) + self.assertNotIn('Authorization', self.proxy.session.headers) + self.proxy.session.headers['Authorization'] = 'Bearer token' + self.assertIsNone(self.proxy.delete_bearer()) + self.assertNotIn('Authorization', self.proxy.session.headers) + + def test_update_bearer(self): + token = 'token' + bearer = 'Bearer %s' % token + self.assertNotIn('Authorization', self.proxy.session.headers) + self.assertIsNone(self.proxy.update_bearer(token)) + self.assertIn('Authorization', self.proxy.session.headers) + self.assertEqual(self.proxy.session.headers['Authorization'], bearer) + + def test_update_token(self): + token = 'token' + bearer = 'Bearer %s' % token + self.assertIsNone(self.proxy.update_token(token)) + self.assertEqual(self.proxy.tokens[self.proxy.host], token) + self.assertEqual(self.proxy.session.headers['Authorization'], bearer) + + def test_update_host(self): + token = 'token' + bearer = 'Bearer %s' % token + host = self.cfg.nexenta_rest_addresses[0] + self.proxy.tokens[host] = token + self.assertIsNone(self.proxy.update_host(host)) + self.assertEqual(self.proxy.session.headers['Authorization'], bearer) + + def test_skip_update_host(self): + host = 'nonexistent' + self.assertIsNone(self.proxy.update_host(host)) + + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefSettings.get') + def test_update_lock(self, get_settings): + guid = uuid.uuid4().hex + settings = {'value': guid} + get_settings.return_value = settings + self.assertIsNone(self.proxy.update_lock()) + path = '%s:%s' % (guid, self.proxy.path) + if isinstance(path, six.text_type): + path = path.encode('utf-8') + expected = hashlib.md5(path).hexdigest() + self.assertEqual(expected, self.proxy.lock) + + def test_url(self): + path = '/path/to/api' + result = self.proxy.url(path) + expected = '%s://%s:%s%s' % (self.proxy.scheme, + self.proxy.host, + self.proxy.port, + path) + self.assertEqual(expected, result) + + @mock.patch('eventlet.greenthread.sleep') + def test_delay(self, sleep): + sleep.return_value = None + for attempt in range(0, 10): + expected = int(self.proxy.backoff_factor * (2 ** (attempt - 1))) + self.assertIsNone(self.proxy.delay(attempt)) + sleep.assert_called_with(expected) diff --git a/manila/tests/share/drivers/nexenta/ns5/test_nexenta_nas.py b/manila/tests/share/drivers/nexenta/ns5/test_nexenta_nas.py index f4ce220b20..6c8a49aae5 100644 --- a/manila/tests/share/drivers/nexenta/ns5/test_nexenta_nas.py +++ b/manila/tests/share/drivers/nexenta/ns5/test_nexenta_nas.py @@ -1,4 +1,4 @@ -# Copyright 2016 Nexenta Systems, Inc. +# Copyright 2019 Nexenta by DDN, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -19,13 +19,21 @@ from mock import patch from oslo_utils import units from manila import context -from manila import exception -from manila.share import configuration as conf +from manila.share.drivers.nexenta.ns5 import jsonrpc from manila.share.drivers.nexenta.ns5 import nexenta_nas from manila import test -PATH_TO_RPC = 'manila.share.drivers.nexenta.ns5.jsonrpc.NexentaJSONProxy' +RPC_PATH = 'manila.share.drivers.nexenta.ns5.jsonrpc' DRV_PATH = 'manila.share.drivers.nexenta.ns5.nexenta_nas.NexentaNasDriver' +DRIVER_VERSION = '1.1' +SHARE = {'share_id': 'uuid', 'size': 1, 'share_proto': 'NFS'} +SHARE_PATH = 'pool1/nfs_share/share-uuid' +SHARE2 = {'share_id': 'uuid2', 'size': 2, 'share_proto': 'NFS'} +SHARE2_PATH = 'pool1/nfs_share/share-uuid2' +SNAPSHOT = { + 'snapshot_id': 'snap_id', + 'share': SHARE, + 'snapshot_path': '%s@%s' % (SHARE_PATH, 'snapshot-snap_id')} @ddt.ddt @@ -34,22 +42,27 @@ class TestNexentaNasDriver(test.TestCase): def setUp(self): def _safe_get(opt): return getattr(self.cfg, opt) - self.cfg = conf.Configuration(None) - self.cfg.nexenta_host = '1.1.1.1' - super(TestNexentaNasDriver, self).setUp() - self.ctx = context.get_admin_context() + self.cfg = mock.Mock() self.mock_object( self.cfg, 'safe_get', mock.Mock(side_effect=_safe_get)) + super(TestNexentaNasDriver, self).setUp() + self.cfg.nexenta_nas_host = '1.1.1.1' + self.cfg.nexenta_rest_addresses = ['2.2.2.2'] + self.ctx = context.get_admin_context() self.cfg.nexenta_rest_port = 8080 self.cfg.nexenta_rest_protocol = 'auto' self.cfg.nexenta_pool = 'pool1' + self.cfg.nexenta_dataset_record_size = 131072 self.cfg.reserved_share_percentage = 0 - self.cfg.nexenta_nfs_share = 'nfs_share' + self.cfg.nexenta_folder = 'nfs_share' self.cfg.nexenta_user = 'user' self.cfg.share_backend_name = 'NexentaStor5' self.cfg.nexenta_password = 'password' self.cfg.nexenta_thin_provisioning = False self.cfg.nexenta_mount_point_base = 'mnt' + self.cfg.nexenta_rest_retry_count = 3 + self.cfg.nexenta_share_name_prefix = 'share-' + self.cfg.max_over_subscription_ratio = 20.0 self.cfg.enabled_share_protocols = 'NFS' self.cfg.nexenta_mount_point_base = '$state_path/mnt' self.cfg.nexenta_dataset_compression = 'on' @@ -57,293 +70,304 @@ class TestNexentaNasDriver(test.TestCase): self.cfg.admin_network_config_group = ( 'fake_admin_network_config_group') self.cfg.driver_handles_share_servers = False - + self.cfg.safe_get = self.fake_safe_get + self.nef_mock = mock.Mock() + self.mock_object(jsonrpc, 'NefRequest') self.drv = nexenta_nas.NexentaNasDriver(configuration=self.cfg) self.drv.do_setup(self.ctx) - self.mock_rpc = self.mock_class(PATH_TO_RPC) - self.pool_name = self.cfg.nexenta_pool - self.fs_prefix = self.cfg.nexenta_nfs_share + + def fake_safe_get(self, key): + try: + value = getattr(self.cfg, key) + except AttributeError: + value = None + return value def test_backend_name(self): self.assertEqual('NexentaStor5', self.drv.share_backend_name) - @patch('%s._get_provisioned_capacity' % DRV_PATH) - def test_check_for_setup_error(self, mock_provisioned): - self.drv.nef.get.return_value = None - - self.assertRaises(LookupError, self.drv.check_for_setup_error) - - @patch('%s._get_provisioned_capacity' % DRV_PATH) - def test_check_for_setup_error__none(self, mock_provisioned): - self.drv.nef.get.return_value = { - 'data': [{'filesystem': 'pool1/nfs_share', 'quotaSize': 1}] + @mock.patch('%s._get_provisioned_capacity' % DRV_PATH) + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefServices.get') + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefFilesystems.set') + @mock.patch('manila.share.drivers.nexenta.ns5.' + 'jsonrpc.NefFilesystems.get') + def test_check_for_setup_error(self, get_filesystem, set_filesystem, + get_service, prov_capacity): + prov_capacity.return_value = 1 + get_filesystem.return_value = { + 'mountPoint': '/path/to/volume', + 'nonBlockingMandatoryMode': False, + 'smartCompression': False, + 'isMounted': True + } + get_service.return_value = { + 'state': 'online' } - self.assertIsNone(self.drv.check_for_setup_error()) + get_filesystem.assert_called_with(self.drv.root_path) + set_filesystem.assert_not_called() + get_service.assert_called_with('nfs') + get_filesystem.return_value = { + 'mountPoint': '/path/to/volume', + 'nonBlockingMandatoryMode': True, + 'smartCompression': True, + 'isMounted': True + } + set_filesystem.return_value = {} + payload = { + 'nonBlockingMandatoryMode': False, + 'smartCompression': False + } + self.assertIsNone(self.drv.check_for_setup_error()) + get_filesystem.assert_called_with(self.drv.root_path) + set_filesystem.assert_called_with(self.drv.root_path, payload) + get_service.assert_called_with('nfs') + get_filesystem.return_value = { + 'mountPoint': '/path/to/volume', + 'nonBlockingMandatoryMode': False, + 'smartCompression': True, + 'isMounted': True + } + payload = { + 'smartCompression': False + } + set_filesystem.return_value = {} + self.assertIsNone(self.drv.check_for_setup_error()) + get_filesystem.assert_called_with(self.drv.root_path) + set_filesystem.assert_called_with(self.drv.root_path, payload) + get_service.assert_called_with('nfs') + get_filesystem.return_value = { + 'mountPoint': '/path/to/volume', + 'nonBlockingMandatoryMode': True, + 'smartCompression': False, + 'isMounted': True + } + payload = { + 'nonBlockingMandatoryMode': False + } + set_filesystem.return_value = {} + self.assertIsNone(self.drv.check_for_setup_error()) + get_filesystem.assert_called_with(self.drv.root_path) + set_filesystem.assert_called_with(self.drv.root_path, payload) + get_service.assert_called_with('nfs') + get_filesystem.return_value = { + 'mountPoint': 'none', + 'nonBlockingMandatoryMode': False, + 'smartCompression': False, + 'isMounted': False + } + self.assertRaises(jsonrpc.NefException, + self.drv.check_for_setup_error) + get_filesystem.return_value = { + 'mountPoint': '/path/to/volume', + 'nonBlockingMandatoryMode': False, + 'smartCompression': False, + 'isMounted': False + } + self.assertRaises(jsonrpc.NefException, + self.drv.check_for_setup_error) + get_service.return_value = { + 'state': 'online' + } + self.assertRaises(jsonrpc.NefException, + self.drv.check_for_setup_error) - @patch('%s._get_provisioned_capacity' % DRV_PATH) - def test_check_for_setup_error__with_data(self, mock_provisioned): - self.drv.nef.get.return_value = { - 'data': [{'filesystem': 'asd', 'quotaSize': 1}]} - - self.assertRaises(LookupError, self.drv.check_for_setup_error) - - def test__get_provisioned_capacity(self): - self.drv.nef.get.return_value = { - 'data': [ - {'path': 'pool1/nfs_share/123', 'quotaSize': 1 * units.Gi}] + @patch('%s.NefFilesystems.get' % RPC_PATH) + def test__get_provisioned_capacity(self, fs_get): + fs_get.return_value = { + 'path': 'pool1/nfs_share/123', + 'referencedQuotaSize': 1 * units.Gi } self.drv._get_provisioned_capacity() - self.assertEqual(1, self.drv.provisioned_capacity) - - def test_create_share(self): - share = {'name': 'share', 'size': 1} + self.assertEqual(1 * units.Gi, self.drv.provisioned_capacity) + @patch('%s._mount_filesystem' % DRV_PATH) + @patch('%s.NefFilesystems.create' % RPC_PATH) + @patch('%s.NefFilesystems.delete' % RPC_PATH) + def test_create_share(self, delete_fs, create_fs, mount_fs): + mount_path = '%s:/%s' % (self.cfg.nexenta_nas_host, SHARE_PATH) + mount_fs.return_value = mount_path + size = int(1 * units.Gi * 1.1) self.assertEqual( [{ - 'path': '{}:/{}/{}/{}'.format( - self.cfg.nexenta_host, self.pool_name, - self.fs_prefix, share['name']) + 'path': mount_path, + 'id': 'share-uuid' }], - self.drv.create_share(self.ctx, share)) + self.drv.create_share(self.ctx, SHARE)) - @patch('%s.delete_share' % DRV_PATH) - @patch('%s._add_permission' % DRV_PATH) - def test_create_share__error_on_add_permission( - self, add_permission_mock, delete_share): - share = {'name': 'share', 'size': 1} - add_permission_mock.side_effect = exception.NexentaException( - 'An error occurred while adding permission') - delete_share.side_effect = exception.NexentaException( - 'An error occurred while deleting') - - self.assertRaises( - exception.NexentaException, self.drv.create_share, self.ctx, share) - - def test_create_share_from_snapshot(self): - share = {'name': 'share', 'size': 1} - snapshot = {'name': 'share@first', 'share_name': 'share'} - - self.assertEqual( - [{ - 'path': '{}:/{}/{}/{}'.format( - self.cfg.nexenta_host, self.pool_name, - self.fs_prefix, share['name']) - }], - self.drv.create_share_from_snapshot(self.ctx, share, snapshot) - ) - - @patch('%s.delete_share' % DRV_PATH) - @patch('%s._add_permission' % DRV_PATH) - def test_create_share_from_snapshot__add_permission_error( - self, add_permission_mock, delete_share): - share = {'name': 'share', 'size': 1} - snapshot = {'share_name': 'share', 'name': 'share@first'} - delete_share.side_effect = exception.NexentaException( - 'An error occurred while deleting') - add_permission_mock.side_effect = exception.NexentaException( - 'Some exception') - - self.assertRaises( - exception.NexentaException, self.drv.create_share_from_snapshot, - self.ctx, share, snapshot) - - @patch('%s._add_permission' % DRV_PATH) - def test_create_share_from_snapshot__add_permission_error_error( - self, add_permission_mock): - share = {'name': 'share', 'size': 1} - snapshot = {'share_name': 'share', 'name': 'share@first'} - add_permission_mock.side_effect = exception.NexentaException( - 'Some exception') - self.drv.nef.delete.side_effect = exception.NexentaException( - 'Some exception 2') - - self.assertRaises( - exception.NexentaException, self.drv.create_share_from_snapshot, - self.ctx, share, snapshot) - - def test_delete_share(self): - share = {'name': 'share', 'size': 1} - - self.assertIsNone(self.drv.delete_share(self.ctx, share)) - - def test_extend_share(self): - share = {'name': 'share', 'size': 1} - new_size = 2 - quota = new_size * units.Gi - data = { - 'reservationSize': quota, - 'quotaSize': quota, + payload = { + 'recordSize': 131072, + 'compressionMode': self.cfg.nexenta_dataset_compression, + 'path': SHARE_PATH, + 'referencedQuotaSize': size, + 'nonBlockingMandatoryMode': False, + 'referencedReservationSize': size } - url = 'storage/pools/{}/filesystems/{}%2F{}'.format( - self.pool_name, self.fs_prefix, share['name']) + self.drv.nef.filesystems.create.assert_called_with(payload) - self.drv.extend_share(share, new_size) + mount_fs.side_effect = jsonrpc.NefException('some error') + self.assertRaises(jsonrpc.NefException, + self.drv.create_share, self.ctx, SHARE) + delete_payload = {'force': True} + self.drv.nef.filesystems.delete.assert_called_with( + SHARE_PATH, delete_payload) - self.drv.nef.post.assert_called_with(url, data) + @patch('%s.NefFilesystems.promote' % RPC_PATH) + @patch('%s.NefSnapshots.get' % RPC_PATH) + @patch('%s.NefSnapshots.list' % RPC_PATH) + @patch('%s.NefFilesystems.delete' % RPC_PATH) + def test_delete_share(self, fs_delete, snap_list, snap_get, fs_promote): + delete_payload = {'force': True, 'snapshots': True} + snapshots_payload = {'parent': SHARE_PATH, 'fields': 'path'} + clones_payload = {'fields': 'clones,creationTxg'} + clone_path = '%s:/%s' % (self.cfg.nexenta_nas_host, 'path_to_fs') + fs_delete.side_effect = [ + jsonrpc.NefException({ + 'message': 'some_error', + 'code': 'EEXIST'}), + None] + snap_list.return_value = [{'path': '%s@snap1' % SHARE_PATH}] + snap_get.return_value = {'clones': [clone_path], 'creationTxg': 1} + self.assertIsNone(self.drv.delete_share(self.ctx, SHARE)) + fs_delete.assert_called_with(SHARE_PATH, delete_payload) + fs_promote.assert_called_with(clone_path) + snap_get.assert_called_with('%s@snap1' % SHARE_PATH, clones_payload) + snap_list.assert_called_with(snapshots_payload) - def test_shrink_share(self): - share = {'name': 'share', 'size': 2} - new_size = 1 - quota = new_size * units.Gi - data = { - 'reservationSize': quota, - 'quotaSize': quota + @patch('%s.NefFilesystems.mount' % RPC_PATH) + @patch('%s.NefFilesystems.get' % RPC_PATH) + def test_mount_filesystem(self, fs_get, fs_mount): + mount_path = '%s:/%s' % (self.cfg.nexenta_nas_host, SHARE_PATH) + fs_get.return_value = { + 'mountPoint': '/%s' % SHARE_PATH, 'isMounted': False} + self.assertEqual(mount_path, self.drv._mount_filesystem(SHARE)) + self.drv.nef.filesystems.mount.assert_called_with(SHARE_PATH) + + @patch('%s.NefHpr.activate' % RPC_PATH) + @patch('%s.NefFilesystems.mount' % RPC_PATH) + @patch('%s.NefFilesystems.get' % RPC_PATH) + def test_mount_filesystem_with_activate( + self, fs_get, fs_mount, hpr_activate): + mount_path = '%s:/%s' % (self.cfg.nexenta_nas_host, SHARE_PATH) + fs_get.side_effect = [ + {'mountPoint': 'none', 'isMounted': False}, + {'mountPoint': '/%s' % SHARE_PATH, 'isMounted': False}] + self.assertEqual(mount_path, self.drv._mount_filesystem(SHARE)) + payload = {'datasetName': SHARE_PATH} + self.drv.nef.hpr.activate.assert_called_once_with(payload) + + @patch('%s.NefFilesystems.mount' % RPC_PATH) + @patch('%s.NefFilesystems.unmount' % RPC_PATH) + def test_remount_filesystem(self, fs_unmount, fs_mount): + self.drv._remount_filesystem(SHARE_PATH) + fs_unmount.assert_called_once_with(SHARE_PATH) + fs_mount.assert_called_once_with(SHARE_PATH) + + def parse_fqdn(self, fqdn): + address_mask = fqdn.strip().split('/', 1) + address = address_mask[0] + ls = {"allow": True, "etype": "fqdn", "entity": address} + if len(address_mask) == 2: + ls['mask'] = address_mask[1] + ls['etype'] = 'network' + return ls + + @ddt.data({'key': 'value'}, {}) + @patch('%s.NefNfs.list' % RPC_PATH) + @patch('%s.NefNfs.set' % RPC_PATH) + @patch('%s.NefFilesystems.acl' % RPC_PATH) + def test_update_nfs_access(self, acl, nfs_set, nfs_list, list_data): + security_contexts = {'securityModes': ['sys']} + nfs_list.return_value = list_data + rw_list = ['1.1.1.1/24', '2.2.2.2'] + ro_list = ['3.3.3.3', '4.4.4.4/30'] + security_contexts['readWriteList'] = [] + security_contexts['readOnlyList'] = [] + for fqdn in rw_list: + ls = self.parse_fqdn(fqdn) + if ls.get('mask'): + ls['mask'] = int(ls['mask']) + security_contexts['readWriteList'].append(ls) + for fqdn in ro_list: + ls = self.parse_fqdn(fqdn) + if ls.get('mask'): + ls['mask'] = int(ls['mask']) + security_contexts['readOnlyList'].append(ls) + + self.assertIsNone(self.drv._update_nfs_access(SHARE, rw_list, ro_list)) + payload = { + 'flags': ['file_inherit', 'dir_inherit'], + 'permissions': ['full_set'], + 'principal': 'everyone@', + 'type': 'allow' } - url = 'storage/pools/{}/filesystems/{}%2F{}'.format( - self.pool_name, self.fs_prefix, share['name']) - self.drv.nef.get.return_value = {'bytesUsed': 512} - - self.drv.shrink_share(share, new_size) - - self.drv.nef.post.assert_called_with(url, data) - - def test_create_snapshot(self): - snapshot = {'share_name': 'share', 'name': 'share@first'} - url = 'storage/pools/%(pool)s/filesystems/%(fs)s/snapshots' % { - 'pool': self.pool_name, - 'fs': nexenta_nas.PATH_DELIMITER.join( - [self.fs_prefix, snapshot['share_name']]) - } - data = {'name': snapshot['name']} - - self.drv.create_snapshot(self.ctx, snapshot) - - self.drv.nef.post.assert_called_with(url, data) - - def test_delete_snapshot(self): - self.mock_rpc.side_effect = exception.NexentaException( - 'err', code='ENOENT') - snapshot = {'share_name': 'share', 'name': 'share@first'} - - self.assertIsNone(self.drv.delete_snapshot(self.ctx, snapshot)) - - self.mock_rpc.side_effect = exception.NexentaException( - 'err', code='somecode') - - self.assertRaises( - exception.NexentaException, self.drv.delete_snapshot, - self.ctx, snapshot) - - def build_access_security_context(self, level, ip, mask=None): - ls = [{"allow": True, "etype": "network", "entity": ip}] - if mask is not None: - ls[0]['mask'] = mask - new_sc = { - "securityModes": ["sys"], - } - if level == 'rw': - new_sc['readWriteList'] = ls - elif level == 'ro': - new_sc['readOnlyList'] = ls + self.drv.nef.filesystems.acl.assert_called_with(SHARE_PATH, payload) + payload = {'securityContexts': [security_contexts]} + if list_data: + self.drv.nef.nfs.set.assert_called_with(SHARE_PATH, payload) else: - raise exception.ManilaException('Wrong access level') - return new_sc + payload['filesystem'] = SHARE_PATH + self.drv.nef.nfs.create.assert_called_with(payload) - def test_update_access__unsupported_access_type(self): - share = {'name': 'share', 'size': 1} - access = { - 'access_type': 'group', - 'access_to': 'ordinary_users', - 'access_level': 'rw' - } + def test_update_nfs_access_bad_mask(self): + security_contexts = {'securityModes': ['sys']} + rw_list = ['1.1.1.1/24', '2.2.2.2/1a'] + ro_list = ['3.3.3.3', '4.4.4.4/30'] + security_contexts['readWriteList'] = [] + security_contexts['readOnlyList'] = [] + for fqdn in rw_list: + security_contexts['readWriteList'].append(self.parse_fqdn(fqdn)) + for fqdn in ro_list: + security_contexts['readOnlyList'].append(self.parse_fqdn(fqdn)) - self.assertRaises(exception.InvalidShareAccess, self.drv.update_access, - self.ctx, share, [access], None, None) + self.assertRaises(ValueError, self.drv._update_nfs_access, + SHARE, rw_list, ro_list) - def test_update_access__cidr(self): - share = {'name': 'share', 'size': 1} - access = { - 'access_type': 'ip', - 'access_to': '1.1.1.1/24', - 'access_level': 'rw' - } - url = 'nas/nfs/' + nexenta_nas.PATH_DELIMITER.join( - (self.pool_name, self.fs_prefix, share['name'])) - self.drv.nef.get.return_value = {} - - self.drv.update_access(self.ctx, share, [access], None, None) - - self.drv.nef.put.assert_called_with( - url, {'securityContexts': [ - self.build_access_security_context('rw', '1.1.1.1', 24)]}) - - def test_update_access__ip(self): - share = {'name': 'share', 'size': 1} + @patch('%s._update_nfs_access' % DRV_PATH) + def test_update_access__ip_rw(self, update_nfs_access): access = { 'access_type': 'ip', 'access_to': '1.1.1.1', - 'access_level': 'rw' + 'access_level': 'rw', + 'access_id': 'fake_id' } - url = 'nas/nfs/' + nexenta_nas.PATH_DELIMITER.join( - (self.pool_name, self.fs_prefix, share['name'])) - self.drv.nef.get.return_value = {} - self.drv.update_access(self.ctx, share, [access], None, None) + self.assertEqual( + {'fake_id': {'state': 'active'}}, + self.drv.update_access( + self.ctx, SHARE, [access], None, None)) + self.drv._update_nfs_access.assert_called_with(SHARE, ['1.1.1.1'], []) - self.drv.nef.put.assert_called_with( - url, {'securityContexts': [ - self.build_access_security_context('rw', '1.1.1.1')]}) - - @ddt.data('rw', 'ro') - def test_update_access__cidr_wrong_mask(self, access_level): - share = {'name': 'share', 'size': 1} + @patch('%s._update_nfs_access' % DRV_PATH) + def test_update_access__ip_ro(self, update_nfs_access): access = { 'access_type': 'ip', - 'access_to': '1.1.1.1/aa', - 'access_level': access_level, + 'access_to': '1.1.1.1', + 'access_level': 'ro', + 'access_id': 'fake_id' } - self.assertRaises(exception.InvalidInput, self.drv.update_access, - self.ctx, share, [access], None, None) + expected = {'fake_id': {'state': 'active'}} + self.assertEqual( + expected, self.drv.update_access( + self.ctx, SHARE, [access], None, None)) + self.drv._update_nfs_access.assert_called_with(SHARE, [], ['1.1.1.1']) - def test_update_access__one_ip_ro_add_rule_to_existing(self): - share = {'name': 'share', 'size': 1} - access = [ - { - 'access_type': 'ip', - 'access_to': '5.5.5.5', - 'access_level': 'ro' - }, - { - 'access_type': 'ip', - 'access_to': '1.1.1.1/24', - 'access_level': 'rw' - } - ] - url = 'nas/nfs/' + nexenta_nas.PATH_DELIMITER.join( - (self.pool_name, self.fs_prefix, share['name'])) - sc = self.build_access_security_context('rw', '1.1.1.1', 24) - self.drv.nef.get.return_value = {'securityContexts': [sc]} - - self.drv.update_access(self.ctx, share, access, None, None) - - self.drv.nef.put.assert_called_with( - url, {'securityContexts': [ - sc, self.build_access_security_context('ro', '5.5.5.5')]}) - - def test_update_access__one_ip_ro_add_rule_to_existing_wrong_mask( - self): - share = {'name': 'share', 'size': 1} - access = [ - { - 'access_type': 'ip', - 'access_to': '5.5.5.5/aa', - 'access_level': 'ro' - }, - { - 'access_type': 'ip', - 'access_to': '1.1.1.1/24', - 'access_level': 'rw' - } - ] - sc = self.build_access_security_context('rw', '1.1.1.1', 24) - self.drv.nef.get.return_value = {'securityContexts': [sc]} - - self.assertRaises(exception.InvalidInput, self.drv.update_access, - self.ctx, share, access, None, None) + @ddt.data('rw', 'ro') + def test_update_access__not_ip(self, access_level): + access = { + 'access_type': 'username', + 'access_to': 'some_user', + 'access_level': access_level, + 'access_id': 'fake_id' + } + expected = {'fake_id': {'state': 'error'}} + self.assertEqual(expected, self.drv.update_access( + self.ctx, SHARE, [access], None, None)) @patch('%s._get_capacity_info' % DRV_PATH) @patch('manila.share.driver.ShareDriver._update_share_stats') @@ -353,9 +377,13 @@ class TestNexentaNasDriver(test.TestCase): 'vendor_name': 'Nexenta', 'storage_protocol': 'NFS', 'nfs_mount_point_base': self.cfg.nexenta_mount_point_base, - 'driver_version': '1.0', + 'create_share_from_snapshot_support': True, + 'revert_to_snapshot_support': True, + 'snapshot_support': True, + 'driver_version': DRIVER_VERSION, 'share_backend_name': self.cfg.share_backend_name, 'pools': [{ + 'compression': True, 'pool_name': 'pool1', 'total_capacity_gb': 100, 'free_capacity_gb': 90, @@ -373,6 +401,134 @@ class TestNexentaNasDriver(test.TestCase): def test_get_capacity_info(self): self.drv.nef.get.return_value = { - 'bytesAvailable': 10 * units.Gi, 'bytesUsed': 1 * units.Gi} + 'bytesAvailable': 9 * units.Gi, 'bytesUsed': 1 * units.Gi} self.assertEqual((10, 9, 1), self.drv._get_capacity_info()) + + @patch('%s._set_reservation' % DRV_PATH) + @patch('%s._set_quota' % DRV_PATH) + @patch('%s.NefFilesystems.rename' % RPC_PATH) + @patch('%s.NefFilesystems.get' % RPC_PATH) + def test_manage_existing(self, fs_get, fs_rename, set_res, set_quota): + fs_get.return_value = {'referencedQuotaSize': 1073741824} + old_path = '%s:/%s' % (self.cfg.nexenta_nas_host, 'path_to_fs') + new_path = '%s:/%s' % (self.cfg.nexenta_nas_host, SHARE_PATH) + SHARE['export_locations'] = [{'path': old_path}] + expected = {'size': 2, 'export_locations': [{ + 'path': new_path + }]} + self.assertEqual(expected, self.drv.manage_existing(SHARE, None)) + fs_rename.assert_called_with('path_to_fs', {'newPath': SHARE_PATH}) + set_res.assert_called_with(SHARE, 2) + set_quota.assert_called_with(SHARE, 2) + + @patch('%s.NefSnapshots.create' % RPC_PATH) + def test_create_snapshot(self, snap_create): + self.assertIsNone(self.drv.create_snapshot(self.ctx, SNAPSHOT)) + snap_create.assert_called_once_with({ + 'path': SNAPSHOT['snapshot_path']}) + + @patch('%s.NefSnapshots.delete' % RPC_PATH) + def test_delete_snapshot(self, snap_delete): + self.assertIsNone(self.drv.delete_snapshot(self.ctx, SNAPSHOT)) + payload = {'defer': True} + snap_delete.assert_called_once_with( + SNAPSHOT['snapshot_path'], payload) + + @patch('%s._mount_filesystem' % DRV_PATH) + @patch('%s._remount_filesystem' % DRV_PATH) + @patch('%s.NefFilesystems.delete' % RPC_PATH) + @patch('%s.NefSnapshots.clone' % RPC_PATH) + def test_create_share_from_snapshot( + self, snap_clone, fs_delete, remount_fs, mount_fs): + mount_fs.return_value = 'mount_path' + location = { + 'path': 'mount_path', + 'id': 'share-uuid2' + } + self.assertEqual([location], self.drv.create_share_from_snapshot( + self.ctx, SHARE2, SNAPSHOT)) + + size = int(SHARE2['size'] * units.Gi * 1.1) + payload = { + 'targetPath': SHARE2_PATH, + 'referencedQuotaSize': size, + 'recordSize': self.cfg.nexenta_dataset_record_size, + 'compressionMode': self.cfg.nexenta_dataset_compression, + 'nonBlockingMandatoryMode': False, + 'referencedReservationSize': size + } + snap_clone.assert_called_once_with(SNAPSHOT['snapshot_path'], payload) + + @patch('%s._mount_filesystem' % DRV_PATH) + @patch('%s._remount_filesystem' % DRV_PATH) + @patch('%s.NefFilesystems.delete' % RPC_PATH) + @patch('%s.NefSnapshots.clone' % RPC_PATH) + def test_create_share_from_snapshot_error( + self, snap_clone, fs_delete, remount_fs, mount_fs): + fs_delete.side_effect = jsonrpc.NefException('delete error') + mount_fs.side_effect = jsonrpc.NefException('create error') + self.assertRaises( + jsonrpc.NefException, + self.drv.create_share_from_snapshot, self.ctx, SHARE2, SNAPSHOT) + + size = int(SHARE2['size'] * units.Gi * 1.1) + payload = { + 'targetPath': SHARE2_PATH, + 'referencedQuotaSize': size, + 'recordSize': self.cfg.nexenta_dataset_record_size, + 'compressionMode': self.cfg.nexenta_dataset_compression, + 'nonBlockingMandatoryMode': False, + 'referencedReservationSize': size + } + snap_clone.assert_called_once_with(SNAPSHOT['snapshot_path'], payload) + payload = {'force': True} + fs_delete.assert_called_once_with(SHARE2_PATH, payload) + + @patch('%s.NefFilesystems.rollback' % RPC_PATH) + def test_revert_to_snapshot(self, fs_rollback): + self.assertIsNone(self.drv.revert_to_snapshot( + self.ctx, SNAPSHOT, [], [])) + payload = {'snapshot': 'snapshot-snap_id'} + fs_rollback.assert_called_once_with( + SHARE_PATH, payload) + + @patch('%s._set_reservation' % DRV_PATH) + @patch('%s._set_quota' % DRV_PATH) + def test_extend_share(self, set_quota, set_reservation): + self.assertIsNone(self.drv.extend_share( + SHARE, 2)) + set_quota.assert_called_once_with( + SHARE, 2) + set_reservation.assert_called_once_with( + SHARE, 2) + + @patch('%s.NefFilesystems.get' % RPC_PATH) + @patch('%s._set_reservation' % DRV_PATH) + @patch('%s._set_quota' % DRV_PATH) + def test_shrink_share(self, set_quota, set_reservation, fs_get): + fs_get.return_value = { + 'bytesUsedBySelf': 0.5 * units.Gi + } + self.assertIsNone(self.drv.shrink_share( + SHARE2, 1)) + set_quota.assert_called_once_with( + SHARE2, 1) + set_reservation.assert_called_once_with( + SHARE2, 1) + + @patch('%s.NefFilesystems.set' % RPC_PATH) + def test_set_quota(self, fs_set): + quota = int(2 * units.Gi * 1.1) + payload = {'referencedQuotaSize': quota} + self.assertIsNone(self.drv._set_quota( + SHARE, 2)) + fs_set.assert_called_once_with(SHARE_PATH, payload) + + @patch('%s.NefFilesystems.set' % RPC_PATH) + def test_set_reservation(self, fs_set): + reservation = int(2 * units.Gi * 1.1) + payload = {'referencedReservationSize': reservation} + self.assertIsNone(self.drv._set_reservation( + SHARE, 2)) + fs_set.assert_called_once_with(SHARE_PATH, payload) diff --git a/releasenotes/notes/nexentastor5-v1.1-1ad6c8f7b5cc11b6.yaml b/releasenotes/notes/nexentastor5-v1.1-1ad6c8f7b5cc11b6.yaml new file mode 100644 index 0000000000..9a04aead48 --- /dev/null +++ b/releasenotes/notes/nexentastor5-v1.1-1ad6c8f7b5cc11b6.yaml @@ -0,0 +1,23 @@ +--- +features: + - Added revert to snapshot support for NexentaStor5 driver. + - Added manage existing support for NexentaStor5 driver. +upgrade: + - Added a new config option ``nexenta_ssl_cert_verify``. + This option defines whether the NexentaStor5 driver should check + ssl certificate. + - 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_dataset_record_size``. This option + specifies a suggested block size in for files in a filesystem'