From d40987cf7dbdad325f496589055137db21210357 Mon Sep 17 00:00:00 2001 From: inspur-storage Date: Wed, 7 Mar 2018 19:41:56 +0800 Subject: [PATCH] Manila share driver for Inspur AS13000 series. Features that Inspur AS13000 Driver support: share create/delete, snapshot create/delete, extend size, create_share_from_snapshot, update_access. protocol: nfs/cifs ThirdPartySystems: INSPUR CI Change-Id: If0e1134e80f799186bb7cd057ff0f2d713f39a06 Implements: Blueprint inspur-as13000-manila-driver --- ...hare_back_ends_feature_support_mapping.rst | 8 + manila/opts.py | 2 + manila/share/drivers/inspur/__init__.py | 0 .../share/drivers/inspur/as13000/__init__.py | 0 .../drivers/inspur/as13000/as13000_nas.py | 883 ++++++++++++ manila/tests/conf_fixture.py | 5 + manila/tests/share/drivers/inspur/__init__.py | 0 .../share/drivers/inspur/as13000/__init__.py | 0 .../inspur/as13000/test_as13000_nas.py | 1196 +++++++++++++++++ ...nspur-as13000-driver-41f6b7caea82e46e.yaml | 6 + 10 files changed, 2100 insertions(+) create mode 100644 manila/share/drivers/inspur/__init__.py create mode 100644 manila/share/drivers/inspur/as13000/__init__.py create mode 100644 manila/share/drivers/inspur/as13000/as13000_nas.py create mode 100644 manila/tests/share/drivers/inspur/__init__.py create mode 100644 manila/tests/share/drivers/inspur/as13000/__init__.py create mode 100644 manila/tests/share/drivers/inspur/as13000/test_as13000_nas.py create mode 100644 releasenotes/notes/inspur-as13000-driver-41f6b7caea82e46e.yaml 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 b102ae4a68..40b5d8cf1e 100644 --- a/doc/source/admin/share_back_ends_feature_support_mapping.rst +++ b/doc/source/admin/share_back_ends_feature_support_mapping.rst @@ -67,6 +67,8 @@ Mapping of share drivers and share features support +----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+ | INFINIDAT | Q | \- | Q | \- | Q | Q | \- | Q | Q | +----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+ +| INSPUR | R | \- | R | \- | R | R | \- | \- | \- | ++----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+ | LVM | M | \- | M | \- | M | M | \- | O | O | +----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+ | Quobyte | K | \- | M | M | \- | \- | \- | \- | \- | @@ -136,6 +138,8 @@ Mapping of share drivers and share access rules support +----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+ | INFINIDAT | NFS (Q) | \- | \- | \- | \- | NFS (Q) | \- | \- | \- | \- | +----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+ +| INSPUR | NFS (R) | \- | CIFS (R) | \- | \- | NFS (R) | \- | CIFS (R) | \- | \- | ++----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+ | Oracle ZFSSA | NFS,CIFS(K) | \- | \- | \- | \- | \- | \- | \- | \- | \- | +----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+ | CephFS | NFS (P) | \- | \- | \- | CEPHFS (M) | NFS (P) | \- | \- | \- | CEPHFS (N) | @@ -197,6 +201,8 @@ Mapping of share drivers and security services support +----------------------------------------+------------------+-----------------+------------------+ | INFINIDAT | \- | \- | \- | +----------------------------------------+------------------+-----------------+------------------+ +| INSPUR | \- | \- | \- | ++----------------------------------------+------------------+-----------------+------------------+ | Oracle ZFSSA | \- | \- | \- | +----------------------------------------+------------------+-----------------+------------------+ | CephFS | \- | \- | \- | @@ -274,6 +280,8 @@ More information: :ref:`capabilities_and_extra_specs` +----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+ | QNAP | \- | O | Q | Q | O | Q | \- | O | \- | \- | P | \- | +----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+ +| INSPUR | \- | R | \- | \- | R | \- | \- | R | \- | \- | R | \- | ++----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+ .. note:: diff --git a/manila/opts.py b/manila/opts.py index c0e9deeaf7..af9fdff07f 100644 --- a/manila/opts.py +++ b/manila/opts.py @@ -72,6 +72,7 @@ import manila.share.drivers.hpe.hpe_3par_driver import manila.share.drivers.huawei.huawei_nas import manila.share.drivers.ibm.gpfs import manila.share.drivers.infinidat.infinibox +import manila.share.drivers.inspur.as13000.as13000_nas import manila.share.drivers.lvm import manila.share.drivers.maprfs.maprfs_native import manila.share.drivers.netapp.options @@ -157,6 +158,7 @@ _global_opt_lists = [ manila.share.drivers.infinidat.infinibox.infinidat_auth_opts, manila.share.drivers.infinidat.infinibox.infinidat_connection_opts, manila.share.drivers.infinidat.infinibox.infinidat_general_opts, + manila.share.drivers.inspur.as13000.as13000_nas.inspur_as13000_opts, manila.share.drivers.maprfs.maprfs_native.maprfs_native_share_opts, manila.share.drivers.lvm.share_opts, manila.share.drivers.netapp.options.netapp_proxy_opts, diff --git a/manila/share/drivers/inspur/__init__.py b/manila/share/drivers/inspur/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/share/drivers/inspur/as13000/__init__.py b/manila/share/drivers/inspur/as13000/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/share/drivers/inspur/as13000/as13000_nas.py b/manila/share/drivers/inspur/as13000/as13000_nas.py new file mode 100644 index 0000000000..29aa212c23 --- /dev/null +++ b/manila/share/drivers/inspur/as13000/as13000_nas.py @@ -0,0 +1,883 @@ +# Copyright 2018 Inspur Corp. +# 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. + +""" +Share driver for Inspur AS13000 +""" + +import eventlet +import functools +import json +import re +import requests +import six +import time + +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import units + +from manila import exception +from manila.i18n import _ +from manila.share import driver +from manila.share import utils as share_utils + + +inspur_as13000_opts = [ + cfg.HostAddressOpt( + 'as13000_nas_ip', + required=True, + help='IP address for the AS13000 storage.'), + cfg.PortOpt( + 'as13000_nas_port', + default=8088, + help='Port number for the AS13000 storage.'), + cfg.StrOpt( + 'as13000_nas_login', + required=True, + help='Username for the AS13000 storage'), + cfg.StrOpt( + 'as13000_nas_password', + required=True, + secret=True, + help='Password for the AS13000 storage'), + cfg.ListOpt( + 'as13000_share_pools', + required=True, + help='The Storage Pools Manila should use, a comma separated list'), + cfg.IntOpt( + 'as13000_token_available_time', + default=3600, + help='The effective time of token validity in seconds.') +] + +CONF = cfg.CONF +CONF.register_opts(inspur_as13000_opts) +LOG = logging.getLogger(__name__) + + +def inspur_driver_debug_trace(f): + """Log the method entrance and exit including active backend name. + + This should only be used on Share_Driver class methods. It depends on + having a 'self' argument that is a AS13000_Driver. + """ + @functools.wraps(f) + def wrapper(*args, **kwargs): + driver = args[0] + cls_name = driver.__class__.__name__ + method_name = "%(cls_name)s.%(method)s" % {"cls_name": cls_name, + "method": f.__name__} + backend_name = driver.configuration.share_backend_name + LOG.debug("[%(backend_name)s] Enter %(method_name)s", + {"method_name": method_name, "backend_name": backend_name}) + result = f(*args, **kwargs) + LOG.debug("[%(backend_name)s] Leave %(method_name)s", + {"method_name": method_name, "backend_name": backend_name}) + return result + + return wrapper + + +class RestAPIExecutor(object): + def __init__(self, hostname, port, username, password): + self._hostname = hostname + self._port = port + self._username = username + self._password = password + self._token_pool = [] + self._token_size = 1 + + def logins(self): + """login the AS13000 and store the token in token_pool""" + times = self._token_size + while times > 0: + token = self.login() + self._token_pool.append(token) + times = times - 1 + LOG.debug('Logged into the AS13000.') + + def login(self): + """login in the AS13000 and return the token""" + method = 'security/token' + params = {'name': self._username, 'password': self._password} + token = self.send_rest_api(method=method, params=params, + request_type='post').get('token') + return token + + def logout(self): + method = 'security/token' + self.send_rest_api(method=method, request_type='delete') + + def refresh_token(self, force=False): + if force is True: + for i in range(self._token_size): + self._token_pool = [] + token = self.login() + self._token_pool.append(token) + else: + for i in range(self._token_size): + self.logout() + token = self.login() + self._token_pool.append(token) + LOG.debug('Tokens have been refreshed.') + + def send_rest_api(self, method, params=None, request_type='post'): + attempts = 3 + msge = '' + while attempts > 0: + attempts -= 1 + try: + return self.send_api(method, params, request_type) + except exception.NetworkException as e: + msge = six.text_type(e) + LOG.error(msge) + + self.refresh_token(force=True) + eventlet.sleep(1) + except exception.ShareBackendException as e: + msge = six.text_type(e) + break + + msg = (_('Access RestAPI /rest/%(method)s by %(type)s failed,' + ' error: %(msge)s') % {'method': method, + 'msge': msge, + 'type': request_type}) + LOG.error(msg) + raise exception.ShareBackendException(msg) + + @staticmethod + def do_request(cmd, url, header, data): + LOG.debug('CMD: %(cmd)s, URL: %(url)s, DATA: %(data)s', + {'cmd': cmd, 'url': url, 'data': data}) + if cmd == 'post': + req = requests.post(url, + data=data, + headers=header) + elif cmd == 'get': + req = requests.get(url, + data=data, + headers=header) + elif cmd == 'put': + req = requests.put(url, + data=data, + headers=header) + elif cmd == 'delete': + req = requests.delete(url, + data=data, + headers=header) + else: + msg = (_('Unsupported cmd: %s') % cmd) + raise exception.ShareBackendException(msg) + + response = req.json() + code = req.status_code + LOG.debug('CODE: %(code)s, RESPONSE: %(response)s', + {'code': code, 'response': response}) + + if code != 200: + msg = (_('Code: %(code)s, URL: %(url)s, Message: %(msg)s') + % {'code': req.status_code, + 'url': req.url, + 'msg': req.text}) + LOG.error(msg) + raise exception.NetworkException(msg) + + return response + + def send_api(self, method, params=None, request_type='post'): + if params: + params = json.dumps(params) + + url = ('http://%(hostname)s:%(port)s/%(rest)s/%(method)s' + % {'hostname': self._hostname, + 'port': self._port, + 'rest': 'rest', + 'method': method}) + + # header is not needed when the driver login the backend + if method == 'security/token': + # token won't be return to the token_pool + if request_type == 'delete': + header = {'X-Auth-Token': self._token_pool.pop(0)} + else: + header = None + else: + if len(self._token_pool) == 0: + self.logins() + token = self._token_pool.pop(0) + header = {'X-Auth-Token': token} + self._token_pool.append(token) + + response = self.do_request(request_type, url, header, params) + + try: + code = response.get('code') + if code == 0: + if request_type == 'get': + data = response.get('data') + else: + if method == 'security/token': + data = response.get('data') + else: + data = response.get('message') + data = str(data).lower() + if hasattr(data, 'success'): + return + elif code == 301: + msg = _('Token is expired') + LOG.error(msg) + raise exception.NetworkException(msg) + else: + message = response.get('message') + msg = (_('Unexpected RestAPI response: %(code)d %(msg)s') % { + 'code': code, 'msg': message}) + LOG.error(msg) + raise exception.ShareBackendException(msg) + except ValueError: + msg = _("Deal with response failed") + raise exception.ShareBackendException(msg) + + return data + + +class AS13000ShareDriver(driver.ShareDriver): + + """AS13000 Share Driver + + Version history: + V1.0.0: Initial version + Driver support: + share create/delete, + snapshot create/delete, + extend size, + create_share_from_snapshot, + update_access. + protocol: NFS/CIFS + + """ + + VENDOR = 'INSPUR' + VERSION = '1.0.0' + PROTOCOL = 'NFS_CIFS' + + def __init__(self, *args, **kwargs): + super(AS13000ShareDriver, self).__init__(False, *args, **kwargs) + self.configuration.append_config_values(inspur_as13000_opts) + self.hostname = self.configuration.as13000_nas_ip + self.port = self.configuration.as13000_nas_port + self.username = self.configuration.as13000_nas_login + self.password = self.configuration.as13000_nas_password + self.token_available_time = (self.configuration. + as13000_token_available_time) + self.pools = self.configuration.as13000_share_pools + # base dir detail contain the information which we will use + # when we create subdirectorys + self.base_dir_detail = None + self._token_time = 0 + self.ips = [] + self._rest = RestAPIExecutor(self.hostname, self.port, + self.username, self.password) + + @inspur_driver_debug_trace + def do_setup(self, context): + # get access tokens + self._rest.logins() + self._token_time = time.time() + + # Check the pool in conf exist in the backend + self._validate_pools_exist() + + # get the base directory detail + self.base_dir_detail = self._get_directory_detail(self.pools[0]) + + # get all backend node ip + self.ips = self._get_nodes_ips() + + @inspur_driver_debug_trace + def check_for_setup_error(self): + if self.base_dir_detail is None: + msg = _('The pool status is not right') + raise exception.ShareBackendException(msg) + + if len(self.ips) == 0: + msg = _('All backend nodes are down') + raise exception.ShareBackendException(msg) + + @inspur_driver_debug_trace + def create_share(self, context, share, share_server=None): + """Create a share.""" + pool, name, size, proto = self._get_share_instance_pnsp(share) + + # create directory first + share_path = self._create_directory(share_name=name, + pool_name=pool) + + # then create nfs or cifs share + if proto == 'nfs': + self._create_nfs_share(share_path=share_path) + else: + self._create_cifs_share(share_name=name, + share_path=share_path) + + # finally we set the quota of directory + self._set_directory_quota(share_path, size) + + locations = self._get_location_path(name, share_path, proto) + LOG.debug('Create share: name:%(name)s' + ' protocol:%(proto)s,location: %(loc)s', + {'name': name, 'proto': proto, 'loc': locations}) + return locations + + @inspur_driver_debug_trace + def create_share_from_snapshot(self, context, share, snapshot, + share_server=None): + """Create a share from snapshot.""" + pool, name, size, proto = self._get_share_instance_pnsp(share) + + # create directory first + share_path = self._create_directory(share_name=name, + pool_name=pool) + + # as quota must be set when directory is empty + # then we set the quota of directory + self._set_directory_quota(share_path, size) + + # and next clone snapshot to dest_path + self._clone_directory_to_dest(snapshot=snapshot, dest_path=share_path) + + # finally create share + if proto == 'nfs': + self._create_nfs_share(share_path=share_path) + else: + self._create_cifs_share(share_name=name, + share_path=share_path) + + locations = self._get_location_path(name, share_path, proto) + LOG.debug('Create share from snapshot:' + ' name:%(name)s protocol:%(proto)s,location: %(loc)s', + {'name': name, 'proto': proto, 'loc': locations}) + return locations + + @inspur_driver_debug_trace + def delete_share(self, context, share, share_server=None): + """Delete share.""" + pool, name, _, proto = self._get_share_instance_pnsp(share) + share_path = self._generate_share_path(pool, name) + if proto == 'nfs': + share_backend = self._get_nfs_share(share_path) + if len(share_backend) == 0: + return + else: + self._delete_nfs_share(share_path) + else: + share_backend = self._get_cifs_share(name) + if len(share_backend) == 0: + return + else: + self._delete_cifs_share(name) + self._delete_directory(share_path) + LOG.debug('Delete share: %s', name) + + @inspur_driver_debug_trace + def extend_share(self, share, new_size, share_server=None): + """extend share to new size""" + pool, name, size, proto = self._get_share_instance_pnsp(share) + share_path = self._generate_share_path(pool, name) + self._set_directory_quota(share_path, new_size) + LOG.debug('extend share %(name)s to new size %(size)s GB', + {'name': name, 'size': new_size}) + + @inspur_driver_debug_trace + def ensure_share(self, context, share, share_server=None): + """Ensure that share is exported.""" + pool, name, size, proto = self._get_share_instance_pnsp(share) + share_path = self._generate_share_path(pool, name) + + if proto == 'nfs': + share_backend = self._get_nfs_share(share_path) + elif proto == 'cifs': + share_backend = self._get_cifs_share(name) + else: + msg = (_('Invalid NAS protocol supplied: %s.') % proto) + LOG.error(msg) + raise exception.InvalidInput(msg) + + if len(share_backend) == 0: + raise exception.ShareResourceNotFound(share_id=share['share_id']) + + return self._get_location_path(name, share_path, proto) + + @inspur_driver_debug_trace + def create_snapshot(self, context, snapshot, share_server=None): + """create snapshot of share""" + # !!! Attention the share property is a ShareInstance + share = snapshot['share'] + pool, share_name, _, _ = self._get_share_instance_pnsp(share) + share_path = self._generate_share_path(pool, share_name) + + snap_name = self._generate_snapshot_name(snapshot) + + method = 'snapshot/directory' + request_type = 'post' + params = {'path': share_path, 'snapName': snap_name} + self._rest.send_rest_api(method=method, + params=params, + request_type=request_type) + LOG.debug('Create snapshot %(snap)s for share %(share)s', + {'snap': snap_name, 'share': share_name}) + + @inspur_driver_debug_trace + def delete_snapshot(self, context, snapshot, share_server=None): + """delete snapshot of share""" + # !!! Attention the share property is a ShareInstance + share = snapshot['share'] + pool, share_name, _, _ = self._get_share_instance_pnsp(share) + share_path = self._generate_share_path(pool, share_name) + + # if there are no snapshot exist, driver will return directly + snaps_backend = self._get_snapshots_from_share(share_path) + if len(snaps_backend) == 0: + return + + snap_name = self._generate_snapshot_name(snapshot) + + method = ('snapshot/directory?path=%s&snapName=%s' + % (share_path, snap_name)) + request_type = 'delete' + self._rest.send_rest_api(method=method, request_type=request_type) + LOG.debug('Delete snapshot %(snap)s of share %(share)s', + {'snap': snap_name, 'share': share_name}) + + @staticmethod + def transfer_rule_to_client(proto, rule): + """transfer manila access rule to backend client""" + return dict(name=rule['access_to'], + type=(0 if proto == 'nfs' else 1), + authority=rule['access_level']) + + @inspur_driver_debug_trace + def update_access(self, context, share, access_rules, add_rules, + delete_rules, share_server=None): + """update access of share""" + pool, share_name, _, proto = self._get_share_instance_pnsp(share) + share_path = self._generate_share_path(pool, share_name) + + method = 'file/share/%s' % proto + request_type = 'put' + params = { + 'path': share_path, + 'addedClientList': [], + 'deletedClientList': [], + 'editedClientList': [] + } + + if proto == 'nfs': + share_backend = self._get_nfs_share(share_path) + params['pathAuthority'] = share_backend['pathAuthority'] + else: + params['name'] = share_name + + if add_rules or delete_rules: + to_add_clients = [self.transfer_rule_to_client(proto, rule) + for rule in add_rules] + params['addedClientList'] = to_add_clients + to_del_clients = [self.transfer_rule_to_client(proto, rule) + for rule in delete_rules] + params['deletedClientList'] = to_del_clients + else: + access_clients = [self.transfer_rule_to_client(proto, rule) + for rule in access_rules] + params['addedClientList'] = access_clients + self._clear_access(share) + + self._rest.send_rest_api(method=method, + params=params, + request_type=request_type) + LOG.debug('complete the update access work for share %s', share_name) + + @inspur_driver_debug_trace + def _update_share_stats(self, data=None): + """update the backend stats including driver info and pools info""" + # Do a check of the token validity each time we update share stats, + # do a refresh if token already expires + time_difference = time.time() - self._token_time + if time_difference > self.token_available_time: + self._rest.refresh_token() + self._token_time = time.time() + LOG.debug('Token of Driver has been refreshed') + + data = { + 'vendor_name': self.VENDOR, + 'driver_version': self.VERSION, + 'storage_protocol': self.PROTOCOL, + 'share_backend_name': + self.configuration.safe_get('share_backend_name'), + 'snapshot_support': True, + 'create_share_from_snapshot_support': True, + 'pools': [self._get_pool_stats(pool) for pool in self.pools] + } + + super(AS13000ShareDriver, self)._update_share_stats(data) + + @inspur_driver_debug_trace + def _clear_access(self, share): + """clear all access of share""" + pool, share_name, size, proto = self._get_share_instance_pnsp(share) + share_path = self._generate_share_path(pool, share_name) + + method = 'file/share/%s' % proto + request_type = 'put' + params = { + 'path': share_path, + 'addedClientList': [], + 'deletedClientList': [], + 'editedClientList': [] + } + + if proto == 'nfs': + share_backend = self._get_nfs_share(share_path) + params['deletedClientList'] = share_backend['clientList'] + params['pathAuthority'] = share_backend['pathAuthority'] + else: + share_backend = self._get_cifs_share(share_name) + params['deletedClientList'] = share_backend['userList'] + params['name'] = share_name + + self._rest.send_rest_api(method=method, + params=params, + request_type=request_type) + LOG.debug('Clear all the access of share %s', share_name) + + @inspur_driver_debug_trace + def _validate_pools_exist(self): + """Check the pool in conf exist in the backend""" + available_pools = self._get_directory_list('/') + for pool in self.pools: + if pool not in available_pools: + msg = (_('Pool %s is not exist in backend storage.') % pool) + LOG.error(msg) + raise exception.InvalidInput(reason=msg) + + @inspur_driver_debug_trace + def _get_directory_quota(self, path): + """get the quota of directory""" + method = 'file/quota/directory?path=/%s' % path + request_type = 'get' + data = self._rest.send_rest_api(method=method, + request_type=request_type) + quota = data.get('hardthreshold') + if quota is None: + # the method of '_update_share_stats' will check quota of pools. + # To avoid return NONE for pool info, so raise this exception + msg = (_(r'Quota of pool: /%s is not set, ' + r'please set it in GUI of AS13000') % path) + LOG.error(msg) + raise exception.ShareBackendException(msg=msg) + + hardunit = data.get('hardunit') + used_capacity = data.get('capacity') + used_capacity = (str(used_capacity)).upper() + used_capacity = self._unit_convert(used_capacity) + + if hardunit == 1: + quota = quota * 1024 + total_capacity = int(quota) + used_capacity = int(used_capacity) + return total_capacity, used_capacity + + def _get_pool_stats(self, path): + """Get the stats of pools, such as capacity and other information.""" + + total_capacity, used_capacity = self._get_directory_quota(path) + free_capacity = total_capacity - used_capacity + + pool = { + 'pool_name': path, + 'reserved_percentage': + self.configuration.reserved_share_percentage, + 'max_over_subscription_ratio': + self.configuration.max_over_subscription_ratio, + 'dedupe': False, + 'compression': False, + 'qos': False, + 'thin_provisioning': True, + 'total_capacity_gb': total_capacity, + 'free_capacity_gb': free_capacity, + 'allocated_capacity_gb': used_capacity, + 'snapshot_support': True, + 'create_share_from_snapshot_support': True + } + + return pool + + @inspur_driver_debug_trace + def _get_directory_list(self, path): + """Get all the directory list of target path""" + method = 'file/directory?path=%s' % path + request_type = 'get' + directory_list = self._rest.send_rest_api(method=method, + request_type=request_type) + dir_list = [] + for directory in directory_list: + dir_list.append(directory['name']) + return dir_list + + @inspur_driver_debug_trace + def _create_directory(self, share_name, pool_name): + """create a directory for share""" + + method = 'file/directory' + request_type = 'post' + params = {'name': share_name, + 'parentPath': self.base_dir_detail['path'], + 'authorityInfo': self.base_dir_detail['authorityInfo'], + 'dataProtection': self.base_dir_detail['dataProtection'], + 'poolName': self.base_dir_detail['poolName']} + self._rest.send_rest_api(method=method, + params=params, + request_type=request_type) + + return self._generate_share_path(pool_name, share_name) + + @inspur_driver_debug_trace + def _delete_directory(self, share_path): + """delete the directory when delete share""" + method = 'file/directory?path=%s' % share_path + request_type = 'delete' + self._rest.send_rest_api(method=method, request_type=request_type) + + @inspur_driver_debug_trace + def _set_directory_quota(self, share_path, quota): + """set directory quota for share""" + method = 'file/quota/directory' + request_type = 'put' + params = {'path': share_path, 'hardthreshold': quota, 'hardunit': 2} + self._rest.send_rest_api(method=method, + params=params, + request_type=request_type) + + @inspur_driver_debug_trace + def _create_nfs_share(self, share_path): + """create a NFS share""" + method = 'file/share/nfs' + request_type = 'post' + params = {'path': share_path, 'pathAuthority': 'rw', 'client': []} + self._rest.send_rest_api(method=method, + params=params, + request_type=request_type) + + @inspur_driver_debug_trace + def _delete_nfs_share(self, share_path): + """Delete the NFS share""" + method = 'file/share/nfs?path=%s' % share_path + request_type = 'delete' + self._rest.send_rest_api(method=method, request_type=request_type) + + @inspur_driver_debug_trace + def _get_nfs_share(self, share_path): + """Get the nfs share in backend""" + method = 'file/share/nfs?path=%s' % share_path + request_type = 'get' + share_backend = self._rest.send_rest_api(method=method, + request_type=request_type) + return share_backend + + @inspur_driver_debug_trace + def _create_cifs_share(self, share_name, share_path): + """Create a CIFS share.""" + method = 'file/share/cifs' + request_type = 'post' + params = {'path': share_path, + 'name': share_name, + 'userlist': []} + self._rest.send_rest_api(method=method, + params=params, + request_type=request_type) + + @inspur_driver_debug_trace + def _delete_cifs_share(self, share_name): + """Delete the CIFS share.""" + method = 'file/share/cifs?name=%s' % share_name + request_type = 'delete' + self._rest.send_rest_api(method=method, request_type=request_type) + + @inspur_driver_debug_trace + def _get_cifs_share(self, share_name): + """Get the CIFS share in backend""" + method = 'file/share/cifs?name=%s' % share_name + request_type = 'get' + share_backend = self._rest.send_rest_api(method=method, + request_type=request_type) + return share_backend + + @inspur_driver_debug_trace + def _clone_directory_to_dest(self, snapshot, dest_path): + """Clone the directory to the new directory""" + # get the origin share name of the snapshot + share_instance = snapshot['share_instance'] + pool, name, _, _ = self._get_share_instance_pnsp(share_instance) + share_path = self._generate_share_path(pool, name) + + # get the snapshot instance name + snap_name = self._generate_snapshot_name(snapshot) + + method = 'snapshot/directory/clone' + request_type = 'post' + params = {'path': share_path, + 'snapName': snap_name, + 'destPath': dest_path} + self._rest.send_rest_api(method=method, + params=params, + request_type=request_type) + LOG.debug('Clone Path: %(path)s Snapshot: %(snap)s to Path %(dest)s', + {'path': share_path, 'snap': snap_name, 'dest': dest_path}) + + @inspur_driver_debug_trace + def _get_snapshots_from_share(self, path): + """get all the snapshot of share""" + method = 'snapshot/directory?path=%s' % path + request_type = 'get' + snaps = self._rest.send_rest_api(method=method, + request_type=request_type) + return snaps + + @inspur_driver_debug_trace + def _get_location_path(self, share_name, share_path, share_proto): + """return all the location of all nodes""" + if share_proto == 'nfs': + location = [ + {'path': r'%(ip)s:%(share_path)s' + % {'ip': ip, 'share_path': share_path}} + for ip in self.ips] + else: + location = [ + {'path': r'\\%(ip)s\%(share_name)s' + % {'ip': ip, 'share_name': share_name}} + for ip in self.ips] + + return location + + def _get_nodes_virtual_ips(self): + """Get the virtual ip list of the node""" + method = 'ctdb/set' + request_type = 'get' + ctdb_set = self._rest.send_rest_api(method=method, + request_type=request_type) + virtual_ips = [] + for vip in ctdb_set['virtualIpList']: + ip = vip['ip'].split('/')[0] + virtual_ips.append(ip) + return virtual_ips + + def _get_nodes_physical_ips(self): + """Get the physical ip of all the backend nodes""" + method = 'cluster/node/cache' + request_type = 'get' + cached_nodes = self._rest.send_rest_api(method=method, + request_type=request_type) + node_ips = [] + for node in cached_nodes: + if node['runningStatus'] == 1 and node['healthStatus'] == 1: + node_ips.append(node['nodeIp']) + + return node_ips + + def _get_nodes_ips(self): + """Return both the physical ip and virtual ip""" + virtual_ips = self._get_nodes_virtual_ips() + physical_ips = self._get_nodes_physical_ips() + + return virtual_ips + physical_ips + + def _get_share_instance_pnsp(self, share_instance): + """Get pool, name, size, proto information of a share instance. + + AS13000 require all the names can only consist of letters,numbers, + and undercores,and must begin with a letter. + Also the length of name must less than 32 character. + The driver will use the ID as the name in backend, + add 'share_' to the beginning,and convert '-' to '_' + """ + pool = share_utils.extract_host(share_instance['host'], level='pool') + name = self._generate_share_name(share_instance) + # a share instance may not contain size attr. + try: + size = share_instance['size'] + except AttributeError: + size = None + + # a share instance may not contain proto attr. + try: + proto = share_instance['share_proto'].lower() + except AttributeError: + proto = None + + LOG.debug("Pool %s, Name: %s, Size: %s, Protocol: %s", + pool, name, size, proto) + + return pool, name, size, proto + + def _unit_convert(self, capacity): + """Convert all units to GB""" + capacity = str(capacity) + capacity = capacity.upper() + try: + unit_of_used = re.findall(r'[A-Z]', capacity) + unit_of_used = ''.join(unit_of_used) + except BaseException: + unit_of_used = '' + capacity = capacity.replace(unit_of_used, '') + capacity = float(capacity.replace(unit_of_used, '')) + if unit_of_used in ['B', '']: + capacity = capacity / units.Gi + elif unit_of_used in ['K', 'KB']: + capacity = capacity / units.Mi + elif unit_of_used in ['M', 'MB']: + capacity = capacity / units.Ki + elif unit_of_used in ['G', 'GB']: + capacity = capacity + elif unit_of_used in ['T', 'TB']: + capacity = capacity * units.Ki + elif unit_of_used in ['E', 'EB']: + capacity = capacity * units.Mi + + capacity = '%.0f' % capacity + return float(capacity) + + def _format_name(self, name): + """format name to meet the backend requirements""" + name = name[0:32] + name = name.replace('-', '_') + return name + + def _generate_share_name(self, share_instance): + share_name = 'share_%s' % share_instance['id'] + return self._format_name(share_name) + + def _generate_snapshot_name(self, snapshot_instance): + snap_name = 'snap_%s' % snapshot_instance['id'] + return self._format_name(snap_name) + + @staticmethod + def _generate_share_path(pool, share_name): + return r'/%s/%s' % (pool, share_name) + + def _get_directory_detail(self, directory): + method = 'file/directory/detail?path=/%s' % directory + request_type = 'get' + details = self._rest.send_rest_api(method=method, + request_type=request_type) + return details[0] diff --git a/manila/tests/conf_fixture.py b/manila/tests/conf_fixture.py index 1baeedeb93..4c485b5752 100644 --- a/manila/tests/conf_fixture.py +++ b/manila/tests/conf_fixture.py @@ -55,6 +55,11 @@ def set_defaults(conf): _safe_set_of_opts(conf, 'hitachi_hsp_username', 'hsp_user') _safe_set_of_opts(conf, 'hitachi_hsp_password', 'hsp_password') + _safe_set_of_opts(conf, 'as13000_nas_ip', '1.1.1.1') + _safe_set_of_opts(conf, 'as13000_nas_login', 'admin') + _safe_set_of_opts(conf, 'as13000_nas_password', 'password') + _safe_set_of_opts(conf, 'as13000_share_pools', 'pool0') + _safe_set_of_opts(conf, 'qnap_management_url', 'http://1.2.3.4:8080') _safe_set_of_opts(conf, 'qnap_share_ip', '1.2.3.4') _safe_set_of_opts(conf, 'qnap_nas_login', 'admin') diff --git a/manila/tests/share/drivers/inspur/__init__.py b/manila/tests/share/drivers/inspur/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/tests/share/drivers/inspur/as13000/__init__.py b/manila/tests/share/drivers/inspur/as13000/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/tests/share/drivers/inspur/as13000/test_as13000_nas.py b/manila/tests/share/drivers/inspur/as13000/test_as13000_nas.py new file mode 100644 index 0000000000..ef7289545e --- /dev/null +++ b/manila/tests/share/drivers/inspur/as13000/test_as13000_nas.py @@ -0,0 +1,1196 @@ +# Copyright 2018 Inspur Corp. +# 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. + +""" +Share driver test for Inspur AS13000 +""" + +import ddt +import json +import mock +from oslo_config import cfg +import requests +import time + +from manila import context +from manila import exception +from manila.share import driver +from manila.share.drivers.inspur.as13000 import as13000_nas +from manila import test +from manila.tests import fake_share + +CONF = cfg.CONF + + +class FakeConfig(object): + def __init__(self, *args, **kwargs): + self.driver_handles_share_servers = False + self.share_driver = 'fake_share_driver_name' + self.share_backend_name = 'fake_as13000' + self.as13000_nas_ip = kwargs.get( + 'as13000_nas_ip', 'some_ip') + self.as13000_nas_port = kwargs.get( + 'as13000_nas_port', 'some_port') + self.as13000_nas_login = kwargs.get( + 'as13000_nas_login', 'username') + self.as13000_nas_password = kwargs.get( + 'as13000_nas_password', 'password') + self.as13000_share_pools = kwargs.get( + 'as13000_share_pools', ['fakepool']) + self.as13000_token_available_time = kwargs.get( + 'as13000_token_available_time', 3600) + self.network_config_group = kwargs.get( + "network_config_group", "fake_network_config_group") + self.admin_network_config_group = kwargs.get( + "admin_network_config_group", "fake_admin_network_config_group") + self.config_group = kwargs.get("config_group", "fake_config_group") + self.reserved_share_percentage = kwargs.get( + "reserved_share_percentage", 0) + self.max_over_subscription_ratio = kwargs.get( + "max_over_subscription_ratio", 20.0) + self.filter_function = kwargs.get("filter_function", None) + self.goodness_function = kwargs.get("goodness_function", None) + + def safe_get(self, key): + return getattr(self, key) + + def append_config_values(self, *args, **kwargs): + pass + + +test_config = FakeConfig() + + +class FakeResponse(object): + def __init__(self, status, output): + self.status_code = status + self.text = 'return message' + self._json = output + + def json(self): + return self._json + + def close(self): + pass + + +@ddt.ddt +class RestAPIExecutorTestCase(test.TestCase): + def setUp(self): + self.rest_api = as13000_nas.RestAPIExecutor( + test_config.as13000_nas_ip, + test_config.as13000_nas_port, + test_config.as13000_nas_login, + test_config.as13000_nas_password) + super(RestAPIExecutorTestCase, self).setUp() + + def test_logins(self): + mock_login = self.mock_object(self.rest_api, 'login', + mock.Mock(return_value='fake_token')) + self.rest_api.logins() + mock_login.assert_called_once() + + def test_login(self): + fake_response = { + 'token': 'fake_token', + 'expireTime': '7200', + 'type': 0} + mock_sra = self.mock_object(self.rest_api, 'send_rest_api', + mock.Mock(return_value=fake_response)) + result = self.rest_api.login() + + self.assertEqual('fake_token', result) + + login_params = {'name': test_config.as13000_nas_login, + 'password': test_config.as13000_nas_password} + mock_sra.assert_called_once_with(method='security/token', + params=login_params, + request_type='post') + + def test_logout(self): + mock_sra = self.mock_object(self.rest_api, 'send_rest_api', + mock.Mock(return_value=None)) + self.rest_api.logout() + mock_sra.assert_called_once_with( + method='security/token', request_type='delete') + + @ddt.data(True, False) + def test_refresh_token(self, force): + mock_login = self.mock_object(self.rest_api, 'login', + mock.Mock(return_value='fake_token')) + mock_logout = self.mock_object(self.rest_api, 'logout', + mock.Mock()) + self.rest_api.refresh_token(force) + if force is not True: + mock_logout.assert_called_once_with() + mock_login.assert_called_once_with() + + def test_send_rest_api(self): + expected = {'value': 'abc'} + mock_sa = self.mock_object(self.rest_api, 'send_api', + mock.Mock(return_value=expected)) + result = self.rest_api.send_rest_api( + method='fake_method', + params='fake_params', + request_type='fake_type') + self.assertEqual(expected, result) + mock_sa.assert_called_once_with( + 'fake_method', + 'fake_params', + 'fake_type') + + def test_send_rest_api_retry(self): + expected = {'value': 'abc'} + mock_sa = self.mock_object( + self.rest_api, + 'send_api', + mock.Mock( + side_effect=( + exception.NetworkException, + expected))) + # mock.Mock(side_effect=exception.NetworkException)) + mock_rt = self.mock_object(self.rest_api, 'refresh_token', mock.Mock()) + result = self.rest_api.send_rest_api( + method='fake_method', + params='fake_params', + request_type='fake_type' + ) + self.assertEqual(expected, result) + + mock_sa.assert_called_with( + 'fake_method', + 'fake_params', + 'fake_type') + mock_rt.assert_called_with(force=True) + + def test_send_rest_api_3times_fail(self): + mock_sa = self.mock_object( + self.rest_api, 'send_api', mock.Mock( + side_effect=(exception.NetworkException))) + mock_rt = self.mock_object(self.rest_api, 'refresh_token', mock.Mock()) + self.assertRaises( + exception.ShareBackendException, + self.rest_api.send_rest_api, + method='fake_method', + params='fake_params', + request_type='fake_type') + mock_sa.assert_called_with('fake_method', + 'fake_params', + 'fake_type') + mock_rt.assert_called_with(force=True) + + def test_send_rest_api_backend_error_fail(self): + mock_sa = self.mock_object(self.rest_api, 'send_api', mock.Mock( + side_effect=(exception.ShareBackendException( + 'fake_error_message')))) + mock_rt = self.mock_object(self.rest_api, 'refresh_token') + self.assertRaises( + exception.ShareBackendException, + self.rest_api.send_rest_api, + method='fake_method', + params='fake_params', + request_type='fake_type') + mock_sa.assert_called_with('fake_method', + 'fake_params', + 'fake_type') + mock_rt.assert_not_called() + + @ddt.data( + {'method': 'fake_method', 'request_type': 'post', 'params': + {'fake_param': 'fake_value'}}, + {'method': 'fake_method', 'request_type': 'get', 'params': + {'fake_param': 'fake_value'}}, + {'method': 'fake_method', 'request_type': 'delete', 'params': + {'fake_param': 'fake_value'}}, + {'method': 'fake_method', 'request_type': 'put', 'params': + {'fake_param': 'fake_value'}}, ) + @ddt.unpack + def test_send_api(self, method, params, request_type): + self.rest_api._token_pool = ['fake_token'] + if request_type in ('post', 'delete', 'put'): + fake_output = {'code': 0, 'message': 'success'} + elif request_type == 'get': + fake_output = {'code': 0, 'data': 'fake_date'} + + fake_response = FakeResponse(200, fake_output) + mock_request = self.mock_object(requests, + request_type, + mock.Mock(return_value=fake_response)) + self.rest_api.send_api(method, + params=params, + request_type=request_type) + + url = 'http://%s:%s/rest/%s' % (test_config.as13000_nas_ip, + test_config.as13000_nas_port, + method) + headers = {'X-Auth-Token': 'fake_token'} + mock_request.assert_called_once_with(url, + data=json.dumps(params), + headers=headers) + + @ddt.data({'method': r'security/token', + 'params': {'name': test_config.as13000_nas_login, + 'password': test_config.as13000_nas_password}, + 'request_type': 'post'}, + {'method': r'security/token', + 'params': None, + 'request_type': 'delete'}) + @ddt.unpack + def test_send_api_access_success(self, method, params, request_type): + if request_type == 'post': + fake_value = {'code': 0, 'data': { + 'token': 'fake_token', + 'expireTime': '7200', + 'type': 0}} + mock_requests = self.mock_object( + requests, 'post', mock.Mock( + return_value=FakeResponse( + 200, fake_value))) + result = self.rest_api.send_api(method, params, request_type) + self.assertEqual(fake_value['data'], result) + mock_requests.assert_called_once_with( + 'http://%s:%s/rest/%s' % + (test_config.as13000_nas_ip, + test_config.as13000_nas_port, + method), + data=json.dumps(params), + headers=None) + if request_type == 'delete': + fake_value = {'code': 0, 'message': 'Success!'} + self.rest_api._token_pool = ['fake_token'] + mock_requests = self.mock_object( + requests, 'delete', mock.Mock( + return_value=FakeResponse( + 200, fake_value))) + self.rest_api.send_api(method, params, request_type) + mock_requests.assert_called_once_with( + 'http://%s:%s/rest/%s' % + (test_config.as13000_nas_ip, + test_config.as13000_nas_port, + method), + data=None, + headers={'X-Auth-Token': 'fake_token'}) + + def test_send_api_wrong_access_fail(self): + req_params = {'method': r'security/token', + 'params': {'name': test_config.as13000_nas_login, + 'password': 'fake_password'}, + 'request_type': 'post'} + fake_value = {'message': ' User name or password error.', 'code': 400} + mock_request = self.mock_object( + requests, 'post', mock.Mock( + return_value=FakeResponse( + 200, fake_value))) + self.assertRaises( + exception.ShareBackendException, + self.rest_api.send_api, + method=req_params['method'], + params=req_params['params'], + request_type=req_params['request_type']) + mock_request.assert_called_once_with( + 'http://%s:%s/rest/%s' % + (test_config.as13000_nas_ip, + test_config.as13000_nas_port, + req_params['method']), + data=json.dumps( + req_params['params']), + headers=None) + + def test_send_api_token_overtime_fail(self): + self.rest_api._token_pool = ['fake_token'] + fake_value = {'method': 'fake_url', + 'params': 'fake_params', + 'reuest_type': 'post'} + fake_out_put = {'message': 'Unauthorized access!', 'code': 301} + mock_requests = self.mock_object( + requests, 'post', mock.Mock( + return_value=FakeResponse( + 200, fake_out_put))) + self.assertRaises(exception.NetworkException, + self.rest_api.send_api, + method='fake_url', + params='fake_params', + request_type='post') + mock_requests.assert_called_once_with( + 'http://%s:%s/rest/%s' % + (test_config.as13000_nas_ip, + test_config.as13000_nas_port, + fake_value['method']), + data=json.dumps('fake_params'), + headers={ + 'X-Auth-Token': 'fake_token'}) + + def test_send_api_fail(self): + self.rest_api._token_pool = ['fake_token'] + fake_output = {'code': 100, 'message': 'fake_message'} + mock_request = self.mock_object( + requests, 'post', mock.Mock( + return_value=FakeResponse( + 200, fake_output))) + self.assertRaises( + exception.ShareBackendException, + self.rest_api.send_api, + method='fake_method', + params='fake_params', + request_type='post') + mock_request.assert_called_once_with( + 'http://%s:%s/rest/%s' % + (test_config.as13000_nas_ip, + test_config.as13000_nas_port, + 'fake_method'), + data=json.dumps('fake_params'), + headers={'X-Auth-Token': 'fake_token'} + ) + + +@ddt.ddt +class AS13000ShareDriverTestCase(test.TestCase): + def __init__(self, *args, **kwds): + super(AS13000ShareDriverTestCase, self).__init__(*args, **kwds) + self._ctxt = context.get_admin_context() + self.configuration = FakeConfig() + + def setUp(self): + self.mock_object(as13000_nas.CONF, '_check_required_opts') + self.driver = as13000_nas.AS13000ShareDriver( + configuration=self.configuration) + super(AS13000ShareDriverTestCase, self).setUp() + + def test_do_setup(self): + mock_login = self.mock_object( + as13000_nas.RestAPIExecutor, 'logins', mock.Mock()) + mock_vpe = self.mock_object( + self.driver, + '_validate_pools_exist', + mock.Mock()) + mock_gdd = self.mock_object( + self.driver, '_get_directory_detail', mock.Mock( + return_value='{}')) + mock_gni = self.mock_object( + self.driver, '_get_nodes_ips', mock.Mock( + return_value=['fake_ips'])) + self.driver.do_setup(self._ctxt) + mock_login.assert_called_once() + mock_vpe.assert_called_once() + mock_gdd.assert_called_once_with( + test_config.as13000_share_pools[0]) + mock_gni.assert_called_once() + + def test_do_setup_login_fail(self): + mock_login = self.mock_object( + as13000_nas.RestAPIExecutor, 'logins', mock.Mock( + side_effect=exception.ShareBackendException('fake_exception'))) + self.assertRaises( + exception.ShareBackendException, + self.driver.do_setup, + self._ctxt) + mock_login.assert_called_once() + + def test_do_setup_vpe_failed(self): + mock_login = self.mock_object(as13000_nas.RestAPIExecutor, + 'logins', mock.Mock()) + side_effect = exception.InvalidInput(reason='fake_exception') + mock_vpe = self.mock_object(self.driver, + '_validate_pools_exist', + mock.Mock(side_effect=side_effect)) + self.assertRaises(exception.InvalidInput, + self.driver.do_setup, + self._ctxt) + mock_login.assert_called_once() + mock_vpe.assert_called_once() + + def test_check_for_setup_error_base_dir_detail_failed(self): + self.driver.base_dir_detail = None + self.driver.ips = ['fake_ip'] + self.assertRaises( + exception.ShareBackendException, + self.driver.check_for_setup_error) + + def test_check_for_setup_error_node_status_fail(self): + self.driver.base_dir_detail = 'fakepool' + self.driver.ips = [] + self.assertRaises(exception.ShareBackendException, + self.driver.check_for_setup_error) + + @ddt.data('nfs', 'cifs') + def test_create_share(self, share_proto): + share = fake_share.fake_share(share_proto=share_proto) + share_instance = fake_share.fake_share_instance(share, host="H@B#P") + mock_cd = self.mock_object(self.driver, '_create_directory', + mock.Mock(return_value='/fake/path')) + mock_cns = self.mock_object(self.driver, '_create_nfs_share') + mock_ccs = self.mock_object(self.driver, '_create_cifs_share') + mock_sdq = self.mock_object(self.driver, '_set_directory_quota') + + self.driver.ips = ['127.0.0.1'] + locations = self.driver.create_share(self._ctxt, share_instance) + if share_proto == 'nfs': + expect_locations = [{'path': r'127.0.0.1:/fake/path'}] + self.assertEqual(locations, expect_locations) + else: + expect_locations = [{'path': r'\\127.0.0.1\share_fakeinstanceid'}] + self.assertEqual(locations, expect_locations) + + mock_cd.assert_called_once_with(share_name='share_fakeinstanceid', + pool_name='P') + + if share_proto == 'nfs': + mock_cns.assert_called_once_with(share_path='/fake/path') + elif share['share_proto'] is 'cifs': + mock_ccs.assert_called_once_with(share_path='/fake/path', + share_name='share_fakeinstanceid') + + mock_sdq.assert_called_once_with('/fake/path', share['size']) + + @ddt.data('nfs', 'cifs') + def test_create_share_from_snapshot(self, share_proto): + share = fake_share.fake_share(share_proto=share_proto) + share_instance = fake_share.fake_share_instance(share, host="H@B#P") + mock_cd = self.mock_object(self.driver, '_create_directory', + mock.Mock(return_value='/fake/path')) + mock_cns = self.mock_object(self.driver, '_create_nfs_share') + mock_ccs = self.mock_object(self.driver, '_create_cifs_share') + mock_sdq = self.mock_object(self.driver, '_set_directory_quota') + mock_cdtd = self.mock_object(self.driver, '_clone_directory_to_dest') + + self.driver.ips = ['127.0.0.1'] + locations = self.driver.create_share_from_snapshot( + self._ctxt, share_instance, None) + if share_proto == 'nfs': + expect_locations = [{'path': r'127.0.0.1:/fake/path'}] + self.assertEqual(locations, expect_locations) + else: + expect_locations = [{'path': r'\\127.0.0.1\share_fakeinstanceid'}] + self.assertEqual(locations, expect_locations) + + mock_cd.assert_called_once_with(share_name='share_fakeinstanceid', + pool_name='P') + + if share_proto == 'nfs': + mock_cns.assert_called_once_with(share_path='/fake/path') + elif share['share_proto'] is 'cifs': + mock_ccs.assert_called_once_with(share_path='/fake/path', + share_name='share_fakeinstanceid') + + mock_sdq.assert_called_once_with('/fake/path', share['size']) + mock_cdtd.assert_called_once_with(snapshot=None, + dest_path='/fake/path') + + @ddt.data('nfs', 'cifs') + def test_delete_share(self, share_proto): + share = fake_share.fake_share(share_proto=share_proto) + share_instance = fake_share.fake_share_instance(share, host="H@B#P") + expect_share_path = r'/P/share_fakeinstanceid' + + mock_gns = self.mock_object(self.driver, '_get_nfs_share', + mock.Mock(return_value=['fake_share'])) + mock_dns = self.mock_object(self.driver, '_delete_nfs_share') + mock_gcs = self.mock_object(self.driver, '_get_cifs_share', + mock.Mock(return_value=['fake_share'])) + mock_dcs = self.mock_object(self.driver, '_delete_cifs_share') + mock_dd = self.mock_object(self.driver, '_delete_directory') + + self.driver.delete_share(self._ctxt, share_instance) + if share_proto == 'nfs': + mock_gns.assert_called_once_with(expect_share_path) + mock_dns.assert_called_once_with(expect_share_path) + else: + mock_gcs.assert_called_once_with('share_fakeinstanceid') + mock_dcs.assert_called_once_with('share_fakeinstanceid') + + mock_dd.assert_called_once_with(expect_share_path) + + @ddt.data('nfs', 'cifs') + def test_delete_share_not_exist(self, share_proto): + share = fake_share.fake_share(share_proto=share_proto) + share_instance = fake_share.fake_share_instance(share, host="H@B#P") + expect_share_path = r'/P/share_fakeinstanceid' + + mock_gns = self.mock_object(self.driver, '_get_nfs_share', + mock.Mock(return_value=[])) + mock_gcs = self.mock_object(self.driver, '_get_cifs_share', + mock.Mock(return_value=[])) + self.driver.delete_share(self._ctxt, share_instance) + if share_proto == 'nfs': + mock_gns.assert_called_once_with(expect_share_path) + elif share_proto == 'cifs': + mock_gcs.assert_called_once_with('share_fakeinstanceid') + + def test_extend_share(self): + share = fake_share.fake_share() + share_instance = fake_share.fake_share_instance(share, host="H@B#P") + expect_share_path = r'/P/share_fakeinstanceid' + + mock_sdq = self.mock_object(self.driver, '_set_directory_quota') + + self.driver.extend_share(share_instance, 2) + + mock_sdq.assert_called_once_with(expect_share_path, 2) + + @ddt.data('nfs', 'cifs') + def test_ensure_share(self, share_proto): + share = fake_share.fake_share(share_proto=share_proto) + share_instance = fake_share.fake_share_instance(share, host="H@B#P") + + mock_gns = self.mock_object(self.driver, '_get_nfs_share', + mock.Mock(return_value=['fake_share'])) + mock_gcs = self.mock_object(self.driver, '_get_cifs_share', + mock.Mock(return_value=['fake_share'])) + + self.driver.ips = ['127.0.0.1'] + locations = self.driver.ensure_share(self._ctxt, share_instance) + if share_proto == 'nfs': + expect_locations = [{'path': r'127.0.0.1:/P/share_fakeinstanceid'}] + self.assertEqual(locations, expect_locations) + mock_gns.assert_called_once_with(r'/P/share_fakeinstanceid') + else: + expect_locations = [{'path': r'\\127.0.0.1\share_fakeinstanceid'}] + self.assertEqual(locations, expect_locations) + mock_gcs.assert_called_once_with(r'share_fakeinstanceid') + + def test_ensure_share_fail_1(self): + share = fake_share.fake_share() + share_instance = fake_share.fake_share_instance(share, host="H@B#P") + + self.assertRaises(exception.InvalidInput, self.driver.ensure_share, + self._ctxt, share_instance) + + @ddt.data('nfs', 'cifs') + def test_ensure_share_None_share_fail(self, share_proto): + share = fake_share.fake_share(share_proto=share_proto) + share_instance = fake_share.fake_share_instance(share, host="H@B#P") + + mock_gns = self.mock_object(self.driver, '_get_nfs_share', + mock.Mock(return_value=[])) + mock_gcs = self.mock_object(self.driver, '_get_cifs_share', + mock.Mock(return_value=[])) + self.assertRaises(exception.ShareResourceNotFound, + self.driver.ensure_share, + self._ctxt, share_instance) + + if share_proto == 'nfs': + mock_gns.assert_called_once_with(r'/P/share_fakeinstanceid') + elif share['share_proto'] is 'cifs': + mock_gcs.assert_called_once_with(r'share_fakeinstanceid') + + def test_create_snapshot(self): + share = fake_share.fake_share() + share_instance = fake_share.fake_share_instance(share, host="H@B#P") + snapshot_instance_pseudo = { + 'share': share_instance, + 'id': 'fakesnapid' + } + + mock_rest = self.mock_object(as13000_nas.RestAPIExecutor, + 'send_rest_api') + + self.driver.create_snapshot(self._ctxt, snapshot_instance_pseudo) + + method = 'snapshot/directory' + request_type = 'post' + params = {'path': r'/P/share_fakeinstanceid', + 'snapName': 'snap_fakesnapid'} + mock_rest.assert_called_once_with(method=method, + request_type=request_type, + params=params) + + def test_delete_snapshot_normal(self): + share = fake_share.fake_share() + share_instance = fake_share.fake_share_instance(share, host="H@B#P") + snapshot_instance_pseudo = { + 'share': share_instance, + 'id': 'fakesnapid' + } + + mock_gsfs = self.mock_object(self.driver, '_get_snapshots_from_share', + mock.Mock(return_value=['fakesnapshot'])) + mock_rest = self.mock_object(as13000_nas.RestAPIExecutor, + 'send_rest_api') + + self.driver.delete_snapshot(self._ctxt, snapshot_instance_pseudo) + + mock_gsfs.assert_called_once_with('/P/share_fakeinstanceid') + method = ('snapshot/directory?' + 'path=/P/share_fakeinstanceid&snapName=snap_fakesnapid') + request_type = 'delete' + mock_rest.assert_called_once_with(method=method, + request_type=request_type) + + def test_delete_snapshot_not_exist(self): + share = fake_share.fake_share() + share_instance = fake_share.fake_share_instance(share, host="H@B#P") + snapshot_instance_pseudo = { + 'share': share_instance, + 'snapshot_id': 'fakesnapid' + } + + mock_gsfs = self.mock_object(self.driver, '_get_snapshots_from_share', + mock.Mock(return_value=[])) + mock_rest = self.mock_object(as13000_nas.RestAPIExecutor, + 'send_rest_api') + + self.driver.delete_snapshot(self._ctxt, snapshot_instance_pseudo) + + mock_gsfs.assert_called_once_with('/P/share_fakeinstanceid') + mock_rest.assert_not_called() + + @ddt.data('nfs', 'icfs') + def test_transfer_rule_to_client(self, proto): + rule = {'access_to': '1.1.1.1', 'access_level': 'rw'} + + result = self.driver.transfer_rule_to_client(proto, rule) + + client = {'name': '1.1.1.1', 'authority': 'rw'} + if proto == 'nfs': + client.update({'type': 0}) + else: + client.update({'type': 1}) + + self.assertEqual(client, result) + + @ddt.data({'share_proto': 'nfs', 'use_access': True}, + {'share_proto': 'nfs', 'use_access': False}, + {'share_proto': 'cifs', 'use_access': True}, + {'share_proto': 'cifs', 'use_access': False}) + @ddt.unpack + def test_update_access(self, share_proto, use_access): + share = fake_share.fake_share(share_proto=share_proto) + share_instance = fake_share.fake_share_instance(share, host="H@B#P") + + access_rules = [{'access_to': 'fakename1', + 'access_level': 'fakelevel1'}, + {'access_to': 'fakename2', + 'access_level': 'fakelevel2'}] + add_rules = [{'access_to': 'fakename1', 'access_level': 'fakelevel1'}] + del_rules = [{'access_to': 'fakename2', 'access_level': 'fakelevel2'}] + + mock_ca = self.mock_object(self.driver, '_clear_access') + + fake_share_backend = {'pathAuthority': 'fakepathAuthority'} + mock_gns = self.mock_object(self.driver, '_get_nfs_share', + mock.Mock(return_value=fake_share_backend)) + mock_rest = self.mock_object(as13000_nas.RestAPIExecutor, + 'send_rest_api') + if use_access: + self.driver.update_access(self._ctxt, share_instance, + access_rules, [], []) + else: + self.driver.update_access(self._ctxt, share_instance, + [], add_rules, del_rules) + + access_clients = [{'name': rule['access_to'], + 'type': 0 if share_proto == 'nfs' else 1, + 'authority': rule['access_level'] + } for rule in access_rules] + add_clients = [{'name': rule['access_to'], + 'type': 0 if share_proto == 'nfs' else 1, + 'authority': rule['access_level'] + } for rule in add_rules] + del_clients = [{'name': rule['access_to'], + 'type': 0 if share_proto == 'nfs' else 1, + 'authority': rule['access_level'] + } for rule in del_rules] + + params = { + 'path': r'/P/share_fakeinstanceid', + 'addedClientList': [], + 'deletedClientList': [], + 'editedClientList': [] + } + + if share_proto == 'nfs': + mock_gns.assert_called_once_with(r'/P/share_fakeinstanceid') + params['pathAuthority'] = fake_share_backend['pathAuthority'] + else: + params['name'] = 'share_fakeinstanceid' + + if use_access: + mock_ca.assert_called_once_with(share_instance) + params['addedClientList'] = access_clients + else: + params['addedClientList'] = add_clients + params['deletedClientList'] = del_clients + + mock_rest.assert_called_once_with( + method=('file/share/%s' % share_proto), + params=params, + request_type='put') + + def test__update_share_stats(self): + mock_sg = self.mock_object(FakeConfig, 'safe_get', + mock.Mock(return_value='fake_as13000')) + self.driver.pools = ['fake_pool'] + mock_gps = self.mock_object(self.driver, '_get_pool_stats', + mock.Mock(return_value='fake_pool')) + self.driver._token_time = time.time() + mock_rt = self.mock_object(as13000_nas.RestAPIExecutor, + 'refresh_token') + mock_uss = self.mock_object(driver.ShareDriver, '_update_share_stats') + + self.driver._update_share_stats() + + data = {} + data['vendor_name'] = self.driver.VENDOR + data['driver_version'] = self.driver.VERSION + data['storage_protocol'] = self.driver.PROTOCOL + data['share_backend_name'] = 'fake_as13000' + data['snapshot_support'] = True + data['create_share_from_snapshot_support'] = True + data['pools'] = ['fake_pool'] + mock_sg.assert_called_once_with('share_backend_name') + mock_gps.assert_called_once_with('fake_pool') + mock_rt.assert_not_called() + mock_uss.assert_called_once_with(data) + + def test__update_share_stats_refresh_token(self): + mock_sg = self.mock_object(FakeConfig, 'safe_get', + mock.Mock(return_value='fake_as13000')) + self.driver.pools = ['fake_pool'] + mock_gps = self.mock_object(self.driver, '_get_pool_stats', + mock.Mock(return_value='fake_pool')) + self.driver._token_time = ( + time.time() - self.driver.token_available_time - 1) + mock_rt = self.mock_object(as13000_nas.RestAPIExecutor, + 'refresh_token') + mock_uss = self.mock_object(driver.ShareDriver, '_update_share_stats') + + self.driver._update_share_stats() + + data = {} + data['vendor_name'] = self.driver.VENDOR + data['driver_version'] = self.driver.VERSION + data['storage_protocol'] = self.driver.PROTOCOL + data['share_backend_name'] = 'fake_as13000' + data['snapshot_support'] = True + data['create_share_from_snapshot_support'] = True + data['pools'] = ['fake_pool'] + mock_sg.assert_called_once_with('share_backend_name') + mock_gps.assert_called_once_with('fake_pool') + mock_rt.assert_called_once() + mock_uss.assert_called_once_with(data) + + @ddt.data('nfs', 'cifs') + def test__clear_access(self, share_proto): + share = fake_share.fake_share(share_proto=share_proto) + share_instance = fake_share.fake_share_instance(share, host="H@B#P") + + fake_share_backend = {'pathAuthority': 'fakepathAuthority', + 'clientList': ['fakeclient'], + 'userList': ['fakeuser']} + mock_gns = self.mock_object(self.driver, '_get_nfs_share', + mock.Mock(return_value=fake_share_backend)) + mock_gcs = self.mock_object(self.driver, '_get_cifs_share', + mock.Mock(return_value=fake_share_backend)) + mock_rest = self.mock_object(as13000_nas.RestAPIExecutor, + 'send_rest_api') + + self.driver._clear_access(share_instance) + + method = 'file/share/%s' % share_proto + request_type = 'put' + params = { + 'path': r'/P/share_fakeinstanceid', + 'addedClientList': [], + 'deletedClientList': [], + 'editedClientList': [] + } + + if share_proto == 'nfs': + mock_gns.assert_called_once_with(r'/P/share_fakeinstanceid') + + params['deletedClientList'] = fake_share_backend['clientList'] + params['pathAuthority'] = fake_share_backend['pathAuthority'] + else: + mock_gcs.assert_called_once_with('share_fakeinstanceid') + + params['deletedClientList'] = fake_share_backend['userList'] + params['name'] = 'share_fakeinstanceid' + + mock_rest.assert_called_once_with(method=method, + request_type=request_type, + params=params) + + def test__validate_pools_exist(self): + self.driver.pools = ['fakepool'] + mock_gdl = self.mock_object(self.driver, '_get_directory_list', + mock.Mock(return_value=['fakepool'])) + self.driver._validate_pools_exist() + mock_gdl.assert_called_once_with('/') + + def test__validate_pools_exist_fail(self): + self.driver.pools = ['fakepool_fail'] + mock_gdl = self.mock_object(self.driver, '_get_directory_list', + mock.Mock(return_value=['fakepool'])) + self.assertRaises(exception.InvalidInput, + self.driver._validate_pools_exist) + mock_gdl.assert_called_once_with('/') + + @ddt.data(0, 1) + def test__get_directory_quota(self, hardunit): + fake_data = {'hardthreshold': 200, + 'hardunit': hardunit, + 'capacity': '50GB'} + mock_rest = self.mock_object(as13000_nas.RestAPIExecutor, + 'send_rest_api', + mock.Mock(return_value=fake_data)) + + total, used = (self.driver._get_directory_quota('fakepath')) + + if hardunit == 0: + self.assertEqual((200, 50), (total, used)) + else: + self.assertEqual((200 * 1024, 50), (total, used)) + method = 'file/quota/directory?path=/fakepath' + request_type = 'get' + mock_rest.assert_called_once_with(method=method, + request_type=request_type) + + def test__get_directory_quota_fail(self): + fake_data = {'hardthreshold': None, + 'hardunit': 0, + 'capacity': '50GB'} + mock_rest = self.mock_object(as13000_nas.RestAPIExecutor, + 'send_rest_api', + mock.Mock(return_value=fake_data)) + + self.assertRaises(exception.ShareBackendException, + self.driver._get_directory_quota, 'fakepath') + method = 'file/quota/directory?path=/fakepath' + request_type = 'get' + mock_rest.assert_called_once_with(method=method, + request_type=request_type) + + def test__get_pool_stats(self): + mock_gdq = self.mock_object(self.driver, '_get_directory_quota', + mock.Mock(return_value=(200, 50))) + pool = dict() + pool['pool_name'] = 'fakepath' + pool['reserved_percentage'] = 0 + pool['max_over_subscription_ratio'] = 20.0 + pool['dedupe'] = False + pool['compression'] = False + pool['qos'] = False + pool['thin_provisioning'] = True + pool['total_capacity_gb'] = 200 + pool['free_capacity_gb'] = 150 + pool['allocated_capacity_gb'] = 50 + pool['snapshot_support'] = True + pool['create_share_from_snapshot_support'] = True + + result = self.driver._get_pool_stats('fakepath') + self.assertEqual(pool, result) + mock_gdq.assert_called_once_with('fakepath') + + def test__get_directory_list(self): + fake_dir_list = [{'name': 'fakedirectory1', 'size': 20}, + {'name': 'fakedirectory2', 'size': 30}] + mock_rest = self.mock_object(as13000_nas.RestAPIExecutor, + 'send_rest_api', + mock.Mock(return_value=fake_dir_list)) + + expected = ['fakedirectory1', 'fakedirectory2'] + result = self.driver._get_directory_list('/fakepath') + self.assertEqual(expected, result) + method = 'file/directory?path=/fakepath' + mock_rest.assert_called_once_with(method=method, + request_type='get') + + def test__create_directory(self): + base_dir_detail = { + 'path': '/fakepath', + 'authorityInfo': {'user': 'root', + 'group': 'root', + 'authority': 'rwxrwxrwx' + }, + 'dataProtection': {'type': 0, + 'dc': 2, + 'cc': 1, + 'rn': 0, + 'st': 4}, + 'poolName': 'storage_pool' + } + mock_rest = self.mock_object(as13000_nas.RestAPIExecutor, + 'send_rest_api') + + self.driver.base_dir_detail = base_dir_detail + result = self.driver._create_directory('fakename', 'fakepool') + + self.assertEqual('/fakepool/fakename', result) + + method = 'file/directory' + request_type = 'post' + params = {'name': 'fakename', + 'parentPath': base_dir_detail['path'], + 'authorityInfo': base_dir_detail['authorityInfo'], + 'dataProtection': base_dir_detail['dataProtection'], + 'poolName': base_dir_detail['poolName']} + mock_rest.assert_called_once_with(method=method, + request_type=request_type, + params=params) + + def test__delete_directory(self): + mock_rest = self.mock_object(as13000_nas.RestAPIExecutor, + 'send_rest_api') + + self.driver._delete_directory('/fakepath') + + method = 'file/directory?path=/fakepath' + request_type = 'delete' + mock_rest.assert_called_once_with(method=method, + request_type=request_type) + + def test__set_directory_quota(self): + mock_rest = self.mock_object(as13000_nas.RestAPIExecutor, + 'send_rest_api') + + self.driver._set_directory_quota('fakepath', 200) + + method = 'file/quota/directory' + request_type = 'put' + params = {'path': 'fakepath', + 'hardthreshold': 200, + 'hardunit': 2} + mock_rest.assert_called_once_with(method=method, + request_type=request_type, + params=params) + + def test__create_nfs_share(self): + mock_rest = self.mock_object(as13000_nas.RestAPIExecutor, + 'send_rest_api') + + self.driver._create_nfs_share('fakepath') + + method = 'file/share/nfs' + request_type = 'post' + params = {'path': 'fakepath', 'pathAuthority': 'rw', 'client': []} + mock_rest.assert_called_once_with(method=method, + request_type=request_type, + params=params) + + def test__delete_nfs_share(self): + mock_rest = self.mock_object(as13000_nas.RestAPIExecutor, + 'send_rest_api') + + self.driver._delete_nfs_share('/fakepath') + + method = 'file/share/nfs?path=/fakepath' + request_type = 'delete' + mock_rest.assert_called_once_with(method=method, + request_type=request_type) + + def test__get_nfs_share(self): + mock_rest = self.mock_object(as13000_nas.RestAPIExecutor, + 'send_rest_api', + mock.Mock(return_value='fakebackend')) + + result = self.driver._get_nfs_share('/fakepath') + self.assertEqual('fakebackend', result) + + method = 'file/share/nfs?path=/fakepath' + request_type = 'get' + mock_rest.assert_called_once_with(method=method, + request_type=request_type) + + def test__create_cifs_share(self): + mock_rest = self.mock_object(as13000_nas.RestAPIExecutor, + 'send_rest_api') + + self.driver._create_cifs_share('fakename', 'fakepath') + + method = 'file/share/cifs' + request_type = 'post' + params = {'path': 'fakepath', 'name': 'fakename', 'userlist': []} + mock_rest.assert_called_once_with(method=method, + request_type=request_type, + params=params) + + def test__delete_cifs_share(self): + mock_rest = self.mock_object(as13000_nas.RestAPIExecutor, + 'send_rest_api') + + self.driver._delete_cifs_share('fakename') + + method = 'file/share/cifs?name=fakename' + request_type = 'delete' + mock_rest.assert_called_once_with(method=method, + request_type=request_type) + + def test__get_cifs_share(self): + mock_rest = self.mock_object(as13000_nas.RestAPIExecutor, + 'send_rest_api', + mock.Mock(return_value='fakebackend')) + + result = self.driver._get_cifs_share('fakename') + self.assertEqual('fakebackend', result) + + method = 'file/share/cifs?name=fakename' + request_type = 'get' + mock_rest.assert_called_once_with(method=method, + request_type=request_type) + + def test__clone_directory_to_dest(self): + share = fake_share.fake_share() + share_instance = fake_share.fake_share_instance(share, host="H@B#P") + snapshot_instance_pseudo = { + 'id': 'fakesnapid', + 'share_instance': share_instance + } + + mock_rest = self.mock_object(as13000_nas.RestAPIExecutor, + 'send_rest_api') + + self.driver._clone_directory_to_dest(snapshot_instance_pseudo, + 'fakepath') + + method = 'snapshot/directory/clone' + request_type = 'post' + params = {'path': '/P/share_fakeinstanceid', + 'snapName': 'snap_fakesnapid', + 'destPath': 'fakepath'} + mock_rest.assert_called_once_with(method=method, + request_type=request_type, + params=params) + + def test__get_snapshots_from_share(self): + mock_rest = self.mock_object(as13000_nas.RestAPIExecutor, + 'send_rest_api', + mock.Mock(return_value=['fakesnap'])) + + result = self.driver._get_snapshots_from_share('/fakepath') + + self.assertEqual(['fakesnap'], result) + method = 'snapshot/directory?path=/fakepath' + request_type = 'get' + mock_rest.assert_called_once_with(method=method, + request_type=request_type) + + @ddt.data('nfs', 'cifs') + def test__get_location_path(self, proto): + self.driver.ips = ['ip1', 'ip2'] + + result = self.driver._get_location_path('fake_name', + '/fake/path', + proto) + if proto is 'nfs': + expect = [{'path': 'ip1:/fake/path'}, + {'path': 'ip2:/fake/path'}] + else: + expect = [{'path': r'\\ip1\fake_name'}, + {'path': r'\\ip2\fake_name'}] + self.assertEqual(expect, result) + + def test__get_nodes_virtual_ips(self): + ctdb_set = { + 'virtualIpList': [{'ip': 'fakeip1/24'}, + {'ip': 'fakeip2/24'}] + } + + mock_rest = self.mock_object(as13000_nas.RestAPIExecutor, + 'send_rest_api', + mock.Mock(return_value=ctdb_set)) + + result = self.driver._get_nodes_virtual_ips() + self.assertEqual(result, ['fakeip1', 'fakeip2']) + mock_rest.assert_called_once_with(method='ctdb/set', + request_type='get') + + def test__get_nodes_physical_ips(self): + nodes = [{'nodeIp': 'fakeip1', 'runningStatus': 1, 'healthStatus': 1}, + {'nodeIp': 'fakeip2', 'runningStatus': 1, 'healthStatus': 0}, + {'nodeIp': 'fakeip3', 'runningStatus': 0, 'healthStatus': 1}, + {'nodeIp': 'fakeip4', 'runningStatus': 0, 'healthStatus': 0}] + mock_rest = self.mock_object(as13000_nas.RestAPIExecutor, + 'send_rest_api', + mock.Mock(return_value=nodes)) + + result = self.driver._get_nodes_physical_ips() + + expect = ['fakeip1'] + self.assertEqual(expect, result) + mock_rest.assert_called_once_with(method='cluster/node/cache', + request_type='get') + + def test__get_nodes_ips(self): + mock_virtual = self.mock_object(self.driver, '_get_nodes_virtual_ips', + mock.Mock(return_value=['ip1'])) + mock_physical = self.mock_object(self.driver, + '_get_nodes_physical_ips', + mock.Mock(return_value=['ip2'])) + + result = self.driver._get_nodes_ips() + self.assertEqual(['ip1', 'ip2'], result) + mock_virtual.assert_called_once() + mock_physical.assert_called_once() + + @ddt.data('nfs', 'cifs') + def test__get_share_instance_pnsp(self, share_proto): + share = fake_share.fake_share(share_proto=share_proto) + share_instance = fake_share.fake_share_instance(share, host="H@B#P") + + result = self.driver._get_share_instance_pnsp(share_instance) + + self.assertEqual(('P', 'share_fakeinstanceid', 1, share_proto), result) + + @ddt.data('5000000000', '5000000k', '5000mb', '50G', '5TB') + def test__unit_convert(self, capacity): + trans = {'5000000000': '%.0f' % (float(5000000000) / 1024**3), + '5000000k': '%.0f' % (float(5000000) / 1024**2), + '5000mb': '%.0f' % (float(5000) / 1024), + '50G': '%.0f' % float(50), + '5TB': '%.0f' % (float(5) * 1024)} + expect = float(trans[capacity]) + result = self.driver._unit_convert(capacity) + self.assertEqual(expect, result) + + def test__format_name(self): + a = 'atest-1234567890-1234567890-1234567890' + expect = 'atest_1234567890_1234567890_1234' + result = self.driver._format_name(a) + self.assertEqual(expect, result) + + def test__generate_share_name(self): + share = fake_share.fake_share() + share_instance = fake_share.fake_share_instance(share, host="H@B#P") + + result = self.driver._generate_share_name(share_instance) + + self.assertEqual('share_fakeinstanceid', result) + + def test__generate_snapshot_name(self): + snapshot_instance_pesudo = {'id': 'fakesnapinstanceid'} + + result = self.driver._generate_snapshot_name(snapshot_instance_pesudo) + + self.assertEqual('snap_fakesnapinstanceid', result) + + def test__generate_share_path(self): + result = self.driver._generate_share_path('fakepool', 'fakename') + + self.assertEqual('/fakepool/fakename', result) + + def test__get_directory_detail(self): + details = [{'poolName': 'fakepool1'}, + {'poolName': 'fakepool2'}] + mock_rest = self.mock_object(as13000_nas.RestAPIExecutor, + 'send_rest_api', + mock.Mock(return_value=details)) + + result = self.driver._get_directory_detail('fakepath') + + self.assertEqual(details[0], result) + method = 'file/directory/detail?path=/fakepath' + request_type = 'get' + mock_rest.assert_called_once_with(method=method, + request_type=request_type) diff --git a/releasenotes/notes/inspur-as13000-driver-41f6b7caea82e46e.yaml b/releasenotes/notes/inspur-as13000-driver-41f6b7caea82e46e.yaml new file mode 100644 index 0000000000..9aa6d1a3a7 --- /dev/null +++ b/releasenotes/notes/inspur-as13000-driver-41f6b7caea82e46e.yaml @@ -0,0 +1,6 @@ +--- +prelude: > + Add Inspur AS13000 driver. +features: + - Added new Inspur AS13000 driver, which supports snapshots operation along with + all the minimum driver features.