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 e935e2503e..d8130f52fb 100644 --- a/doc/source/admin/share_back_ends_feature_support_mapping.rst +++ b/doc/source/admin/share_back_ends_feature_support_mapping.rst @@ -71,6 +71,8 @@ Mapping of share drivers and share features support +----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+ | INSPUR InStorage | T | \- | T | \- | \- | \- | \- | \- | \- | +----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+ +| Infortrend | T | T | T | T | \- | \- | \- | \- | \- | ++----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+ | LVM | M | \- | M | \- | M | M | \- | O | O | +----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+--------------------+--------------------+ | Quobyte | K | \- | M | M | \- | \- | \- | \- | \- | @@ -144,6 +146,8 @@ Mapping of share drivers and share access rules support +----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+ | INSPUR InStorage | NFS (T) | \- | CIFS (T) | \- | \- | NFS (T) | \- | CIFS (T) | \- | \- | +----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+ +| Infortrend | NFS (T) | \- | CIFS (T) | \- | \- | NFS (T) | \- | CIFS (T) | \- | \- | ++----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+ | Oracle ZFSSA | NFS,CIFS(K) | \- | \- | \- | \- | \- | \- | \- | \- | \- | +----------------------------------------+--------------+--------------+----------------+------------+--------------+--------------+--------------+----------------+------------+------------+ | CephFS | NFS (P) | NFS (T) | \- | \- | CEPHFS (M) | NFS (P) | NFS (T) | \- | \- | CEPHFS (N) | @@ -209,6 +213,8 @@ Mapping of share drivers and security services support +----------------------------------------+------------------+-----------------+------------------+ | INSPUR InStorage | \- | \- | \- | +----------------------------------------+------------------+-----------------+------------------+ +| Infortrend | \- | \- | \- | ++----------------------------------------+------------------+-----------------+------------------+ | Oracle ZFSSA | \- | \- | \- | +----------------------------------------+------------------+-----------------+------------------+ | CephFS | \- | \- | \- | @@ -264,6 +270,8 @@ More information: :ref:`capabilities_and_extra_specs` +----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+ | INFINIDAT | \- | Q | \- | \- | Q | Q | \- | Q | Q | Q | Q | \- | +----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+ +| Infortrend | \- | T | \- | \- | \- | \- | \- | \- | \- | \- | T | \- | ++----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+ | LVM | \- | M | \- | \- | \- | M | \- | K | O | O | P | P | +----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+----------------------------+--------------------+--------------------+--------------+--------------+ | Quobyte | \- | K | \- | \- | \- | L | \- | M | \- | \- | P | \- | diff --git a/manila/exception.py b/manila/exception.py index f8d1d39ded..f65d15a7f7 100644 --- a/manila/exception.py +++ b/manila/exception.py @@ -967,3 +967,13 @@ class LockingFailed(ManilaException): # Ganesha library class GaneshaException(ManilaException): message = _("Unknown NFS-Ganesha library exception.") + + +# Infortrend Storage driver +class InfortrendCLIException(ShareBackendException): + message = _("Infortrend CLI exception: %(err)s " + "Return Code: %(rc)s, Output: %(out)s") + + +class InfortrendNASException(ShareBackendException): + message = _("Infortrend NAS exception: %(err)s") diff --git a/manila/opts.py b/manila/opts.py index 04b2c44db0..2f2a06929f 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.infortrend.driver import manila.share.drivers.inspur.as13000.as13000_nas import manila.share.drivers.inspur.instorage.instorage import manila.share.drivers.lvm @@ -159,6 +160,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.infortrend.driver.infortrend_nas_opts, manila.share.drivers.inspur.as13000.as13000_nas.inspur_as13000_opts, manila.share.drivers.inspur.instorage.instorage.instorage_opts, manila.share.drivers.maprfs.maprfs_native.maprfs_native_share_opts, diff --git a/manila/share/drivers/infortrend/__init__.py b/manila/share/drivers/infortrend/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/share/drivers/infortrend/driver.py b/manila/share/drivers/infortrend/driver.py new file mode 100644 index 0000000000..064f31f6e2 --- /dev/null +++ b/manila/share/drivers/infortrend/driver.py @@ -0,0 +1,257 @@ +# Copyright (c) 2019 Infortrend Technology, 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. + +from oslo_config import cfg +from oslo_log import log + +from manila import exception +from manila.i18n import _ +from manila.share import driver +from manila.share.drivers.infortrend import infortrend_nas + +LOG = log.getLogger(__name__) + +infortrend_nas_opts = [ + cfg.HostAddressOpt('infortrend_nas_ip', + required=True, + help='Infortrend NAS IP for management.'), + cfg.StrOpt('infortrend_nas_user', + default='manila', + help='User for the Infortrend NAS server.'), + cfg.StrOpt('infortrend_nas_password', + default=None, + secret=True, + help='Password for the Infortrend NAS server. ' + 'This is not necessary ' + 'if infortrend_nas_ssh_key is set.'), + cfg.StrOpt('infortrend_nas_ssh_key', + default=None, + help='SSH key for the Infortrend NAS server. ' + 'This is not necessary ' + 'if infortrend_nas_password is set.'), + cfg.ListOpt('infortrend_share_pools', + required=True, + help='Comma separated list of Infortrend NAS pools.'), + cfg.ListOpt('infortrend_share_channels', + required=True, + help='Comma separated list of Infortrend channels.'), + cfg.IntOpt('infortrend_ssh_timeout', + default=30, + help='SSH timeout in seconds.'), +] + +CONF = cfg.CONF +CONF.register_opts(infortrend_nas_opts) + + +class InfortrendNASDriver(driver.ShareDriver): + + """Infortrend Share Driver for GS/GSe Family using NASCLI. + + Version history: + 1.0.0 - Initial driver + """ + + VERSION = "1.0.0" + PROTOCOL = "NFS_CIFS" + + def __init__(self, *args, **kwargs): + super(InfortrendNASDriver, self).__init__(False, *args, **kwargs) + self.configuration.append_config_values(infortrend_nas_opts) + + nas_ip = self.configuration.safe_get('infortrend_nas_ip') + username = self.configuration.safe_get('infortrend_nas_user') + password = self.configuration.safe_get('infortrend_nas_password') + ssh_key = self.configuration.safe_get('infortrend_nas_ssh_key') + timeout = self.configuration.safe_get('infortrend_ssh_timeout') + self.backend_name = self.configuration.safe_get('share_backend_name') + + if not (password or ssh_key): + msg = _('Either infortrend_nas_password or infortrend_nas_ssh_key ' + 'should be set.') + raise exception.InvalidParameterValue(err=msg) + + pool_dict = self._init_pool_dict() + channel_dict = self._init_channel_dict() + self.ift_nas = infortrend_nas.InfortrendNAS(nas_ip, username, password, + ssh_key, timeout, + pool_dict, channel_dict) + + def _init_pool_dict(self): + pools_names = self.configuration.safe_get('infortrend_share_pools') + + return {el: {} for el in pools_names} + + def _init_channel_dict(self): + channels = self.configuration.safe_get('infortrend_share_channels') + + return {el: '' for el in channels} + + def do_setup(self, context): + """Any initialization the share driver does while starting.""" + LOG.debug('Infortrend NAS do_setup start.') + self.ift_nas.do_setup() + + def check_for_setup_error(self): + """Check for setup error.""" + LOG.debug('Infortrend NAS check_for_setup_error start.') + self.ift_nas.check_for_setup_error() + + def _update_share_stats(self): + """Retrieve stats info from share group.""" + + LOG.debug('Updating Infortrend backend [%s].', self.backend_name) + + data = dict( + share_backend_name=self.backend_name, + vendor_name='Infortrend', + driver_version=self.VERSION, + storage_protocol=self.PROTOCOL, + reserved_percentage=self.configuration.reserved_share_percentage, + pools=self.ift_nas.update_pools_stats()) + LOG.debug('Infortrend pools status: %s', data['pools']) + + super(InfortrendNASDriver, self)._update_share_stats(data) + + def update_access(self, context, share, access_rules, add_rules, + delete_rules, share_server=None): + """Update access rules for given share. + + :param context: Current context + :param share: Share model with share data. + :param access_rules: All access rules for given share + :param add_rules: Empty List or List of access rules which should be + added. access_rules already contains these rules. + :param delete_rules: Empty List or List of access rules which should be + removed. access_rules doesn't contain these rules. + :param share_server: Not used by this driver. + + :returns: None, or a dictionary of ``access_id``, ``access_key`` as + key: value pairs for the rules added, where, ``access_id`` + is the UUID (string) of the access rule, and ``access_key`` + is the credential (string) of the entity granted access. + During recovery after error, the returned dictionary must + contain ``access_id``, ``access_key`` for all the rules that + the driver is ordered to resync, i.e. rules in the + ``access_rules`` parameter. + """ + + return self.ift_nas.update_access(share, access_rules, add_rules, + delete_rules, share_server) + + def create_share(self, context, share, share_server=None): + """Create a share.""" + + LOG.debug('Creating share: %s.', share['id']) + + return self.ift_nas.create_share(share, share_server) + + def delete_share(self, context, share, share_server=None): + """Remove a share.""" + + LOG.debug('Deleting share: %s.', share['id']) + + return self.ift_nas.delete_share(share, share_server) + + def get_pool(self, share): + """Return pool name where the share resides on. + + :param share: The share hosted by the driver. + """ + return self.ift_nas.get_pool(share) + + def ensure_share(self, context, share, share_server=None): + """Invoked to ensure that share is exported. + + Driver can use this method to update the list of export locations of + the share if it changes. To do that, you should return list with + export locations. + + :return None or list with export locations + """ + return self.ift_nas.ensure_share(share, share_server) + + 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 existing for share: %(share)s,', { + 'share': share['share_id'], + }) + return self.ift_nas.manage_existing(share, driver_options) + + def unmanage(self, share): + """Removes the specified share from Manila management. + + Does not delete the underlying backend share. + + For most drivers, this will not need to do anything. However, some + drivers might use this call as an opportunity to clean up any + Manila-specific configuration that they have associated with the + backend share. + + If provided share cannot be unmanaged, then raise an + UnmanageInvalidShare exception, specifying a reason for the failure. + + This method is invoked when the share is being unmanaged with + a share type that has ``driver_handles_share_servers`` + extra-spec set to False. + """ + LOG.debug( + 'Unmanage share: %(share)s', { + 'share': share['share_id'], + }) + return self.ift_nas.unmanage(share) + + def extend_share(self, share, new_size, share_server=None): + """Extends size of existing share. + + :param share: Share model + :param new_size: New size of share (new_size > share['size']) + :param share_server: Optional -- Share server model + """ + return self.ift_nas.extend_share(share, new_size, share_server) + + def shrink_share(self, share, new_size, share_server=None): + """Shrinks size of existing share. + + If consumed space on share larger than new_size driver should raise + ShareShrinkingPossibleDataLoss exception: + raise ShareShrinkingPossibleDataLoss(share_id=share['id']) + + :param share: Share model + :param new_size: New size of share (new_size < share['size']) + :param share_server: Optional -- Share server model + + :raises ShareShrinkingPossibleDataLoss, NotImplementedError + """ + return self.ift_nas.shrink_share(share, new_size, share_server) diff --git a/manila/share/drivers/infortrend/infortrend_nas.py b/manila/share/drivers/infortrend/infortrend_nas.py new file mode 100644 index 0000000000..1167286447 --- /dev/null +++ b/manila/share/drivers/infortrend/infortrend_nas.py @@ -0,0 +1,642 @@ +# Copyright (c) 2019 Infortrend Technology, 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. + +import json +import re + +from oslo_concurrency import processutils +from oslo_log import log +from oslo_utils import units + +from manila.common import constants +from manila import exception +from manila.i18n import _ +from manila.share import utils as share_utils +from manila import utils as manila_utils + +LOG = log.getLogger(__name__) + + +def _bi_to_gi(bi_size): + return bi_size / units.Gi + + +class InfortrendNAS(object): + + _SSH_PORT = 22 + + def __init__(self, nas_ip, username, password, ssh_key, + timeout, pool_dict, channel_dict): + self.nas_ip = nas_ip + self.port = self._SSH_PORT + self.username = username + self.password = password + self.ssh_key = ssh_key + self.ssh_timeout = timeout + self.pool_dict = pool_dict + self.channel_dict = channel_dict + self.command = "" + self.ssh = None + self.sshpool = None + self.location = 'a@0' + + def _execute(self, command_line): + command_line.extend(['-z', self.location]) + commands = ' '.join(command_line) + manila_utils.check_ssh_injection(commands) + LOG.debug('Executing: %(command)s', {'command': commands}) + + cli_out = self._ssh_execute(commands) + + return self._parser(cli_out) + + def _ssh_execute(self, commands): + try: + out, err = processutils.ssh_execute( + self.ssh, commands, + timeout=self.ssh_timeout, check_exit_code=True) + except processutils.ProcessExecutionError as pe: + rc = pe.exit_code + out = pe.stdout + out = out.replace('\n', '\\n') + msg = _('Error on execute ssh command. ' + 'Exit code: %(rc)d, msg: %(out)s') % { + 'rc': rc, 'out': out} + raise exception.InfortrendNASException(err=msg) + + return out + + def _parser(self, content=None): + LOG.debug('parsing data:\n%s', content) + content = content.replace("\r", "") + content = content.strip() + json_string = content.replace("'", "\"") + cli_data = json_string.splitlines()[2] + if cli_data: + try: + data_dict = json.loads(cli_data) + except Exception: + msg = _('Failed to parse data: ' + '%(cli_data)s to dictionary.') % { + 'cli_data': cli_data} + LOG.error(msg) + raise exception.InfortrendNASException(err=msg) + + rc = int(data_dict['cliCode'][0]['Return'], 16) + if rc == 0: + result = data_dict['data'] + else: + result = data_dict['cliCode'][0]['CLI'] + else: + msg = _('No data is returned from NAS.') + LOG.error(msg) + raise exception.InfortrendNASException(err=msg) + + if rc != 0: + msg = _('NASCLI error, returned: %(result)s.') % { + 'result': result} + LOG.error(msg) + raise exception.InfortrendCLIException( + err=msg, rc=rc, out=result) + + return rc, result + + def do_setup(self): + self._init_connect() + self._ensure_service_on('nfs') + self._ensure_service_on('cifs') + + def _init_connect(self): + if not (self.sshpool and self.ssh): + self.sshpool = manila_utils.SSHPool(ip=self.nas_ip, + port=self.port, + conn_timeout=None, + login=self.username, + password=self.password, + privatekey=self.ssh_key) + self.ssh = self.sshpool.create() + + if not self.ssh.get_transport().is_active(): + self.sshpool = manila_utils.SSHPool(ip=self.nas_ip, + port=self.port, + conn_timeout=None, + login=self.username, + password=self.password, + privatekey=self.ssh_key) + self.ssh = self.sshpool.create() + + LOG.debug('NAScmd [%s@%s] start!', self.username, self.nas_ip) + + def check_for_setup_error(self): + self._check_pools_setup() + self._check_channels_status() + + def _ensure_service_on(self, proto, slot='A'): + command_line = ['service', 'status', proto] + rc, service_status = self._execute(command_line) + if not service_status[0][slot][proto.upper()]['enabled']: + command_line = ['service', 'restart', proto] + self._execute(command_line) + + def _check_channels_status(self): + channel_list = list(self.channel_dict.keys()) + command_line = ['ifconfig', 'inet', 'show'] + rc, channels_status = self._execute(command_line) + for channel in channels_status: + if 'CH' in channel['datalink']: + ch = channel['datalink'].strip('CH') + if ch in self.channel_dict.keys(): + self.channel_dict[ch] = channel['IP'] + channel_list.remove(ch) + if channel['status'] == 'DOWN': + LOG.warning('Channel [%(ch)s] status ' + 'is down, please check.', { + 'ch': ch}) + if len(channel_list) != 0: + msg = _('Channel setting %(channel_list)s is invalid!') % { + 'channel_list': channel_list} + LOG.error(msg) + raise exception.InfortrendNASException(message=msg) + + def _check_pools_setup(self): + pool_list = list(self.pool_dict.keys()) + command_line = ['folder', 'status'] + rc, pool_data = self._execute(command_line) + for pool in pool_data: + pool_name = self._extract_pool_name(pool) + if pool_name in self.pool_dict.keys(): + pool_list.remove(pool_name) + self.pool_dict[pool_name]['id'] = pool['volumeId'] + self.pool_dict[pool_name]['path'] = pool['directory'] + '/' + if len(pool_list) == 0: + break + + if len(pool_list) != 0: + msg = _('Please create %(pool_list)s pool/s in advance!') % { + 'pool_list': pool_list} + LOG.error(msg) + raise exception.InfortrendNASException(message=msg) + + def _extract_pool_name(self, pool_info): + return pool_info['directory'].split('/')[1] + + def _extract_lv_name(self, pool_info): + return pool_info['path'].split('/')[2] + + def update_pools_stats(self): + pools = [] + command_line = ['folder', 'status'] + rc, pools_data = self._execute(command_line) + + for pool_info in pools_data: + pool_name = self._extract_pool_name(pool_info) + + if pool_name in self.pool_dict.keys(): + total_space = float(pool_info['size']) + pool_quota_used = self._get_pool_quota_used(pool_name) + available_space = total_space - pool_quota_used + + total_capacity_gb = round(_bi_to_gi(total_space), 2) + free_capacity_gb = round(_bi_to_gi(available_space), 2) + + pool = { + 'pool_name': pool_name, + 'total_capacity_gb': total_capacity_gb, + 'free_capacity_gb': free_capacity_gb, + 'reserved_percentage': 0, + 'qos': False, + 'dedupe': False, + 'compression': False, + 'snapshot_support': False, + 'thin_provisioning': False, + 'thick_provisioning': True, + 'replication_type': None, + } + pools.append(pool) + + return pools + + def _get_pool_quota_used(self, pool_name): + pool_quota_used = 0.0 + pool_data = self._get_share_pool_data(pool_name) + folder_name = self._extract_lv_name(pool_data) + + command_line = ['fquota', 'status', pool_data['id'], + folder_name, '-t', 'folder'] + rc, quota_status = self._execute(command_line) + + for share_quota in quota_status: + pool_quota_used += int(share_quota['quota']) + + return pool_quota_used + + def _get_share_pool_data(self, pool_name): + if not pool_name: + msg = _("Pool is not available in the share host.") + raise exception.InvalidHost(reason=msg) + + if pool_name in self.pool_dict.keys(): + return self.pool_dict[pool_name] + else: + msg = _('Pool [%(pool_name)s] not set in conf.') % { + 'pool_name': pool_name} + LOG.error(msg) + raise exception.InfortrendNASException(err=msg) + + def create_share(self, share, share_server=None): + pool_name = share_utils.extract_host(share['host'], level='pool') + pool_data = self._get_share_pool_data(pool_name) + folder_name = self._extract_lv_name(pool_data) + share_proto = share['share_proto'].lower() + share_name = share['id'].replace('-', '') + share_path = pool_data['path'] + share_name + + command_line = ['folder', 'options', pool_data['id'], + folder_name, '-c', share_name] + self._execute(command_line) + + self._set_share_size( + pool_data['id'], pool_name, share_name, share['size']) + self._ensure_protocol_on(share_path, share_proto, share_name) + + LOG.info('Create Share [%(share)s] completed.', { + 'share': share['id']}) + + return self._export_location( + share_name, share_proto, pool_data['path']) + + def _export_location(self, share_name, share_proto, pool_path=None): + location = [] + location_data = { + 'pool_path': pool_path, + 'share_name': share_name, + } + self._check_channels_status() + for ch in sorted(self.channel_dict.keys()): + ip = self.channel_dict[ch] + if share_proto == 'nfs': + location.append( + ip + ':%(pool_path)s%(share_name)s' % location_data) + elif share_proto == 'cifs': + location.append( + '\\\\' + ip + '\\%(share_name)s' % location_data) + else: + msg = _('Unsupported protocol: [%s].') % share_proto + raise exception.InvalidInput(msg) + + return location + + def _set_share_size(self, pool_id, pool_name, share_name, share_size): + pool_data = self._get_share_pool_data(pool_name) + folder_name = self._extract_lv_name(pool_data) + command_line = ['fquota', 'create', pool_id, folder_name, + share_name, str(share_size) + 'G', '-t', 'folder'] + self._execute(command_line) + + LOG.debug('Set Share [%(share_name)s] ' + 'Size [%(share_size)s G] completed.', { + 'share_name': share_name, + 'share_size': share_size}) + return + + def _get_share_size(self, pool_id, pool_name, share_name): + share_size = None + command_line = ['fquota', 'status', pool_id, + share_name, '-t', 'folder'] + rc, quota_status = self._execute(command_line) + + for share_quota in quota_status: + if share_quota['name'] == share_name: + share_size = round(_bi_to_gi(float(share_quota['quota'])), 2) + break + + return share_size + + def delete_share(self, share, share_server=None): + pool_name = share_utils.extract_host(share['host'], level='pool') + pool_data = self._get_share_pool_data(pool_name) + folder_name = self._extract_lv_name(pool_data) + share_name = share['id'].replace('-', '') + + if self._check_share_exist(pool_name, share_name): + command_line = ['folder', 'options', pool_data['id'], + folder_name, '-d', share_name] + self._execute(command_line) + else: + LOG.warning('Share [%(share_name)s] is already deleted.', { + 'share_name': share_name}) + + LOG.info('Delete Share [%(share)s] completed.', { + 'share': share['id']}) + + def _check_share_exist(self, pool_name, share_name): + path = self.pool_dict[pool_name]['path'] + command_line = ['pagelist', 'folder', path] + rc, subfolders = self._execute(command_line) + return any(subfolder['name'] == share_name for subfolder in subfolders) + + def update_access(self, share, access_rules, add_rules, + delete_rules, share_server=None): + self._evict_unauthorized_clients(share, access_rules, share_server) + access_dict = {} + for access in access_rules: + try: + self._allow_access(share, access, share_server) + except (exception.InfortrendNASException) as e: + msg = _('Failed to allow access to client %(access)s, ' + 'reason %(e)s.') % { + 'access': access['access_to'], 'e': e} + LOG.error(msg) + access_dict[access['id']] = 'error' + + return access_dict + + def _evict_unauthorized_clients(self, share, access_rules, + share_server=None): + pool_name = share_utils.extract_host(share['host'], level='pool') + pool_data = self._get_share_pool_data(pool_name) + share_proto = share['share_proto'].lower() + share_name = share['id'].replace('-', '') + share_path = pool_data['path'] + share_name + + access_list = [] + for access in access_rules: + access_list.append(access['access_to']) + + if share_proto == 'nfs': + host_ip_list = [] + command_line = ['share', 'status', '-f', share_path] + rc, nfs_status = self._execute(command_line) + host_list = nfs_status[0]['nfs_detail']['hostList'] + for host in host_list: + if host['host'] != '*': + host_ip_list.append(host['host']) + for ip in host_ip_list: + if ip not in access_list: + command_line = ['share', 'options', share_path, + 'nfs', '-c', ip] + try: + self._execute(command_line) + except exception.InfortrendNASException: + msg = _("Failed to remove share access rule %s") % (ip) + LOG.exception(msg) + pass + + elif share_proto == 'cifs': + host_user_list = [] + command_line = ['acl', 'get', share_path] + rc, cifs_status = self._execute(command_line) + for cifs_rule in cifs_status: + if cifs_rule['name']: + host_user_list.append(cifs_rule['name']) + for user in host_user_list: + if user not in access_list: + command_line = ['acl', 'delete', share_path, '-u', user] + try: + self._execute(command_line) + except exception.InfortrendNASException: + msg = _("Failed to remove share access rule %s") % ( + user) + LOG.exception(msg) + pass + + def _allow_access(self, share, access, share_server=None): + pool_name = share_utils.extract_host(share['host'], level='pool') + pool_data = self._get_share_pool_data(pool_name) + share_name = share['id'].replace('-', '') + share_path = pool_data['path'] + share_name + share_proto = share['share_proto'].lower() + access_type = access['access_type'] + access_level = access['access_level'] or constants.ACCESS_LEVEL_RW + access_to = access['access_to'] + ACCESS_LEVEL_MAP = {access_level: access_level} + msg = self._check_access_legal(share_proto, access_type) + if msg: + raise exception.InvalidShareAccess(reason=msg) + + if share_proto == 'nfs': + command_line = ['share', 'options', share_path, 'nfs', + '-h', access_to, '-p', access_level] + self._execute(command_line) + + elif share_proto == 'cifs': + if not self._check_user_exist(access_to): + msg = _('Please create user [%(user)s] in advance.') % { + 'user': access_to} + LOG.error(msg) + raise exception.InfortrendNASException(err=msg) + + if access_level == constants.ACCESS_LEVEL_RW: + cifs_access = 'f' + elif access_level == constants.ACCESS_LEVEL_RO: + cifs_access = 'r' + try: + access_level = ACCESS_LEVEL_MAP[access_level] + except KeyError: + msg = _('Unsupported access_level: [%s].') % access_level + raise exception.InvalidInput(msg) + + command_line = ['acl', 'set', share_path, + '-u', access_to, '-a', cifs_access] + self._execute(command_line) + + LOG.info('Share [%(share)s] access to [%(access_to)s] ' + 'level [%(level)s] protocol [%(share_proto)s] completed.', { + 'share': share['id'], + 'access_to': access_to, + 'level': access_level, + 'share_proto': share_proto}) + + def _ensure_protocol_on(self, share_path, share_proto, cifs_name): + if not self._check_proto_enabled(share_path, share_proto): + command_line = ['share', share_path, share_proto, 'on'] + if share_proto == 'cifs': + command_line.extend(['-n', cifs_name]) + self._execute(command_line) + + def _check_proto_enabled(self, share_path, share_proto): + command_line = ['share', 'status', '-f', share_path] + rc, share_status = self._execute(command_line) + if share_status: + check_enabled = share_status[0][share_proto] + if check_enabled: + return True + return False + + def _check_user_exist(self, user_name): + command_line = ['useradmin', 'user', 'list'] + rc, user_list = self._execute(command_line) + for user in user_list: + if user['Name'] == user_name: + return True + return False + + def _check_access_legal(self, share_proto, access_type): + msg = None + if share_proto == 'cifs' and access_type != 'user': + msg = _('Infortrend CIFS share only supports USER access type.') + elif share_proto == 'nfs' and access_type != 'ip': + msg = _('Infortrend NFS share only supports IP access type.') + elif share_proto not in ('nfs', 'cifs'): + msg = _('Unsupported share protocol [%s].') % share_proto + return msg + + def get_pool(self, share): + pool_name = share_utils.extract_host(share['host'], level='pool') + if not pool_name: + share_name = share['id'].replace('-', '') + for pool in self.pool_dict.keys(): + if self._check_share_exist(pool, share_name): + pool_name = pool + break + return pool_name + + def ensure_share(self, share, share_server=None): + share_proto = share['share_proto'].lower() + pool_name = share_utils.extract_host(share['host'], level='pool') + pool_data = self._get_share_pool_data(pool_name) + share_name = share['id'].replace('-', '') + return self._export_location( + share_name, share_proto, pool_data['path']) + + def extend_share(self, share, new_size, share_server=None): + pool_name = share_utils.extract_host(share['host'], level='pool') + pool_data = self._get_share_pool_data(pool_name) + share_name = share['id'].replace('-', '') + self._set_share_size(pool_data['id'], pool_name, share_name, new_size) + + LOG.info('Successfully Extend Share [%(share)s] ' + 'to size [%(new_size)s G].', { + 'share': share['id'], + 'new_size': new_size}) + + def shrink_share(self, share, new_size, share_server=None): + pool_name = share_utils.extract_host(share['host'], level='pool') + pool_data = self._get_share_pool_data(pool_name) + share_name = share['id'].replace('-', '') + folder_name = self._extract_lv_name(pool_data) + + command_line = ['fquota', 'status', pool_data['id'], + folder_name, '-t', 'folder'] + rc, quota_status = self._execute(command_line) + + for share_quota in quota_status: + if share_quota['name'] == share_name: + used_space = round(_bi_to_gi(float(share_quota['used'])), 2) + + if new_size < used_space: + raise exception.ShareShrinkingPossibleDataLoss( + share_id=share['id']) + + self._set_share_size(pool_data['id'], pool_name, share_name, new_size) + + LOG.info('Successfully Shrink Share [%(share)s] ' + 'to size [%(new_size)s G].', { + 'share': share['id'], + 'new_size': new_size}) + + def manage_existing(self, share, driver_options): + share_proto = share['share_proto'].lower() + pool_name = share_utils.extract_host(share['host'], level='pool') + pool_data = self._get_share_pool_data(pool_name) + volume_name = self._extract_lv_name(pool_data) + input_location = share['export_locations'][0]['path'] + share_name = share['id'].replace('-', '') + + ch_ip, folder_name = self._parse_location(input_location, share_proto) + + if not self._check_channel_ip(ch_ip): + msg = _('Export location ip: [%(ch_ip)s] ' + 'is incorrect, please use data port ip.') % { + 'ch_ip': ch_ip} + LOG.error(msg) + raise exception.InfortrendNASException(err=msg) + + if not self._check_share_exist(pool_name, folder_name): + msg = _('Can not find folder [%(folder_name)s] ' + 'in pool [%(pool_name)s].') % { + 'folder_name': folder_name, + 'pool_name': pool_name} + LOG.error(msg) + raise exception.InfortrendNASException(err=msg) + + share_path = pool_data['path'] + folder_name + self._ensure_protocol_on(share_path, share_proto, share_name) + share_size = self._get_share_size( + pool_data['id'], pool_name, folder_name) + + if not share_size: + msg = _('Folder [%(folder_name)s] has no size limitation, ' + 'please set it first for Openstack management.') % { + 'folder_name': folder_name} + LOG.error(msg) + raise exception.InfortrendNASException(err=msg) + + # rename folder name + command_line = ['folder', 'options', pool_data['id'], volume_name, + '-k', folder_name, share_name] + self._execute(command_line) + + location = self._export_location( + share_name, share_proto, pool_data['path']) + + LOG.info('Successfully Manage Infortrend Share [%(folder_name)s], ' + 'Size: [%(size)s G], Protocol: [%(share_proto)s], ' + 'new name: [%(share_name)s].', { + 'folder_name': folder_name, + 'size': share_size, + 'share_proto': share_proto, + 'share_name': share_name}) + + return {'size': share_size, 'export_locations': location} + + def _parse_location(self, input_location, share_proto): + ip = None + folder_name = None + pattern_ip = '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' + if share_proto == 'nfs': + pattern_folder = '[^\/]+$' + ip = "".join(re.findall(pattern_ip, input_location)) + folder_name = "".join(re.findall(pattern_folder, input_location)) + + elif share_proto == 'cifs': + pattern_folder = '[^\\\]+$' + ip = "".join(re.findall(pattern_ip, input_location)) + folder_name = "".join(re.findall(pattern_folder, input_location)) + + if not (ip and folder_name): + msg = _('Export location error, please check ' + 'ip: [%(ip)s], folder_name: [%(folder_name)s].') % { + 'ip': ip, + 'folder_name': folder_name} + LOG.error(msg) + raise exception.InfortrendNASException(err=msg) + + return ip, folder_name + + def _check_channel_ip(self, channel_ip): + return any(ip == channel_ip for ip in self.channel_dict.values()) + + def unmanage(self, share): + pool_name = share_utils.extract_host(share['host'], level='pool') + share_name = share['id'].replace('-', '') + + if not self._check_share_exist(pool_name, share_name): + LOG.warning('Share [%(share_name)s] does not exist.', { + 'share_name': share_name}) + return + + LOG.info('Successfully Unmanaged Share [%(share)s].', { + 'share': share['id']}) diff --git a/manila/tests/conf_fixture.py b/manila/tests/conf_fixture.py index 2518f133e9..28a1325f25 100644 --- a/manila/tests/conf_fixture.py +++ b/manila/tests/conf_fixture.py @@ -65,6 +65,10 @@ def set_defaults(conf): _safe_set_of_opts(conf, 'instorage_nas_password', 'password') _safe_set_of_opts(conf, 'instorage_nas_pools', 'pool0') + _safe_set_of_opts(conf, 'infortrend_nas_ip', '172.27.1.1') + _safe_set_of_opts(conf, 'infortrend_share_pools', 'share-pool-01') + _safe_set_of_opts(conf, 'infortrend_share_channels', '0,1') + _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/infortrend/__init__.py b/manila/tests/share/drivers/infortrend/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/tests/share/drivers/infortrend/fake_infortrend_manila_data.py b/manila/tests/share/drivers/infortrend/fake_infortrend_manila_data.py new file mode 100644 index 0000000000..c807b5f4d7 --- /dev/null +++ b/manila/tests/share/drivers/infortrend/fake_infortrend_manila_data.py @@ -0,0 +1,408 @@ +# Copyright (c) 2019 Infortrend Technology, 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. + + +class InfortrendManilaTestData(object): + + fake_share_id = ['4d6984fd-8572-4467-964f-24936a8c4ea2', # NFS + 'a7b933e6-bb77-4823-a86f-f2c3ab41a8a5'] # CIFS + + fake_id = ['iftt8862-2226-0126-7610-chengweichou', + '987c8763-3333-4444-5555-666666666666'] + + fake_share_nfs = { + 'share_id': fake_share_id[0], + 'availability_zone': 'nova', + 'terminated_at': 'datetime.datetime(2017, 5, 8, 8, 27, 25)', + 'availability_zone_id': 'fd32d76d-b5a8-4c5c-93d7-8f09fc2a8ad3', + 'updated_at': 'datetime.datetime(2017, 5, 8, 8, 27, 25)', + 'share_network_id': None, + 'export_locations': [], + 'share_server_id': None, + 'snapshot_id': None, + 'deleted_at': None, + 'id': '5a0aa06e-1c57-4996-be46-b81e360e8866', + 'size': 30, + 'replica_state': None, + 'user_id': '4944594433f0405588928a4212964658', + 'export_location': '172.27.112.223:/share-pool-01/LV-1/' + + fake_share_id[0], + 'display_description': None, + 'consistency_group_id': None, + 'project_id': '0e63326c50a246ac81fa1a0c8e003d5b', + 'launched_at': 'datetime.datetime(2017, 5, 8, 8, 23, 33)', + 'scheduled_at': 'datetime.datetime(2017, 5, 8, 8, 23, 29)', + 'status': 'deleting', + 'share_type_id': '23d8c637-0192-47fa-b921-958f22ed772f', + 'deleted': 'False', + 'host': 'compute@ift-manila#share-pool-01', + 'access_rules_status': 'active', + 'display_name': 'nfs-01', + 'name': 'share-5a0aa06e-1c57-4996-be46-b81e360e8866', + 'created_at': 'datetime.datetime(2017, 5, 8, 8, 23, 29)', + 'share_proto': 'NFS', + 'is_public': False, + 'source_cgsnapshot_member_id': None + } + + fake_share_cifs = { + 'share_id': fake_share_id[1], + 'availability_zone': 'nova', + 'terminated_at': None, + 'availability_zone_id': 'fd32d76d-b5a8-4c5c-93d7-8f09fc2a8ad3', + 'updated_at': 'datetime.datetime(2017, 5, 9, 2, 28, 35)', + 'share_network_id': None, + 'export_locations': [], + 'share_server_id': None, + 'snapshot_id': None, + 'deleted_at': None, + 'id': 'aac4fe64-7a9c-472a-b156-9adbb50b4d29', + 'size': 50, + 'replica_state': None, + 'user_id': '4944594433f0405588928a4212964658', + 'export_location': None, + 'display_description': None, + 'consistency_group_id': None, + 'project_id': '0e63326c50a246ac81fa1a0c8e003d5b', + 'launched_at': None, + 'scheduled_at': 'datetime.datetime(2017, 5, 9, 2, 28, 35)', + 'status': 'creating', + 'share_type_id': '23d8c637-0192-47fa-b921-958f22ed772f', + 'deleted': 'False', + 'host': 'compute@ift-manila#share-pool-01', + 'access_rules_status': 'active', + 'display_name': 'cifs-01', + 'name': 'share-aac4fe64-7a9c-472a-b156-9adbb50b4d29', + 'created_at': 'datetime.datetime(2017, 5, 9, 2, 28, 35)', + 'share_proto': 'CIFS', + 'is_public': False, + 'source_cgsnapshot_member_id': None + } + + fake_share_cifs_no_host = { + 'share_id': fake_share_id[1], + 'availability_zone': 'nova', + 'terminated_at': None, + 'availability_zone_id': 'fd32d76d-b5a8-4c5c-93d7-8f09fc2a8ad3', + 'updated_at': 'datetime.datetime(2017, 5, 9, 2, 28, 35)', + 'share_network_id': None, + 'export_locations': [], + 'share_server_id': None, + 'snapshot_id': None, + 'deleted_at': None, + 'id': 'aac4fe64-7a9c-472a-b156-9adbb50b4d29', + 'size': 50, + 'replica_state': None, + 'user_id': '4944594433f0405588928a4212964658', + 'export_location': None, + 'display_description': None, + 'consistency_group_id': None, + 'project_id': '0e63326c50a246ac81fa1a0c8e003d5b', + 'launched_at': None, + 'scheduled_at': 'datetime.datetime(2017, 5, 9, 2, 28, 35)', + 'status': 'creating', + 'share_type_id': '23d8c637-0192-47fa-b921-958f22ed772f', + 'deleted': 'False', + 'host': '', + 'access_rules_status': 'active', + 'display_name': 'cifs-01', + 'name': 'share-aac4fe64-7a9c-472a-b156-9adbb50b4d29', + 'created_at': 'datetime.datetime(2017, 5, 9, 2, 28, 35)', + 'share_proto': 'CIFS', + 'is_public': False, + 'source_cgsnapshot_member_id': None + } + + fake_non_exist_share = { + 'share_id': fake_id[0], + 'availability_zone': 'nova', + 'terminated_at': 'datetime.datetime(2017, 5, 8, 8, 27, 25)', + 'availability_zone_id': 'fd32d76d-b5a8-4c5c-93d7-8f09fc2a8ad3', + 'updated_at': 'datetime.datetime(2017, 5, 8, 8, 27, 25)', + 'share_network_id': None, + 'export_locations': [], + 'share_server_id': None, + 'snapshot_id': None, + 'deleted_at': None, + 'id': fake_id[1], + 'size': 30, + 'replica_state': None, + 'user_id': '4944594433f0405588928a4212964658', + 'export_location': '172.27.112.223:/share-pool-01/LV-1/' + + fake_id[0], + 'display_description': None, + 'consistency_group_id': None, + 'project_id': '0e63326c50a246ac81fa1a0c8e003d5b', + 'launched_at': 'datetime.datetime(2017, 5, 8, 8, 23, 33)', + 'scheduled_at': 'datetime.datetime(2017, 5, 8, 8, 23, 29)', + 'status': 'available', + 'share_type_id': '23d8c637-0192-47fa-b921-958f22ed772f', + 'deleted': 'False', + 'host': 'compute@ift-manila#share-pool-01', + 'access_rules_status': 'active', + 'display_name': 'nfs-01', + 'name': 'share-5a0aa06e-1c57-4996-be46-b81e360e8866', + 'created_at': 'datetime.datetime(2017, 5, 8, 8, 23, 29)', + 'share_proto': 'NFS', + 'is_public': False, + 'source_cgsnapshot_member_id': None + } + + fake_access_rules_nfs = [{ + 'share_id': fake_share_id[0], + 'deleted': 'False', + 'created_at': 'datetime.datetime(2017, 5, 9, 8, 41, 21)', + 'updated_at': None, + 'access_type': 'ip', + 'access_to': '172.27.1.1', + 'access_level': 'rw', + 'instance_mappings': [], + 'deleted_at': None, + 'id': 'fa60b50f-1428-44a2-9931-7e31f0c5b033'}, { + 'share_id': fake_share_id[0], + 'deleted': 'False', + 'created_at': 'datetime.datetime(2017, 5, 9, 8, 45, 37)', + 'updated_at': None, + 'access_type': 'ip', + 'access_to': '172.27.1.2', + 'access_level': 'rw', + 'instance_mappings': [], + 'deleted_at': None, + 'id': '9bcdd5e6-11c7-4f8f-939c-84fa2f3334bc' + }] + + fake_rule_ip_1 = [{ + 'share_id': fake_share_id[0], + 'deleted': 'False', + 'created_at': 'datetime.datetime(2017, 5, 9, 8, 41, 21)', + 'updated_at': None, + 'access_type': 'ip', + 'access_to': '172.27.1.1', + 'access_level': 'rw', + 'instance_mappings': [], + 'deleted_at': None, + 'id': 'fa60b50f-1428-44a2-9931-7e31f0c5b033' + }] + + fake_rule_ip_2 = [{ + 'share_id': fake_share_id[0], + 'deleted': 'False', + 'created_at': 'datetime.datetime(2017, 5, 9, 8, 45, 37)', + 'updated_at': None, + 'access_type': 'ip', + 'access_to': '172.27.1.2', + 'access_level': 'rw', + 'instance_mappings': [], + 'deleted_at': None, + 'id': '9bcdd5e6-11c7-4f8f-939c-84fa2f3334bc' + }] + + fake_access_rules_cifs = [{ + 'share_id': fake_share_id[1], + 'deleted': 'False', + 'created_at': 'datetime.datetime(2017, 5, 9, 9, 39, 18)', + 'updated_at': None, + 'access_type': 'user', + 'access_to': 'user02', + 'access_level': 'ro', + 'instance_mappings': [], + 'deleted_at': None, + 'id': '6e8bc969-51c9-4bbb-8e8b-020dc5fec81e'}, { + 'share_id': fake_share_id[1], + 'deleted': 'False', + 'created_at': 'datetime.datetime(2017, 5, 9, 9, 38, 59)', + 'updated_at': None, + 'access_type': 'user', + 'access_to': 'user01', + 'access_level': 'rw', + 'instance_mappings': [], + 'deleted_at': None, + 'id': '0cd9926d-fac4-4122-a523-538e98752e78' + }] + + fake_rule_user01 = [{ + 'share_id': fake_share_id[1], + 'deleted': 'False', + 'created_at': 'datetime.datetime(2017, 5, 9, 9, 38, 59)', + 'updated_at': None, + 'access_type': 'user', + 'access_to': 'user01', + 'access_level': 'rw', + 'instance_mappings': [], + 'deleted_at': None, + 'id': '0cd9926d-fac4-4122-a523-538e98752e78' + }] + + fake_rule_user02 = [{ + 'share_id': fake_share_id[1], + 'deleted': 'False', + 'created_at': 'datetime.datetime(2017, 5, 9, 9, 39, 18)', + 'updated_at': None, + 'access_type': 'user', + 'access_to': 'user02', + 'access_level': 'ro', + 'instance_mappings': [], + 'deleted_at': None, + 'id': '6e8bc969-51c9-4bbb-8e8b-020dc5fec81e' + }] + + fake_rule_user03 = [{ + 'share_id': fake_id[0], + 'deleted': 'False', + 'created_at': 'datetime.datetime(2017, 5, 9, 9, 39, 18)', + 'updated_at': None, + 'access_type': 'user', + 'access_to': 'user03', + 'access_level': 'rw', + 'instance_mappings': [], + 'deleted_at': None, + 'id': fake_id[1] + }] + + fake_share_for_manage_nfs = { + 'share_id': '419ab73c-c0fc-4e73-b56a-70756e0b6d27', + 'availability_zone': None, + 'terminated_at': None, + 'availability_zone_id': None, + 'updated_at': None, + 'share_network_id': None, + 'export_locations': [{ + 'uuid': '0ebd59e4-e65e-4fda-9457-320375efd0be', + 'deleted': 0, + 'created_at': 'datetime.datetime(2017, 5, 10, 10, 0, 3)', + 'updated_at': 'datetime.datetime(2017, 5, 10, 10, 0, 3)', + 'is_admin_only': False, + 'share_instance_id': 'd3cfe195-85cf-41e6-be4f-a96f7e7db192', + 'path': '172.27.112.223:/share-pool-01/LV-1/test-folder', + 'el_metadata': {}, + 'deleted_at': None, + 'id': 83 + }], + 'share_server_id': None, + 'snapshot_id': None, + 'deleted_at': None, + 'id': '615ac1ed-e808-40b5-8d7b-87018c6f66eb', + 'size': None, + 'replica_state': None, + 'user_id': '4944594433f0405588928a4212964658', + 'export_location': '172.27.112.223:/share-pool-01/LV-1/test-folder', + 'display_description': '', + 'consistency_group_id': None, + 'project_id': '0e63326c50a246ac81fa1a0c8e003d5b', + 'launched_at': None, + 'scheduled_at': 'datetime.datetime(2017, 5, 10, 9, 22, 5)', + 'status': 'manage_starting', + 'share_type_id': '23d8c637-0192-47fa-b921-958f22ed772f', + 'deleted': 'False', + 'host': 'compute@ift-manila#share-pool-01', + 'access_rules_status': 'active', + 'display_name': 'test-manage', + 'name': 'share-615ac1ed-e808-40b5-8d7b-87018c6f66eb', + 'created_at': 'datetime.datetime(2017, 5, 10, 9, 22, 5)', + 'share_proto': 'NFS', + 'is_public': False, + 'source_cgsnapshot_member_id': None + } + + def _get_fake_share_for_manage(self, location=''): + return { + 'share_id': '419ab73c-c0fc-4e73-b56a-70756e0b6d27', + 'availability_zone': None, + 'terminated_at': None, + 'availability_zone_id': None, + 'updated_at': None, + 'share_network_id': None, + 'export_locations': [{ + 'uuid': '0ebd59e4-e65e-4fda-9457-320375efd0be', + 'deleted': 0, + 'created_at': 'datetime.datetime(2017, 5, 10, 10, 0, 3)', + 'updated_at': 'datetime.datetime(2017, 5, 10, 10, 0, 3)', + 'is_admin_only': False, + 'share_instance_id': 'd3cfe195-85cf-41e6-be4f-a96f7e7db192', + 'path': location, + 'el_metadata': {}, + 'deleted_at': None, + 'id': 83 + }], + 'share_server_id': None, + 'snapshot_id': None, + 'deleted_at': None, + 'id': '615ac1ed-e808-40b5-8d7b-87018c6f66eb', + 'size': None, + 'replica_state': None, + 'user_id': '4944594433f0405588928a4212964658', + 'export_location': location, + 'display_description': '', + 'consistency_group_id': None, + 'project_id': '0e63326c50a246ac81fa1a0c8e003d5b', + 'launched_at': None, + 'scheduled_at': 'datetime.datetime(2017, 5, 10, 9, 22, 5)', + 'status': 'manage_starting', + 'share_type_id': '23d8c637-0192-47fa-b921-958f22ed772f', + 'deleted': 'False', + 'host': 'compute@ift-manila#share-pool-01', + 'access_rules_status': 'active', + 'display_name': 'test-manage', + 'name': 'share-615ac1ed-e808-40b5-8d7b-87018c6f66eb', + 'created_at': 'datetime.datetime(2017, 5, 10, 9, 22, 5)', + 'share_proto': 'NFS', + 'is_public': False, + 'source_cgsnapshot_member_id': None + } + + fake_share_for_manage_cifs = { + 'share_id': '3a1222d3-c981-490a-9390-4d560ced68eb', + 'availability_zone': None, + 'terminated_at': None, + 'availability_zone_id': None, + 'updated_at': None, + 'share_network_id': None, + 'export_locations': [{ + 'uuid': '0ebd59e4-e65e-4fda-9457-320375efd0de', + 'deleted': 0, + 'created_at': 'datetime.datetime(2017, 5, 11, 10, 10, 3)', + 'updated_at': 'datetime.datetime(2017, 5, 11, 10, 10, 3)', + 'is_admin_only': False, + 'share_instance_id': 'd3cfe195-85cf-41e6-be4f-a96f7e7db192', + 'path': '\\\\172.27.113.209\\test-folder-02', + 'el_metadata': {}, + 'deleted_at': None, + 'id': 87 + }], + 'share_server_id': None, + 'snapshot_id': None, + 'deleted_at': None, + 'id': 'd156baf7-5422-4c9b-8c78-ee7943d000ec', + 'size': None, + 'replica_state': None, + 'user_id': '4944594433f0405588928a4212964658', + 'export_location': '\\\\172.27.113.209\\test-folder-02', + 'display_description': '', + 'consistency_group_id': None, + 'project_id': '0e63326c50a246ac81fa1a0c8e003d5b', + 'launched_at': None, + 'scheduled_at': 'datetime.datetime(2017, 5, 11, 3, 7, 59)', + 'status': 'manage_starting', + 'share_type_id': '23d8c637-0192-47fa-b921-958f22ed772f', + 'deleted': 'False', + 'host': 'compute@ift-manila#share-pool-01', + 'access_rules_status': 'active', + 'display_name': 'test-manage-02', + 'name': 'share-d156baf7-5422-4c9b-8c78-ee7943d000ec', + 'created_at': 'datetime.datetime(2017, 5, 11, 3, 7, 59)', + 'share_proto': 'CIFS', + 'is_public': False, + 'source_cgsnapshot_member_id': None + } diff --git a/manila/tests/share/drivers/infortrend/fake_infortrend_nas_data.py b/manila/tests/share/drivers/infortrend/fake_infortrend_nas_data.py new file mode 100644 index 0000000000..b556f2a263 --- /dev/null +++ b/manila/tests/share/drivers/infortrend/fake_infortrend_nas_data.py @@ -0,0 +1,416 @@ +# Copyright (c) 2019 Infortrend Technology, 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. + + +class InfortrendNASTestData(object): + + fake_share_id = ['5a0aa06e-1c57-4996-be46-b81e360e8866', # NFS + 'aac4fe64-7a9c-472a-b156-9adbb50b4d29'] # CIFS + + fake_share_name = [fake_share_id[0].replace('-', ''), + fake_share_id[1].replace('-', '')] + + fake_channel_ip = ['172.27.112.223', '172.27.113.209'] + + fake_service_status_data = ('(64175, 1234, 272, 0)\n\n' + '{"cliCode": ' + '[{"Return": "0x0000", "CLI": "Successful"}], ' + '"returnCode": [], ' + '"data": ' + '[{"A": ' + '{"NFS": ' + '{"displayName": "NFS", ' + '"state_time": "2017-05-04 14:19:53", ' + '"enabled": true, ' + '"cpu_rate": "0.0", ' + '"mem_rate": "0.0", ' + '"state": "exited", ' + '"type": "share"}}}]}\n\n') + + fake_folder_status_data = ('(64175, 1234, 1017, 0)\n\n' + '{"cliCode": ' + '[{"Return": "0x0000", "CLI": "Successful"}], ' + '"returnCode": [], ' + '"data": ' + '[{"utility": "1.00", ' + '"used": "33886208", ' + '"subshare": true, ' + '"share": false, ' + '"worm": "", ' + '"free": "321931374592", ' + '"fsType": "xfs", ' + '"owner": "A", ' + '"readOnly": false, ' + '"modifyTime": "2017-04-27 16:16", ' + '"directory": "/share-pool-01/LV-1", ' + '"volumeId": "6541BAFB2E6C57B6", ' + '"mounted": true, ' + '"size": "321965260800"}, ' + '{"utility": "1.00", ' + '"used": "33779712", ' + '"subshare": false, ' + '"share": false, ' + '"worm": "", ' + '"free": "107287973888", ' + '"fsType": "xfs", ' + '"owner": "A", ' + '"readOnly": false, ' + '"modifyTime": "2017-04-27 15:45", ' + '"directory": "/share-pool-02/LV-1", ' + '"volumeId": "147A8FB67DA39914", ' + '"mounted": true, ' + '"size": "107321753600"}]}\n\n') + + fake_nfs_status_off = [{ + 'A': { + 'NFS': { + 'displayName': 'NFS', + 'state_time': '2017-05-04 14:19:53', + 'enabled': False, + 'cpu_rate': '0.0', + 'mem_rate': '0.0', + 'state': 'exited', + 'type': 'share', + } + } + }] + + fake_folder_status = [{ + 'utility': '1.00', + 'used': '33886208', + 'subshare': True, + 'share': False, + 'worm': '', + 'free': '321931374592', + 'fsType': 'xfs', + 'owner': 'A', + 'readOnly': False, + 'modifyTime': '2017-04-27 16:16', + 'directory': '/share-pool-01/LV-1', + 'volumeId': '6541BAFB2E6C57B6', + 'mounted': True, + 'size': '321965260800'}, { + 'utility': '1.00', + 'used': '33779712', + 'subshare': False, + 'share': False, + 'worm': '', + 'free': '107287973888', + 'fsType': 'xfs', + 'owner': 'A', + 'readOnly': False, + 'modifyTime': '2017-04-27 15:45', + 'directory': '/share-pool-02/LV-1', + 'volumeId': '147A8FB67DA39914', + 'mounted': True, + 'size': '107321753600', + }] + + def fake_get_channel_status(self, ch1_status='UP'): + return [{ + 'datalink': 'mgmt0', + 'status': 'UP', + 'typeConfig': 'DHCP', + 'IP': '172.27.112.125', + 'MAC': '00:d0:23:00:15:a6', + 'netmask': '255.255.240.0', + 'type': 'dhcp', + 'gateway': '172.27.127.254'}, { + 'datalink': 'CH0', + 'status': 'UP', + 'typeConfig': 'DHCP', + 'IP': self.fake_channel_ip[0], + 'MAC': '00:d0:23:80:15:a6', + 'netmask': '255.255.240.0', + 'type': 'dhcp', + 'gateway': '172.27.127.254'}, { + 'datalink': 'CH1', + 'status': ch1_status, + 'typeConfig': 'DHCP', + 'IP': self.fake_channel_ip[1], + 'MAC': '00:d0:23:40:15:a6', + 'netmask': '255.255.240.0', + 'type': 'dhcp', + 'gateway': '172.27.127.254'}, { + 'datalink': 'CH2', + 'status': 'DOWN', + 'typeConfig': 'DHCP', + 'IP': '', + 'MAC': '00:d0:23:c0:15:a6', + 'netmask': '', + 'type': '', + 'gateway': ''}, { + 'datalink': 'CH3', + 'status': 'DOWN', + 'typeConfig': 'DHCP', + 'IP': '', + 'MAC': '00:d0:23:20:15:a6', + 'netmask': '', + 'type': '', + 'gateway': '', + }] + + fake_fquota_status = [{ + 'quota': '21474836480', + 'used': '0', + 'name': 'test-folder', + 'type': 'subfolder', + 'id': '537178178'}, { + 'quota': '32212254720', + 'used': '0', + 'name': fake_share_name[0], + 'type': 'subfolder', + 'id': '805306752'}, { + 'quota': '53687091200', + 'used': '21474836480', + 'name': fake_share_name[1], + 'type': 'subfolder', + 'id': '69'}, { + 'quota': '94091997184', + 'used': '0', + 'type': 'subfolder', + 'id': '70', + "name": 'test-folder-02' + }] + + fake_fquota_status_with_no_settings = [] + + def fake_get_share_status_nfs(self, status=False): + fake_share_status_nfs = [{ + 'ftp': False, + 'cifs': False, + 'oss': False, + 'sftp': False, + 'nfs': status, + 'directory': '/LV-1/share-pool-01/' + self.fake_share_name[0], + 'exist': True, + 'afp': False, + 'webdav': False + }] + if status: + fake_share_status_nfs[0]['nfs_detail'] = { + 'hostList': [{ + 'uid': '65534', + 'insecure': 'insecure', + 'squash': 'all', + 'access': 'ro', + 'host': '*', + 'gid': '65534', + 'mode': 'async', + 'no_subtree_check': 'no_subtree_check', + }] + } + return fake_share_status_nfs + + def fake_get_share_status_cifs(self, status=False): + fake_share_status_cifs = [{ + 'ftp': False, + 'cifs': status, + 'oss': False, + 'sftp': False, + 'nfs': False, + 'directory': '/share-pool-01/LV-1/' + self.fake_share_name[1], + 'exist': True, + 'afp': False, + 'webdav': False + }] + if status: + fake_share_status_cifs[0]['cifs_detail'] = { + 'available': True, + 'encrypt': False, + 'description': '', + 'sharename': 'cifs-01', + 'failover': '', + 'AIO': True, + 'priv': 'None', + 'recycle_bin': False, + 'ABE': True, + } + return fake_share_status_cifs + + fake_subfolder_data = [{ + 'size': '6', + 'index': '34', + 'description': '', + 'encryption': '', + 'isEnd': False, + 'share': False, + 'volumeId': '6541BAFB2E6C57B6', + 'quota': '', + 'modifyTime': '2017-04-06 11:35', + 'owner': 'A', + 'path': '/share-pool-01/LV-1/UserHome', + 'subshare': True, + 'type': 'subfolder', + 'empty': False, + 'name': 'UserHome'}, { + 'size': '6', + 'index': '39', + 'description': '', + 'encryption': '', + 'isEnd': False, + 'share': False, + 'volumeId': '6541BAFB2E6C57B6', + 'quota': '21474836480', + 'modifyTime': '2017-04-27 15:44', + 'owner': 'A', + 'path': '/share-pool-01/LV-1/test-folder', + 'subshare': False, + 'type': 'subfolder', + 'empty': True, + 'name': 'test-folder'}, { + 'size': '6', + 'index': '45', + 'description': '', + 'encryption': '', + 'isEnd': False, + 'share': True, + 'volumeId': '6541BAFB2E6C57B6', + 'quota': '32212254720', + 'modifyTime': '2017-04-27 16:15', + 'owner': 'A', + 'path': '/share-pool-01/LV-1/' + fake_share_name[0], + 'subshare': False, + 'type': 'subfolder', + 'empty': True, + 'name': fake_share_name[0]}, { + 'size': '6', + 'index': '512', + 'description': '', + 'encryption': '', + 'isEnd': True, + 'share': True, + 'volumeId': '6541BAFB2E6C57B6', + 'quota': '53687091200', + 'modifyTime': '2017-04-27 16:16', + 'owner': 'A', + 'path': '/share-pool-01/LV-1/' + fake_share_name[1], + 'subshare': False, + 'type': 'subfolder', + 'empty': True, + 'name': fake_share_name[1]}, { + 'size': '6', + 'index': '777', + 'description': '', + 'encryption': '', + 'isEnd': False, + 'share': False, + 'volumeId': '6541BAFB2E6C57B6', + 'quota': '94091997184', + 'modifyTime': '2017-04-28 15:44', + 'owner': 'A', + 'path': '/share-pool-01/LV-1/test-folder-02', + 'subshare': False, + 'type': 'subfolder', + 'empty': True, + 'name': 'test-folder-02' + }] + + fake_cifs_user_list = [{ + 'Superuser': 'No', + 'Group': 'users', + 'Description': '', + 'Quota': 'none', + 'PWD Expiry Date': '2291-01-19', + 'Home Directory': '/share-pool-01/LV-1/UserHome/user01', + 'UID': '100001', + 'Type': 'Local', + 'Name': 'user01'}, { + 'Superuser': 'No', + 'Group': 'users', + 'Description': '', + 'Quota': 'none', + 'PWD Expiry Date': '2017-08-07', + 'Home Directory': '/share-pool-01/LV-1/UserHome/user02', + 'UID': '100002', + 'Type': 'Local', + 'Name': 'user02' + }] + + fake_share_status_nfs_with_rules = [{ + 'ftp': False, + 'cifs': False, + 'oss': False, + 'sftp': False, + 'nfs': True, + 'directory': '/share-pool-01/LV-1/' + fake_share_name[0], + 'exist': True, + 'nfs_detail': { + 'hostList': [{ + 'uid': '65534', + 'insecure': 'insecure', + 'squash': 'all', + 'access': 'ro', + 'host': '*', + 'gid': '65534', + 'mode': 'async', + 'no_subtree_check': + 'no_subtree_check'}, { + 'uid': '65534', + 'insecure': 'insecure', + 'squash': 'all', + 'access': 'rw', + 'host': '172.27.1.1', + 'gid': '65534', + 'mode': 'async', + 'no_subtree_check': 'no_subtree_check'}, { + 'uid': '65534', + 'insecure': 'insecure', + 'squash': 'all', + 'access': 'rw', + 'host': '172.27.1.2', + 'gid': '65534', + 'mode': 'async', + 'no_subtree_check': 'no_subtree_check'}] + }, + 'afp': False, + 'webdav': False, + }] + + fake_share_status_cifs_with_rules = [ + { + 'permission': { + 'Read': True, + 'Write': True, + 'Execute': True}, + 'type': 'user', + 'id': '100001', + 'name': 'user01' + }, { + 'permission': { + 'Read': True, + 'Write': False, + 'Execute': True}, + 'type': 'user', + 'id': '100002', + 'name': 'user02' + }, { + 'permission': { + 'Read': True, + 'Write': False, + 'Execute': True}, + 'type': 'group@', + 'id': '100', + 'name': 'users' + }, { + 'permission': { + 'Read': True, + 'Write': False, + 'Execute': True}, + 'type': 'other@', + 'id': '', + 'name': '' + } + ] diff --git a/manila/tests/share/drivers/infortrend/test_infortrend_nas.py b/manila/tests/share/drivers/infortrend/test_infortrend_nas.py new file mode 100644 index 0000000000..e55cc54d2d --- /dev/null +++ b/manila/tests/share/drivers/infortrend/test_infortrend_nas.py @@ -0,0 +1,573 @@ +# Copyright (c) 2019 Infortrend Technology, 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. + +import ddt +import mock + +from oslo_config import cfg + +from manila import context +from manila import exception +from manila.share import configuration +from manila.share.drivers.infortrend import driver +from manila.share.drivers.infortrend import infortrend_nas +from manila import test +from manila.tests.share.drivers.infortrend import fake_infortrend_manila_data +from manila.tests.share.drivers.infortrend import fake_infortrend_nas_data + +CONF = cfg.CONF + +SUCCEED = (0, []) + + +@ddt.ddt +class InfortrendNASDriverTestCase(test.TestCase): + def __init__(self, *args, **kwargs): + super(InfortrendNASDriverTestCase, self).__init__(*args, **kwargs) + self._ctxt = context.get_admin_context() + self.nas_data = fake_infortrend_nas_data.InfortrendNASTestData() + self.m_data = fake_infortrend_manila_data.InfortrendManilaTestData() + + def setUp(self): + CONF.set_default('driver_handles_share_servers', False) + CONF.set_default('infortrend_nas_ip', '172.27.1.1') + CONF.set_default('infortrend_nas_user', 'fake_user') + CONF.set_default('infortrend_nas_password', 'fake_password') + CONF.set_default('infortrend_nas_ssh_key', 'fake_sshkey') + CONF.set_default('infortrend_share_pools', 'share-pool-01') + CONF.set_default('infortrend_share_channels', '0,1') + self.fake_conf = configuration.Configuration(None) + super(InfortrendNASDriverTestCase, self).setUp() + + def _get_driver(self, fake_conf, init_dict=False): + self._driver = driver.InfortrendNASDriver( + configuration=fake_conf) + self._iftnas = self._driver.ift_nas + self.pool_id = ['6541BAFB2E6C57B6'] + self.pool_path = ['/share-pool-01/LV-1/'] + + if init_dict: + self._iftnas.pool_dict = { + 'share-pool-01': { + 'id': self.pool_id[0], + 'path': self.pool_path[0], + } + } + self._iftnas.channel_dict = { + '0': self.nas_data.fake_channel_ip[0], + '1': self.nas_data.fake_channel_ip[1], + } + + def test_no_login_ssh_key_and_pass(self): + self.fake_conf.set_default('infortrend_nas_password', None) + self.fake_conf.set_default('infortrend_nas_ssh_key', None) + + self.assertRaises( + exception.InvalidParameterValue, + self._get_driver, + self.fake_conf) + + def test_parser_with_service_status(self): + self._get_driver(self.fake_conf) + expect_service_status = [{ + 'A': { + 'NFS': { + 'displayName': 'NFS', + 'state_time': '2017-05-04 14:19:53', + 'enabled': True, + 'cpu_rate': '0.0', + 'mem_rate': '0.0', + 'state': 'exited', + 'type': 'share', + } + } + }] + + rc, service_status = self._iftnas._parser( + self.nas_data.fake_service_status_data) + + self.assertEqual(0, rc) + self.assertDictListMatch(expect_service_status, service_status) + + def test_parser_with_folder_status(self): + self._get_driver(self.fake_conf) + expect_folder_status = [{ + 'utility': '1.00', + 'used': '33886208', + 'subshare': True, + 'share': False, + 'worm': '', + 'free': '321931374592', + 'fsType': 'xfs', + 'owner': 'A', + 'readOnly': False, + 'modifyTime': '2017-04-27 16:16', + 'directory': self.pool_path[0][:-1], + 'volumeId': self.pool_id[0], + 'mounted': True, + 'size': '321965260800'}, { + 'utility': '1.00', + 'used': '33779712', + 'subshare': False, + 'share': False, + 'worm': '', + 'free': '107287973888', + 'fsType': 'xfs', + 'owner': 'A', + 'readOnly': False, + 'modifyTime': '2017-04-27 15:45', + 'directory': '/share-pool-02/LV-1', + 'volumeId': '147A8FB67DA39914', + 'mounted': True, + 'size': '107321753600' + }] + + rc, folder_status = self._iftnas._parser( + self.nas_data.fake_folder_status_data) + + self.assertEqual(0, rc) + self.assertDictListMatch(expect_folder_status, folder_status) + + def test_ensure_service_on(self): + self._get_driver(self.fake_conf) + mock_execute = mock.Mock( + side_effect=[(0, self.nas_data.fake_nfs_status_off), SUCCEED]) + self._iftnas._execute = mock_execute + + self._iftnas._ensure_service_on('nfs') + + mock_execute.assert_called_with(['service', 'restart', 'nfs']) + + def test_check_channels_status(self): + self._get_driver(self.fake_conf) + expect_channel_dict = { + '0': self.nas_data.fake_channel_ip[0], + '1': self.nas_data.fake_channel_ip[1], + } + + self._iftnas._execute = mock.Mock( + return_value=(0, self.nas_data.fake_get_channel_status())) + + self._iftnas._check_channels_status() + + self.assertDictMatch(expect_channel_dict, self._iftnas.channel_dict) + + @mock.patch.object(infortrend_nas.LOG, 'warning') + def test_channel_status_down(self, log_warning): + self._get_driver(self.fake_conf) + self._iftnas._execute = mock.Mock( + return_value=(0, self.nas_data.fake_get_channel_status('DOWN'))) + + self._iftnas._check_channels_status() + + self.assertEqual(1, log_warning.call_count) + + @mock.patch.object(infortrend_nas.LOG, 'error') + def test_invalid_channel(self, log_error): + self.fake_conf.set_default('infortrend_share_channels', '0, 6') + self._get_driver(self.fake_conf) + self._iftnas._execute = mock.Mock( + return_value=(0, self.nas_data.fake_get_channel_status())) + + self.assertRaises( + exception.InfortrendNASException, + self._iftnas._check_channels_status) + + def test_check_pools_setup(self): + self._get_driver(self.fake_conf) + expect_pool_dict = { + 'share-pool-01': { + 'id': self.pool_id[0], + 'path': self.pool_path[0], + } + } + self._iftnas._execute = mock.Mock( + return_value=(0, self.nas_data.fake_folder_status)) + + self._iftnas._check_pools_setup() + + self.assertDictMatch(expect_pool_dict, self._iftnas.pool_dict) + + def test_unknow_pools_setup(self): + self.fake_conf.set_default( + 'infortrend_share_pools', 'chengwei, share-pool-01') + self._get_driver(self.fake_conf) + self._iftnas._execute = mock.Mock( + return_value=(0, self.nas_data.fake_folder_status)) + + self.assertRaises( + exception.InfortrendNASException, + self._iftnas._check_pools_setup) + + @mock.patch.object(infortrend_nas.InfortrendNAS, '_execute') + def test_get_pool_quota_used(self, mock_execute): + self._get_driver(self.fake_conf, True) + mock_execute.return_value = (0, self.nas_data.fake_fquota_status) + + pool_quota = self._iftnas._get_pool_quota_used('share-pool-01') + + mock_execute.assert_called_with( + ['fquota', 'status', self.pool_id[0], + 'LV-1', '-t', 'folder']) + self.assertEqual(201466179584, pool_quota) + + @mock.patch.object(infortrend_nas.InfortrendNAS, '_execute') + def test_create_share_nfs(self, mock_execute): + self._get_driver(self.fake_conf, True) + fake_share_id = self.m_data.fake_share_nfs['id'] + fake_share_name = fake_share_id.replace('-', '') + expect_locations = [ + self.nas_data.fake_channel_ip[0] + + ':/share-pool-01/LV-1/' + fake_share_name, + self.nas_data.fake_channel_ip[1] + + ':/share-pool-01/LV-1/' + fake_share_name, + ] + mock_execute.side_effect = [ + SUCCEED, # create folder + SUCCEED, # set size + (0, self.nas_data.fake_get_share_status_nfs()), # check proto + SUCCEED, # enable proto + (0, self.nas_data.fake_get_channel_status()) # update channel + ] + + locations = self._driver.create_share( + self._ctxt, self.m_data.fake_share_nfs) + + self.assertEqual(expect_locations, locations) + mock_execute.assert_any_call( + ['share', self.pool_path[0] + fake_share_name, 'nfs', 'on']) + + @mock.patch.object(infortrend_nas.InfortrendNAS, '_execute') + def test_create_share_cifs(self, mock_execute): + self._get_driver(self.fake_conf, True) + fake_share_id = self.m_data.fake_share_cifs['id'] + fake_share_name = fake_share_id.replace('-', '') + expect_locations = [ + '\\\\' + self.nas_data.fake_channel_ip[0] + + '\\' + fake_share_name, + '\\\\' + self.nas_data.fake_channel_ip[1] + + '\\' + fake_share_name, + ] + mock_execute.side_effect = [ + SUCCEED, # create folder + SUCCEED, # set size + (0, self.nas_data.fake_get_share_status_cifs()), # check proto + SUCCEED, # enable proto + (0, self.nas_data.fake_get_channel_status()) # update channel + ] + + locations = self._driver.create_share( + self._ctxt, self.m_data.fake_share_cifs) + + self.assertEqual(expect_locations, locations) + mock_execute.assert_any_call( + ['share', self.pool_path[0] + fake_share_name, + 'cifs', 'on', '-n', fake_share_name]) + + @mock.patch.object(infortrend_nas.InfortrendNAS, '_execute') + def test_delete_share_nfs(self, mock_execute): + self._get_driver(self.fake_conf, True) + fake_share_id = self.m_data.fake_share_nfs['id'] + fake_share_name = fake_share_id.replace('-', '') + mock_execute.side_effect = [ + (0, self.nas_data.fake_subfolder_data), # pagelist folder + SUCCEED, # delete folder + ] + + self._driver.delete_share( + self._ctxt, self.m_data.fake_share_nfs) + + mock_execute.assert_any_call( + ['folder', 'options', self.pool_id[0], + 'LV-1', '-d', fake_share_name]) + + @mock.patch.object(infortrend_nas.InfortrendNAS, '_execute') + def test_delete_share_cifs(self, mock_execute): + self._get_driver(self.fake_conf, True) + fake_share_id = self.m_data.fake_share_cifs['id'] + fake_share_name = fake_share_id.replace('-', '') + mock_execute.side_effect = [ + (0, self.nas_data.fake_subfolder_data), # pagelist folder + SUCCEED, # delete folder + ] + + self._driver.delete_share( + self._ctxt, self.m_data.fake_share_cifs) + + mock_execute.assert_any_call( + ['folder', 'options', self.pool_id[0], + 'LV-1', '-d', fake_share_name]) + + @mock.patch.object(infortrend_nas.LOG, 'warning') + @mock.patch.object(infortrend_nas.InfortrendNAS, '_execute') + def test_delete_non_exist_share(self, mock_execute, log_warning): + self._get_driver(self.fake_conf, True) + mock_execute.side_effect = [ + (0, self.nas_data.fake_subfolder_data), # pagelist folder + ] + + self._driver.delete_share( + self._ctxt, self.m_data.fake_non_exist_share) + + self.assertEqual(1, log_warning.call_count) + + def test_get_pool(self): + self._get_driver(self.fake_conf, True) + pool = self._driver.get_pool(self.m_data.fake_share_nfs) + + self.assertEqual('share-pool-01', pool) + + def test_get_pool_without_host(self): + self._get_driver(self.fake_conf, True) + self._iftnas._execute = mock.Mock( + return_value=(0, self.nas_data.fake_subfolder_data)) + + pool = self._driver.get_pool(self.m_data.fake_share_cifs_no_host) + + self.assertEqual('share-pool-01', pool) + + def test_ensure_share_nfs(self): + self._get_driver(self.fake_conf, True) + share_id = self.m_data.fake_share_nfs['id'] + share_name = share_id.replace('-', '') + share_path = self.pool_path[0] + share_name + expect_locations = [ + self.nas_data.fake_channel_ip[0] + ':' + share_path, + self.nas_data.fake_channel_ip[1] + ':' + share_path, + ] + self._iftnas._execute = mock.Mock( + return_value=(0, self.nas_data.fake_get_channel_status())) + + locations = self._driver.ensure_share( + self._ctxt, self.m_data.fake_share_nfs) + + self.assertEqual(expect_locations, locations) + + def test_ensure_share_cifs(self): + self._get_driver(self.fake_conf, True) + share_id = self.m_data.fake_share_cifs['id'] + share_name = share_id.replace('-', '') + expect_locations = [ + '\\\\' + self.nas_data.fake_channel_ip[0] + + '\\' + share_name, + '\\\\' + self.nas_data.fake_channel_ip[1] + + '\\' + share_name, + ] + self._iftnas._execute = mock.Mock( + return_value=(0, self.nas_data.fake_get_channel_status())) + + locations = self._driver.ensure_share( + self._ctxt, self.m_data.fake_share_cifs) + + self.assertEqual(expect_locations, locations) + + def test_extend_share(self): + self._get_driver(self.fake_conf, True) + share_id = self.m_data.fake_share_nfs['id'] + share_name = share_id.replace('-', '') + self._iftnas._execute = mock.Mock(return_value=SUCCEED) + + self._driver.extend_share(self.m_data.fake_share_nfs, 100) + + self._iftnas._execute.assert_called_once_with( + ['fquota', 'create', self.pool_id[0], 'LV-1', + share_name, '100G', '-t', 'folder']) + + @mock.patch.object(infortrend_nas.InfortrendNAS, '_execute') + def test_shrink_share(self, mock_execute): + self._get_driver(self.fake_conf, True) + share_id = self.m_data.fake_share_nfs['id'] + share_name = share_id.replace('-', '') + mock_execute.side_effect = [ + (0, self.nas_data.fake_fquota_status), # check used + SUCCEED, + ] + + self._driver.shrink_share(self.m_data.fake_share_nfs, 10) + + mock_execute.assert_has_calls([ + mock.call(['fquota', 'status', self.pool_id[0], + 'LV-1', '-t', 'folder']), + mock.call(['fquota', 'create', self.pool_id[0], + 'LV-1', share_name, '10G', '-t', 'folder'])]) + + @mock.patch.object(infortrend_nas.InfortrendNAS, '_execute') + def test_shrink_share_smaller_than_used_size(self, mock_execute): + self._get_driver(self.fake_conf, True) + mock_execute.side_effect = [ + (0, self.nas_data.fake_fquota_status), # check used + ] + + self.assertRaises( + exception.ShareShrinkingPossibleDataLoss, + self._driver.shrink_share, + self.m_data.fake_share_cifs, + 10) + + def test_get_share_size(self): + self._get_driver(self.fake_conf, True) + self._iftnas._execute = mock.Mock( + return_value=(0, self.nas_data.fake_fquota_status)) + + size = self._iftnas._get_share_size('', '', 'test-folder-02') + + self.assertEqual(87.63, size) + + @mock.patch.object(infortrend_nas.InfortrendNAS, '_execute') + def test_manage_existing_nfs(self, mock_execute): + self._get_driver(self.fake_conf, True) + share_id = self.m_data.fake_share_for_manage_nfs['id'] + share_name = share_id.replace('-', '') + origin_share_path = self.pool_path[0] + 'test-folder' + export_share_path = self.pool_path[0] + share_name + expect_result = { + 'size': 20.0, + 'export_locations': [ + self.nas_data.fake_channel_ip[0] + ':' + export_share_path, + self.nas_data.fake_channel_ip[1] + ':' + export_share_path, + ] + } + mock_execute.side_effect = [ + (0, self.nas_data.fake_subfolder_data), # pagelist folder + (0, self.nas_data.fake_get_share_status_nfs()), # check proto + SUCCEED, # enable nfs + (0, self.nas_data.fake_fquota_status), # get share size + SUCCEED, # rename share + (0, self.nas_data.fake_get_channel_status()) # update channel + ] + + result = self._driver.manage_existing( + self.m_data.fake_share_for_manage_nfs, + {} + ) + + self.assertEqual(expect_result, result) + mock_execute.assert_has_calls([ + mock.call(['pagelist', 'folder', self.pool_path[0]]), + mock.call(['share', 'status', '-f', origin_share_path]), + mock.call(['share', origin_share_path, 'nfs', 'on']), + mock.call(['fquota', 'status', self.pool_id[0], + origin_share_path.split('/')[3], '-t', 'folder']), + mock.call(['folder', 'options', self.pool_id[0], + 'LV-1', '-k', 'test-folder', share_name]), + mock.call(['ifconfig', 'inet', 'show']), + ]) + + @mock.patch.object(infortrend_nas.InfortrendNAS, '_execute') + def test_manage_existing_cifs(self, mock_execute): + self._get_driver(self.fake_conf, True) + share_id = self.m_data.fake_share_for_manage_cifs['id'] + share_name = share_id.replace('-', '') + origin_share_path = self.pool_path[0] + 'test-folder-02' + expect_result = { + 'size': 87.63, + 'export_locations': [ + '\\\\' + self.nas_data.fake_channel_ip[0] + '\\' + share_name, + '\\\\' + self.nas_data.fake_channel_ip[1] + '\\' + share_name, + ] + } + mock_execute.side_effect = [ + (0, self.nas_data.fake_subfolder_data), # pagelist folder + (0, self.nas_data.fake_get_share_status_cifs()), # check proto + SUCCEED, # enable cifs + (0, self.nas_data.fake_fquota_status), # get share size + SUCCEED, # rename share + (0, self.nas_data.fake_get_channel_status()) # update channel + ] + + result = self._driver.manage_existing( + self.m_data.fake_share_for_manage_cifs, + {} + ) + + self.assertEqual(expect_result, result) + mock_execute.assert_has_calls([ + mock.call(['pagelist', 'folder', self.pool_path[0]]), + mock.call(['share', 'status', '-f', origin_share_path]), + mock.call(['share', origin_share_path, 'cifs', 'on', + '-n', share_name]), + mock.call(['fquota', 'status', self.pool_id[0], + origin_share_path.split('/')[3], '-t', 'folder']), + mock.call(['folder', 'options', self.pool_id[0], + 'LV-1', '-k', 'test-folder-02', share_name]), + mock.call(['ifconfig', 'inet', 'show']), + ]) + + def test_manage_existing_with_no_location(self): + self._get_driver(self.fake_conf, True) + fake_share = self.m_data._get_fake_share_for_manage('') + + self.assertRaises( + exception.InfortrendNASException, + self._driver.manage_existing, + fake_share, {}) + + @ddt.data('172.27.1.1:/share-pool-01/LV-1/test-folder', + '172.27.112.223:/share-pool-01/LV-1/some-folder') + def test_manage_existing_wrong_ip_or_name(self, fake_share_path): + self._get_driver(self.fake_conf, True) + fake_share = self.m_data._get_fake_share_for_manage(fake_share_path) + self._iftnas._execute = mock.Mock( + return_value=(0, self.nas_data.fake_subfolder_data)) + + self.assertRaises( + exception.InfortrendNASException, + self._driver.manage_existing, + fake_share, {}) + + @mock.patch.object(infortrend_nas.InfortrendNAS, '_execute') + def test_manage_existing_with_no_size_setting(self, mock_execute): + self._get_driver(self.fake_conf, True) + mock_execute.side_effect = [ + (0, self.nas_data.fake_subfolder_data), # pagelist folder + (0, self.nas_data.fake_get_share_status_nfs()), # check proto + SUCCEED, # enable nfs + (0, self.nas_data.fake_fquota_status_with_no_settings), + ] + + self.assertRaises( + exception.InfortrendNASException, + self._driver.manage_existing, + self.m_data.fake_share_for_manage_nfs, + {}) + + @ddt.data('NFS', 'CIFS') + @mock.patch.object(infortrend_nas.InfortrendNAS, '_execute') + def test_unmanage(self, protocol, mock_execute): + share_to_unmanage = (self.m_data.fake_share_nfs + if protocol == 'NFS' else + self.m_data.fake_share_cifs) + self._get_driver(self.fake_conf, True) + mock_execute.side_effect = [ + (0, self.nas_data.fake_subfolder_data), # pagelist folder + ] + + self._driver.unmanage(share_to_unmanage) + + mock_execute.assert_called_once_with( + ['pagelist', 'folder', self.pool_path[0]], + ) + + @mock.patch.object(infortrend_nas.LOG, 'warning') + def test_unmanage_share_not_exist(self, log_warning): + self._get_driver(self.fake_conf, True) + self._iftnas._execute = mock.Mock( + return_value=(0, self.nas_data.fake_subfolder_data)) + + self._driver.unmanage( + self.m_data.fake_share_for_manage_nfs, + ) + + self.assertEqual(1, log_warning.call_count) diff --git a/releasenotes/notes/infortrend-manila-driver-a1a2af20de6368cb.yaml b/releasenotes/notes/infortrend-manila-driver-a1a2af20de6368cb.yaml new file mode 100644 index 0000000000..b7f87f333c --- /dev/null +++ b/releasenotes/notes/infortrend-manila-driver-a1a2af20de6368cb.yaml @@ -0,0 +1,5 @@ +--- +prelude: > + Added Manila share driver for Infortrend storage systems. +features: + - The new Infortrend driver supports GS/GSe Family storage systems.