From 2a7d480d0130cae4fad6808edd73ed516564371c Mon Sep 17 00:00:00 2001 From: tpsilva <tiago.pasqualini@gmail.com> Date: Mon, 13 Jun 2016 15:05:47 -0300 Subject: [PATCH] Add Hitachi HSP driver This patch adds the Hitachi HSP driver. Co-Authored-By: Marcus V R Nascimento <marcusvrn@gmail.com> DocImpact Implements: blueprint hsp-driver Change-Id: Ie3028cbff56873721dc51ff4d7fccf7fb7e99611 --- ...hare_back_ends_feature_support_mapping.rst | 8 + manila/exception.py | 13 + manila/opts.py | 2 + manila/share/drivers/hitachi/hsp/__init__.py | 0 manila/share/drivers/hitachi/hsp/driver.py | 321 ++++++++++++++ manila/share/drivers/hitachi/hsp/rest.py | 207 +++++++++ manila/tests/conf_fixture.py | 4 + .../share/drivers/hitachi/hsp/__init__.py | 0 .../tests/share/drivers/hitachi/hsp/fakes.py | 85 ++++ .../share/drivers/hitachi/hsp/test_driver.py | 415 ++++++++++++++++++ .../share/drivers/hitachi/hsp/test_rest.py | 344 +++++++++++++++ .../notes/hsp-driver-e00aff5bc89d4b54.yaml | 7 + 12 files changed, 1406 insertions(+) create mode 100644 manila/share/drivers/hitachi/hsp/__init__.py create mode 100644 manila/share/drivers/hitachi/hsp/driver.py create mode 100644 manila/share/drivers/hitachi/hsp/rest.py create mode 100644 manila/tests/share/drivers/hitachi/hsp/__init__.py create mode 100644 manila/tests/share/drivers/hitachi/hsp/fakes.py create mode 100644 manila/tests/share/drivers/hitachi/hsp/test_driver.py create mode 100644 manila/tests/share/drivers/hitachi/hsp/test_rest.py create mode 100644 releasenotes/notes/hsp-driver-e00aff5bc89d4b54.yaml diff --git a/doc/source/devref/share_back_ends_feature_support_mapping.rst b/doc/source/devref/share_back_ends_feature_support_mapping.rst index b9c526054b..e86789a935 100644 --- a/doc/source/devref/share_back_ends_feature_support_mapping.rst +++ b/doc/source/devref/share_back_ends_feature_support_mapping.rst @@ -53,6 +53,8 @@ Mapping of share drivers and share features support +----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+ | Hitachi HNAS | L | L | L | M | L | L | \- | +----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+ +| Hitachi HSP | N | N | N | N | \- | \- | \- | ++----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+ | HPE 3PAR | K | \- | \- | \- | K | K | \- | +----------------------------------------+-----------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+ | Huawei | K | L | L | L | K | M | \- | @@ -100,6 +102,8 @@ Mapping of share drivers and share access rules support +----------------------------------------+--------------+----------------+------------+--------------+--------------+----------------+------------+------------+ | Hitachi HNAS | NFS (L) | \- | \- | \- | NFS (L) | \- | \- | \- | +----------------------------------------+--------------+----------------+------------+--------------+--------------+----------------+------------+------------+ +| Hitachi HSP | NFS (N) | \- | \- | \- | NFS (N) | \- | \- | \- | ++----------------------------------------+--------------+----------------+------------+--------------+--------------+----------------+------------+------------+ | HPE 3PAR | NFS,CIFS (K) | CIFS (K) | \- | \- | \- | \- | \- | \- | +----------------------------------------+--------------+----------------+------------+--------------+--------------+----------------+------------+------------+ | Huawei | NFS (K) |NFS (M),CIFS (K)| \- | \- | NFS (K) |NFS (M),CIFS (K)| \- | \- | @@ -145,6 +149,8 @@ Mapping of share drivers and security services support +----------------------------------------+------------------+-----------------+------------------+ | Hitachi HNAS | \- | \- | \- | +----------------------------------------+------------------+-----------------+------------------+ +| Hitachi HSP | \- | \- | \- | ++----------------------------------------+------------------+-----------------+------------------+ | HPE 3PAR | \- | \- | \- | +----------------------------------------+------------------+-----------------+------------------+ | Huawei | M | M | \- | @@ -190,6 +196,8 @@ Mapping of share drivers and common capabilities +----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+ | Hitachi HNAS | \- | L | \- | \- | L | \- | \- | +----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+ +| Hitachi HSP | \- | N | \- | \- | N | \- | \- | ++----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+ | HPE 3PAR | L | K | L | \- | L | L | \- | +----------------------------------------+-----------+------------+--------+-------------+-------------------+--------------------+-----+ | Huawei | M | K | L | L | L | L | M | diff --git a/manila/exception.py b/manila/exception.py index dac2c63733..432d0926fe 100644 --- a/manila/exception.py +++ b/manila/exception.py @@ -794,3 +794,16 @@ class StorageCommunicationException(ShareBackendException): class EvaluatorParseException(ManilaException): message = _("Error during evaluator parsing: %(reason)s") + + +# Hitachi Scaleout Platform driver +class HSPBackendException(ShareBackendException): + message = _("HSP Backend Exception: %(msg)s") + + +class HSPTimeoutException(ShareBackendException): + message = _("HSP Timeout Exception: %(msg)s") + + +class HSPItemNotFoundException(ShareBackendException): + message = _("HSP Item Not Found Exception: %(msg)s") diff --git a/manila/opts.py b/manila/opts.py index 870077005c..fada4565e8 100644 --- a/manila/opts.py +++ b/manila/opts.py @@ -61,6 +61,7 @@ import manila.share.drivers.glusterfs.layout_directory import manila.share.drivers.glusterfs.layout_volume import manila.share.drivers.hdfs.hdfs_native import manila.share.drivers.hitachi.hds_hnas +import manila.share.drivers.hitachi.hsp.driver import manila.share.drivers.hpe.hpe_3par_driver import manila.share.drivers.huawei.huawei_nas import manila.share.drivers.ibm.gpfs @@ -127,6 +128,7 @@ _global_opt_lists = [ manila.share.drivers.glusterfs.layout_volume.glusterfs_volume_mapped_opts, manila.share.drivers.hdfs.hdfs_native.hdfs_native_share_opts, manila.share.drivers.hitachi.hds_hnas.hds_hnas_opts, + manila.share.drivers.hitachi.hsp.driver.hitachi_hsp_opts, manila.share.drivers.hpe.hpe_3par_driver.HPE3PAR_OPTS, manila.share.drivers.huawei.huawei_nas.huawei_opts, manila.share.drivers.ibm.gpfs.gpfs_share_opts, diff --git a/manila/share/drivers/hitachi/hsp/__init__.py b/manila/share/drivers/hitachi/hsp/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/share/drivers/hitachi/hsp/driver.py b/manila/share/drivers/hitachi/hsp/driver.py new file mode 100644 index 0000000000..3861968ae8 --- /dev/null +++ b/manila/share/drivers/hitachi/hsp/driver.py @@ -0,0 +1,321 @@ +# Copyright (c) 2016 Hitachi Data Systems, 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 oslo_utils import excutils +from oslo_utils import units + +from manila.common import constants +from manila import exception +from manila.i18n import _ +from manila.i18n import _LE +from manila.i18n import _LI +from manila.share import driver +from manila.share.drivers.hitachi.hsp import rest + +LOG = log.getLogger(__name__) + +hitachi_hsp_opts = [ + cfg.StrOpt('hitachi_hsp_host', + required=True, + help="HSP management host for communication between Manila " + "controller and HSP."), + cfg.StrOpt('hitachi_hsp_username', + required=True, + help="HSP username to perform tasks such as create filesystems" + " and shares."), + cfg.StrOpt('hitachi_hsp_password', + required=True, + secret=True, + help="HSP password for the username provided."), +] + + +class HitachiHSPDriver(driver.ShareDriver): + """Manila HSP Driver implementation. + + 1.0.0 - Initial Version. + """ + + def __init__(self, *args, **kwargs): + super(self.__class__, self).__init__( + [False], *args, config_opts=[hitachi_hsp_opts], **kwargs) + + self.private_storage = kwargs.get('private_storage') + + self.backend_name = self.configuration.safe_get('share_backend_name') + self.hsp_host = self.configuration.safe_get('hitachi_hsp_host') + + self.hsp = rest.HSPRestBackend( + self.hsp_host, + self.configuration.safe_get('hitachi_hsp_username'), + self.configuration.safe_get('hitachi_hsp_password') + ) + + def _update_share_stats(self, data=None): + LOG.debug("Updating Backend Capability Information - Hitachi HSP.") + + reserved = self.configuration.safe_get('reserved_share_percentage') + max_over_subscription_ratio = self.configuration.safe_get( + 'max_over_subscription_ratio') + hsp_cluster = self.hsp.get_cluster() + + total_space = hsp_cluster['properties']['total-storage-capacity'] + free_space = hsp_cluster['properties']['total-storage-available'] + + data = { + 'share_backend_name': self.backend_name, + 'vendor_name': 'Hitachi', + 'driver_version': '1.0.0', + 'storage_protocol': 'NFS', + 'pools': [{ + 'reserved_percentage': reserved, + 'pool_name': 'HSP', + 'thin_provisioning': True, + 'total_capacity_gb': total_space / units.Gi, + 'free_capacity_gb': free_space / units.Gi, + 'max_over_subscription_ratio': max_over_subscription_ratio, + 'qos': False, + 'dedupe': False, + 'compression': False, + }], + } + + LOG.info(_LI("Hitachi HSP Capabilities: %(data)s."), + {'data': data}) + super(HitachiHSPDriver, self)._update_share_stats(data) + + def create_share(self, context, share, share_server=None): + LOG.debug("Creating share in HSP: %(shr)s", {'shr': share['id']}) + + if share['share_proto'].lower() != 'nfs': + msg = _("Only NFS protocol is currently supported.") + raise exception.InvalidShare(reason=msg) + + self.hsp.add_file_system(share['id'], share['size'] * units.Gi) + filesystem_id = self.hsp.get_file_system(share['id'])['id'] + + try: + self.hsp.add_share(share['id'], filesystem_id) + except exception.HSPBackendException: + with excutils.save_and_reraise_exception(): + self.hsp.delete_file_system(filesystem_id) + msg = _LE("Could not create share %s on HSP.") + LOG.exception(msg, share['id']) + + uri = self.hsp_host + ':/' + share['id'] + + LOG.debug("Share created successfully on path: %(uri)s.", + {'uri': uri}) + return [{ + "path": uri, + "metadata": {}, + "is_admin_only": False, + }] + + def delete_share(self, context, share, share_server=None): + LOG.debug("Deleting share in HSP: %(shr)s.", {'shr': share['id']}) + + filesystem_id = hsp_share_id = None + + try: + filesystem_id = self.hsp.get_file_system(share['id'])['id'] + hsp_share_id = self.hsp.get_share(filesystem_id)['id'] + except exception.HSPItemNotFoundException: + LOG.info(_LI("Share %(shr)s already removed from backend."), + {'shr': share['id']}) + + if hsp_share_id: + self.hsp.delete_share(hsp_share_id) + + if filesystem_id: + self.hsp.delete_file_system(filesystem_id) + + LOG.debug("Export and share successfully deleted: %(shr)s.", + {'shr': share['id']}) + + def update_access(self, context, share, access_rules, add_rules, + delete_rules, share_server=None): + + LOG.debug("Updating access rules for share: %(shr)s.", + {'shr': share['id']}) + + try: + filesystem_id = self.hsp.get_file_system(share['id'])['id'] + hsp_share_id = self.hsp.get_share(filesystem_id)['id'] + except exception.HSPItemNotFoundException: + raise exception.ShareResourceNotFound(share_id=share['id']) + + if not (add_rules or delete_rules): + # Recovery mode + current_rules = self.hsp.get_access_rules(hsp_share_id) + + # Indexing the rules for faster searching + hsp_rules_dict = { + rule['host-specification']: rule['read-write'] + for rule in current_rules + } + + manila_rules_dict = {} + + for rule in access_rules: + if rule['access_type'].lower() != 'ip': + msg = _("Only IP access type currently supported.") + raise exception.InvalidShareAccess(reason=msg) + + access_to = rule['access_to'] + is_rw = rule['access_level'] == constants.ACCESS_LEVEL_RW + manila_rules_dict[access_to] = is_rw + + # Remove the rules that exist on HSP but not on manila + remove_rules = self._get_complement(hsp_rules_dict, + manila_rules_dict) + + # Add the rules that exist on manila but not on HSP + add_rules = self._get_complement(manila_rules_dict, hsp_rules_dict) + + for rule in remove_rules: + self.hsp.delete_access_rule(hsp_share_id, rule[0]) + + for rule in add_rules: + self.hsp.add_access_rule(hsp_share_id, rule[0], rule[1]) + else: + for rule in delete_rules: + if rule['access_type'].lower() != 'ip': + continue + self.hsp.delete_access_rule(hsp_share_id, rule['access_to']) + + for rule in add_rules: + if rule['access_type'].lower() != 'ip': + msg = _("Only IP access type currently supported.") + raise exception.InvalidShareAccess(reason=msg) + + self.hsp.add_access_rule( + hsp_share_id, rule['access_to'], + (rule['access_level'] == constants.ACCESS_LEVEL_RW)) + + LOG.debug("Successfully updated share %(shr)s rules.", + {'shr': share['id']}) + + def _get_complement(self, rules_a, rules_b): + """Returns the rules of list A that are not on list B""" + complement = [] + for rule, is_rw in rules_a.items(): + if rule not in rules_b or rules_b[rule] != is_rw: + complement.append((rule, is_rw)) + + return complement + + def extend_share(self, share, new_size, share_server=None): + LOG.debug("Extending share in HSP: %(shr_id)s.", + {'shr_id': share['id']}) + + old_size = share['size'] + hsp_cluster = self.hsp.get_cluster() + free_space = hsp_cluster['properties']['total-storage-available'] + free_space = free_space / units.Gi + + if (new_size - old_size) < free_space: + filesystem_id = self.hsp.get_file_system(share['id'])['id'] + self.hsp.resize_file_system(filesystem_id, new_size * units.Gi) + else: + msg = (_("Share %s cannot be extended due to insufficient space.") + % share['id']) + raise exception.HSPBackendException(msg=msg) + + LOG.info(_LI("Share %(shr_id)s successfully extended to " + "%(shr_size)sG."), + {'shr_id': share['id'], + 'shr_size': new_size}) + + def shrink_share(self, share, new_size, share_server=None): + LOG.debug("Shrinking share in HSP: %(shr_id)s.", + {'shr_id': share['id']}) + + file_system = self.hsp.get_file_system(share['id']) + usage = file_system['properties']['used-capacity'] / units.Gi + + LOG.debug("Usage for share %(shr_id)s in HSP: %(usage)sG.", + {'shr_id': share['id'], 'usage': usage}) + + if new_size > usage: + self.hsp.resize_file_system(file_system['id'], new_size * units.Gi) + else: + raise exception.ShareShrinkingPossibleDataLoss( + share_id=share['id']) + + LOG.info(_LI("Share %(shr_id)s successfully shrunk to " + "%(shr_size)sG."), + {'shr_id': share['id'], + 'shr_size': new_size}) + + def manage_existing(self, share, driver_options): + LOG.debug("Managing share in HSP: %(shr_id)s.", + {'shr_id': share['id']}) + + ip, share_name = share['export_locations'][0]['path'].split(':') + + try: + hsp_share = self.hsp.get_share(name=share_name.strip('/')) + except exception.HSPItemNotFoundException: + msg = _("The share %s trying to be managed was not found on " + "backend.") % share['id'] + raise exception.ManageInvalidShare(reason=msg) + + self.hsp.rename_file_system(hsp_share['properties']['file-system-id'], + share['id']) + + original_name = hsp_share['properties']['file-system-name'] + private_storage_content = { + 'old_name': original_name, + 'new_name': share['id'], + } + self.private_storage.update(share['id'], private_storage_content) + + LOG.debug("Filesystem %(original_name)s was renamed to %(name)s.", + {'original_name': original_name, + 'name': share['id']}) + + file_system = self.hsp.get_file_system(share['id']) + + LOG.info(_LI("Share %(shr_path)s was successfully managed with ID " + "%(shr_id)s."), + {'shr_path': share['export_locations'][0]['path'], + 'shr_id': share['id']}) + + export_locations = [{ + "path": share['export_locations'][0]['path'], + "metadata": {}, + "is_admin_only": False, + }] + + return {'size': file_system['properties']['quota'] / units.Gi, + 'export_locations': export_locations} + + def unmanage(self, share): + original_name = self.private_storage.get(share['id'], 'old_name') + + LOG.debug("Filesystem %(name)s that was originally named " + "%(original_name)s will no longer be managed.", + {'original_name': original_name, + 'name': share['id']}) + + self.private_storage.delete(share['id']) + + LOG.info(_LI("The share with current path %(shr_path)s and ID " + "%(shr_id)s is no longer being managed."), + {'shr_path': share['export_locations'][0]['path'], + 'shr_id': share['id']}) diff --git a/manila/share/drivers/hitachi/hsp/rest.py b/manila/share/drivers/hitachi/hsp/rest.py new file mode 100644 index 0000000000..748e06ea14 --- /dev/null +++ b/manila/share/drivers/hitachi/hsp/rest.py @@ -0,0 +1,207 @@ +# Copyright (c) 2016 Hitachi Data Systems, 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 requests + +from manila import exception +from manila.i18n import _ +from manila import utils + + +# Suppress the Insecure request warnings +requests.packages.urllib3.disable_warnings() + + +class HSPRestBackend(object): + def __init__(self, hsp_host, hsp_username, hsp_password): + self.host = hsp_host + self.username = hsp_username + self.password = hsp_password + + def _send_post(self, url, payload=None): + resp = requests.post(url, auth=(self.username, self.password), + data=payload, verify=False) + + if resp.status_code == 202: + self._wait_job_status(resp.headers['location'], 'COMPLETE') + else: + msg = (_("HSP API post failed: %s.") % + resp.json()['messages'][0]['message']) + raise exception.HSPBackendException(msg=msg) + + def _send_get(self, url, payload=None): + resp = requests.get(url, auth=(self.username, self.password), + data=payload, verify=False) + + if resp.status_code == 200: + if resp.content == 'null': + return None + else: + return resp.json() + else: + msg = (_("HSP API get failed: %s.") % + resp.json()['messages'][0]['message']) + raise exception.HSPBackendException(msg=msg) + + def _send_delete(self, url, payload=None): + resp = requests.delete(url, auth=(self.username, self.password), + data=payload, verify=False) + + if resp.status_code == 202: + self._wait_job_status(resp.headers['location'], 'COMPLETE') + else: + msg = (_("HSP API delete failed: %s.") % + resp.json()['messages'][0]['message']) + raise exception.HSPBackendException(msg=msg) + + def add_file_system(self, name, quota): + url = "https://%s/hspapi/file-systems/" % self.host + payload = { + 'quota': quota, + 'auto-access': False, + 'enabled': True, + 'description': '', + 'record-access-time': True, + 'tags': '', + # Usage percentage in which a warning will be shown + 'space-hwm': 90, + # Usage percentage in which the warning will be cleared + 'space-lwm': 70, + 'name': name, + } + self._send_post(url, payload=json.dumps(payload)) + + def get_file_system(self, name): + url = ("https://%s/hspapi/file-systems/list?name=%s" % + (self.host, name)) + + filesystems = self._send_get(url) + + try: + return filesystems['list'][0] + except (TypeError, KeyError, IndexError): + msg = _("Filesystem does not exist or is not available.") + raise exception.HSPItemNotFoundException(msg=msg) + + def delete_file_system(self, filesystem_id): + url = "https://%s/hspapi/file-systems/%s" % (self.host, filesystem_id) + self._send_delete(url) + + def resize_file_system(self, filesystem_id, new_size): + url = "https://%s/hspapi/file-systems/%s" % (self.host, filesystem_id) + payload = {'quota': new_size} + + self._send_post(url, payload=json.dumps(payload)) + + def rename_file_system(self, filesystem_id, new_name): + url = "https://%s/hspapi/file-systems/%s" % (self.host, filesystem_id) + payload = {'name': new_name} + + self._send_post(url, payload=json.dumps(payload)) + + def add_share(self, name, filesystem_id): + url = "https://%s/hspapi/shares/" % self.host + payload = { + 'description': '', + 'type': 'NFS', + 'enabled': True, + 'tags': '', + 'name': name, + 'file-system-id': filesystem_id, + } + + self._send_post(url, payload=json.dumps(payload)) + + def get_share(self, fs_id=None, name=None): + if fs_id is not None: + url = ('https://%s/hspapi/shares/list?file-system-id=%s' % + (self.host, fs_id)) + elif name is not None: + url = ('https://%s/hspapi/shares/list?name=%s' % + (self.host, name)) + share = self._send_get(url) + + try: + return share['list'][0] + except (TypeError, KeyError, IndexError): + msg = _("Share %s does not exist or is not available.") + + if fs_id is not None: + args = "for filesystem %s" % fs_id + else: + args = name + + raise exception.HSPItemNotFoundException(msg=msg % args) + + def delete_share(self, share_id): + url = "https://%s/hspapi/shares/%s" % (self.host, share_id) + self._send_delete(url) + + def add_access_rule(self, share_id, host_to, read_write): + url = "https://%s/hspapi/shares/%s/" % (self.host, share_id) + payload = { + "action": "add-access-rule", + "name": share_id + host_to, + "host-specification": host_to, + "read-write": read_write, + } + + self._send_post(url, payload=json.dumps(payload)) + + def delete_access_rule(self, share_id, host_to): + url = "https://%s/hspapi/shares/%s/" % (self.host, share_id) + payload = { + "action": "delete-access-rule", + "name": share_id + host_to, + } + + self._send_post(url, payload=json.dumps(payload)) + + def get_access_rules(self, share_id): + url = ("https://%s/hspapi/shares/%s/access-rules" % + (self.host, share_id)) + rules = self._send_get(url) + + try: + rules = rules['list'] + except (TypeError, KeyError, IndexError): + rules = [] + return rules + + def get_cluster(self): + url = "https://%s/hspapi/clusters/list" % self.host + clusters = self._send_get(url) + + try: + return clusters['list'][0] + except (TypeError, KeyError, IndexError): + msg = _("No cluster was found on HSP.") + raise exception.HSPBackendException(msg=msg) + + @utils.retry(exception.HSPTimeoutException, retries=10, wait_random=True) + def _wait_job_status(self, job_url, target_status): + resp_json = self._send_get(job_url) + + status = resp_json['properties']['completion-status'] + + if status == 'ERROR': + msg = _("HSP job %s failed.") + args = resp_json['id'] + raise exception.HSPBackendException(msg=msg % args) + elif status != target_status: + msg = _("Timeout while waiting for job %s to complete.") + args = resp_json['id'] + raise exception.HSPTimeoutException(msg=msg % args) diff --git a/manila/tests/conf_fixture.py b/manila/tests/conf_fixture.py index e8495fef1b..6124307ce2 100644 --- a/manila/tests/conf_fixture.py +++ b/manila/tests/conf_fixture.py @@ -49,6 +49,10 @@ def set_defaults(conf): _safe_set_of_opts(conf, 'zfs_share_helpers', 'NFS=foo.bar.Helper') _safe_set_of_opts(conf, 'zfs_replica_snapshot_prefix', 'foo_prefix_') + _safe_set_of_opts(conf, 'hitachi_hsp_host', '172.24.47.190') + _safe_set_of_opts(conf, 'hitachi_hsp_username', 'hsp_user') + _safe_set_of_opts(conf, 'hitachi_hsp_password', 'hsp_password') + def _safe_set_of_opts(conf, *args, **kwargs): try: diff --git a/manila/tests/share/drivers/hitachi/hsp/__init__.py b/manila/tests/share/drivers/hitachi/hsp/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/manila/tests/share/drivers/hitachi/hsp/fakes.py b/manila/tests/share/drivers/hitachi/hsp/fakes.py new file mode 100644 index 0000000000..a8b770cee8 --- /dev/null +++ b/manila/tests/share/drivers/hitachi/hsp/fakes.py @@ -0,0 +1,85 @@ +# Copyright (c) 2016 Hitachi Data Systems, 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. + +file_system = { + 'id': '33689245-1806-45d0-8507-0700b5f89750', + 'properties': { + 'cluster-id': '85d5b9e2-27f3-11e6-8b50-005056a75f66', + 'quota': 107374182400, + 'name': '07c966f9-fea2-4e12-ab72-97cb3c529bb5', + 'used-capacity': 53687091200, + 'free-capacity': 53687091200 + }, +} + +share = { + 'id': 'aa4a7710-f326-41fb-ad18-b4ad587fc87a', + 'name': 'aa4a7710-f326-41fb-ad18-b4ad587fc87a', + 'properties': { + 'file-system-id': '33689245-1806-45d0-8507-0700b5f89750', + 'file-system-name': 'fake_name', + }, +} + +invalid_share = { + 'id': 'aa4a7710-f326-41fb-ad18-b4ad587fc87a', + 'name': 'aa4a7710-f326-41fb-ad18-b4ad587fc87a', + 'size': 100, + 'host': 'hsp', + 'share_proto': 'CIFS', +} + +access_rule = { + 'id': 'acdc7172b-fe07-46c4-b78f-df3e0324ccd0', + 'access_type': 'ip', + 'access_to': '172.24.44.200', + 'access_level': 'rw', +} + +hsp_rules = [{ + 'name': 'qa_access', + 'host-specification': '172.24.44.200', + 'read-write': 'true', +}] + +hsp_cluster = { + 'id': '835e7c00-9d04-11e5-a935-f4521480e990', + 'properties': { + 'total-storage-capacity': 107374182400, + 'total-storage-used': 53687091200, + 'total-storage-available': 53687091200, + 'total-file-system-capacity': 107374182400, + 'total-file-system-space-used': 53687091200, + 'total-file-system-space-available': 53687091200 + }, +} + +stats_data = { + 'share_backend_name': 'HSP', + 'vendor_name': 'Hitachi', + 'driver_version': '1.0.0', + 'storage_protocol': 'NFS', + 'pools': [{ + 'reserved_percentage': 0, + 'pool_name': 'HSP', + 'thin_provisioning': True, + 'total_capacity_gb': 100, + 'free_capacity_gb': 50, + 'max_over_subscription_ratio': 20, + 'qos': False, + 'dedupe': False, + 'compression': False, + }], +} diff --git a/manila/tests/share/drivers/hitachi/hsp/test_driver.py b/manila/tests/share/drivers/hitachi/hsp/test_driver.py new file mode 100644 index 0000000000..4f832df5a9 --- /dev/null +++ b/manila/tests/share/drivers/hitachi/hsp/test_driver.py @@ -0,0 +1,415 @@ +# Copyright (c) 2016 Hitachi Data Systems, 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 exception +import manila.share.configuration +import manila.share.driver +from manila.share.drivers.hitachi.hsp import driver +from manila.share.drivers.hitachi.hsp import rest +from manila import test +from manila.tests import fake_share +from manila.tests.share.drivers.hitachi.hsp import fakes + +from manila.common import constants +from oslo_utils import units + +CONF = cfg.CONF + + +@ddt.ddt +class HitachiHSPTestCase(test.TestCase): + def setUp(self): + super(HitachiHSPTestCase, self).setUp() + CONF.set_default('driver_handles_share_servers', False) + CONF.hitachi_hsp_host = '172.24.47.190' + CONF.hitachi_hsp_username = 'hsp_user' + CONF.hitachi_hsp_password = 'hsp_password' + CONF.hitachi_hsp_job_timeout = 300 + + self.fake_el = [{ + "path": CONF.hitachi_hsp_host + ":/fakeinstanceid", + "metadata": {}, + "is_admin_only": False, + }] + self.fake_share = fake_share.fake_share(share_proto='nfs') + self.fake_share_instance = fake_share.fake_share_instance( + base_share=self.fake_share, export_locations=self.fake_el) + + self.fake_conf = manila.share.configuration.Configuration(None) + self.fake_private_storage = mock.Mock() + self.mock_object(rest.HSPRestBackend, "get_cluster", + mock.Mock(return_value=fakes.hsp_cluster)) + self._driver = driver.HitachiHSPDriver( + configuration=self.fake_conf, + private_storage=self.fake_private_storage) + self._driver.backend_name = "HSP" + self.mock_log = self.mock_object(driver, 'LOG') + + def test_update_access_add(self): + access = { + 'access_type': 'ip', + 'access_to': '172.24.10.10', + 'access_level': 'rw', + } + + access_list = [access] + + self.mock_object(rest.HSPRestBackend, "get_file_system", + mock.Mock(return_value=fakes.file_system)) + self.mock_object(rest.HSPRestBackend, "get_share", + mock.Mock(return_value=fakes.share)) + self.mock_object(rest.HSPRestBackend, "add_access_rule", mock.Mock()) + + self._driver.update_access('context', self.fake_share_instance, [], + access_list, []) + + self.assertTrue(self.mock_log.debug.called) + + rest.HSPRestBackend.get_file_system.assert_called_once_with( + self.fake_share_instance['id']) + rest.HSPRestBackend.get_share.assert_called_once_with( + fakes.file_system['id']) + rest.HSPRestBackend.add_access_rule.assert_called_once_with( + fakes.share['id'], access['access_to'], + (access['access_level'] == constants.ACCESS_LEVEL_RW)) + + def test_update_access_recovery(self): + access1 = { + 'access_type': 'ip', + 'access_to': '172.24.10.10', + 'access_level': 'rw', + } + access2 = { + 'access_type': 'ip', + 'access_to': '188.100.20.10', + 'access_level': 'ro', + } + + access_list = [access1, access2] + + self.mock_object(rest.HSPRestBackend, "get_file_system", + mock.Mock(return_value=fakes.file_system)) + self.mock_object(rest.HSPRestBackend, "get_share", + mock.Mock(return_value=fakes.share)) + self.mock_object(rest.HSPRestBackend, "get_access_rules", + mock.Mock(return_value=fakes.hsp_rules)) + self.mock_object(rest.HSPRestBackend, "delete_access_rule") + self.mock_object(rest.HSPRestBackend, "add_access_rule") + + self._driver.update_access('context', self.fake_share_instance, + access_list, [], []) + + self.assertTrue(self.mock_log.debug.called) + + rest.HSPRestBackend.get_file_system.assert_called_once_with( + self.fake_share_instance['id']) + rest.HSPRestBackend.get_share.assert_called_once_with( + fakes.file_system['id']) + rest.HSPRestBackend.get_access_rules.assert_called_once_with( + fakes.share['id']) + rest.HSPRestBackend.delete_access_rule.assert_called_once_with( + fakes.share['id'], fakes.hsp_rules[0]['host-specification']) + rest.HSPRestBackend.add_access_rule.assert_has_calls([ + mock.call(fakes.share['id'], access1['access_to'], True), + mock.call(fakes.share['id'], access2['access_to'], False) + ], any_order=True) + + def test_update_access_delete(self): + access1 = { + 'access_type': 'ip', + 'access_to': '172.24.10.10', + 'access_level': 'rw', + } + access2 = { + 'access_type': 'something', + 'access_to': '188.100.20.10', + 'access_level': 'ro', + } + + delete_rules = [access1, access2] + + self.mock_object(rest.HSPRestBackend, "get_file_system", + mock.Mock(return_value=fakes.file_system)) + self.mock_object(rest.HSPRestBackend, "get_share", + mock.Mock(return_value=fakes.share)) + self.mock_object(rest.HSPRestBackend, "delete_access_rule", + mock.Mock()) + + self._driver.update_access('context', self.fake_share_instance, [], [], + delete_rules) + + self.assertTrue(self.mock_log.debug.called) + + rest.HSPRestBackend.get_file_system.assert_called_once_with( + self.fake_share_instance['id']) + rest.HSPRestBackend.get_share.assert_called_once_with( + fakes.file_system['id']) + rest.HSPRestBackend.delete_access_rule.assert_called_once_with( + fakes.share['id'], delete_rules[0]['access_to']) + + @ddt.data(True, False) + def test_update_access_ip_exception(self, is_recovery): + access = { + 'access_type': 'something', + 'access_to': '172.24.10.10', + 'access_level': 'rw', + } + + access_list = [access] + + self.mock_object(rest.HSPRestBackend, "get_file_system", + mock.Mock(return_value=fakes.file_system)) + self.mock_object(rest.HSPRestBackend, "get_share", + mock.Mock(return_value=fakes.share)) + self.mock_object(rest.HSPRestBackend, "get_access_rules", + mock.Mock(return_value=fakes.hsp_rules)) + + if is_recovery: + access_args = [access_list, [], []] + else: + access_args = [[], access_list, []] + + self.assertRaises(exception.InvalidShareAccess, + self._driver.update_access, 'context', + self.fake_share_instance, *access_args) + + rest.HSPRestBackend.get_file_system.assert_called_once_with( + self.fake_share_instance['id']) + rest.HSPRestBackend.get_share.assert_called_once_with( + fakes.file_system['id']) + + if is_recovery: + rest.HSPRestBackend.get_access_rules.assert_called_once_with( + fakes.share['id']) + + def test_update_access_not_found_exception(self): + access_list = [] + + self.mock_object(rest.HSPRestBackend, "get_file_system", mock.Mock( + side_effect=exception.HSPItemNotFoundException(msg='fake'))) + + self.assertRaises(exception.ShareResourceNotFound, + self._driver.update_access, 'context', + self.fake_share_instance, access_list, [], []) + + rest.HSPRestBackend.get_file_system.assert_called_once_with( + self.fake_share_instance['id']) + + def test_create_share(self): + self.mock_object(rest.HSPRestBackend, "add_file_system", mock.Mock()) + self.mock_object(rest.HSPRestBackend, "get_file_system", + mock.Mock(return_value=fakes.file_system)) + self.mock_object(rest.HSPRestBackend, "add_share", mock.Mock()) + + result = self._driver.create_share('context', self.fake_share_instance) + + self.assertEqual(self.fake_el, result) + self.assertTrue(self.mock_log.debug.called) + + rest.HSPRestBackend.add_file_system.assert_called_once_with( + self.fake_share_instance['id'], + self.fake_share_instance['size'] * units.Gi) + rest.HSPRestBackend.get_file_system.assert_called_once_with( + self.fake_share_instance['id']) + rest.HSPRestBackend.add_share.assert_called_once_with( + self.fake_share_instance['id'], fakes.file_system['id']) + + def test_create_share_export_error(self): + self.mock_object(rest.HSPRestBackend, "add_file_system", mock.Mock()) + self.mock_object(rest.HSPRestBackend, "get_file_system", + mock.Mock(return_value=fakes.file_system)) + self.mock_object(rest.HSPRestBackend, "add_share", mock.Mock( + side_effect=exception.HSPBackendException(msg='fake'))) + self.mock_object(rest.HSPRestBackend, "delete_file_system", + mock.Mock()) + + self.assertRaises(exception.HSPBackendException, + self._driver.create_share, 'context', + self.fake_share_instance) + self.assertTrue(self.mock_log.debug.called) + self.assertTrue(self.mock_log.exception.called) + + rest.HSPRestBackend.add_file_system.assert_called_once_with( + self.fake_share_instance['id'], + self.fake_share_instance['size'] * units.Gi) + rest.HSPRestBackend.get_file_system.assert_called_once_with( + self.fake_share_instance['id']) + rest.HSPRestBackend.add_share.assert_called_once_with( + self.fake_share_instance['id'], fakes.file_system['id']) + rest.HSPRestBackend.delete_file_system.assert_called_once_with( + fakes.file_system['id']) + + def test_create_share_invalid_share_protocol(self): + self.assertRaises(exception.InvalidShare, + self._driver.create_share, 'context', + fakes.invalid_share) + + def test_delete_share(self): + self.mock_object(rest.HSPRestBackend, "get_file_system", + mock.Mock(return_value=fakes.file_system)) + self.mock_object(rest.HSPRestBackend, "get_share", + mock.Mock(return_value=fakes.share)) + self.mock_object(rest.HSPRestBackend, "delete_share", mock.Mock()) + self.mock_object(rest.HSPRestBackend, "delete_file_system", + mock.Mock()) + + self._driver.delete_share('context', self.fake_share_instance) + + self.assertTrue(self.mock_log.debug.called) + + rest.HSPRestBackend.get_file_system.assert_called_once_with( + self.fake_share_instance['id']) + rest.HSPRestBackend.get_share.assert_called_once_with( + fakes.file_system['id']) + rest.HSPRestBackend.delete_share.assert_called_once_with( + fakes.share['id']) + rest.HSPRestBackend.delete_file_system.assert_called_once_with( + fakes.file_system['id']) + + def test_delete_share_already_deleted(self): + self.mock_object(rest.HSPRestBackend, "get_file_system", mock.Mock( + side_effect=exception.HSPItemNotFoundException(msg='fake'))) + + self.mock_object(driver.LOG, "info") + + self._driver.delete_share('context', self.fake_share_instance) + + self.assertTrue(self.mock_log.info.called) + + rest.HSPRestBackend.get_file_system.assert_called_once_with( + self.fake_share_instance['id']) + + def test_extend_share(self): + new_size = 2 + + self.mock_object(rest.HSPRestBackend, "get_file_system", + mock.Mock(return_value=fakes.file_system)) + self.mock_object(rest.HSPRestBackend, "resize_file_system", + mock.Mock()) + + self._driver.extend_share(self.fake_share_instance, new_size) + + self.assertTrue(self.mock_log.info.called) + + rest.HSPRestBackend.get_cluster.assert_called_once_with() + rest.HSPRestBackend.get_file_system.assert_called_once_with( + self.fake_share_instance['id']) + rest.HSPRestBackend.resize_file_system.assert_called_once_with( + fakes.file_system['id'], new_size * units.Gi) + + def test_extend_share_with_no_available_space_in_fs(self): + new_size = 150 + + self.assertRaises(exception.HSPBackendException, + self._driver.extend_share, self.fake_share_instance, + new_size) + + rest.HSPRestBackend.get_cluster.assert_called_once_with() + + def test_shrink_share(self): + new_size = 70 + + self.mock_object(rest.HSPRestBackend, "get_file_system", + mock.Mock(return_value=fakes.file_system)) + self.mock_object(rest.HSPRestBackend, "resize_file_system", + mock.Mock()) + + self._driver.shrink_share(self.fake_share_instance, new_size) + + self.assertTrue(self.mock_log.info.called) + + rest.HSPRestBackend.get_file_system.assert_called_once_with( + self.fake_share_instance['id']) + rest.HSPRestBackend.resize_file_system.assert_called_once_with( + fakes.file_system['id'], new_size * units.Gi) + + def test_shrink_share_new_size_lower_than_usage(self): + new_size = 20 + + self.mock_object(rest.HSPRestBackend, "get_file_system", + mock.Mock(return_value=fakes.file_system)) + + self.assertRaises(exception.ShareShrinkingPossibleDataLoss, + self._driver.shrink_share, self.fake_share_instance, + new_size) + + rest.HSPRestBackend.get_file_system.assert_called_once_with( + self.fake_share_instance['id']) + + def test_manage_existing(self): + self.mock_object(self.fake_private_storage, "update") + self.mock_object(rest.HSPRestBackend, "get_share", + mock.Mock(return_value=fakes.share)) + self.mock_object(rest.HSPRestBackend, "rename_file_system", + mock.Mock()) + self.mock_object(rest.HSPRestBackend, "get_file_system", + mock.Mock(return_value=fakes.file_system)) + + result = self._driver.manage_existing(self.fake_share_instance, + 'option') + + expected = { + 'size': fakes.file_system['properties']['quota'] / units.Gi, + 'export_locations': self.fake_el, + } + + self.assertTrue(self.mock_log.info.called) + self.assertEqual(expected, result) + + rest.HSPRestBackend.get_share.assert_called_once_with( + name=self.fake_share_instance['id']) + rest.HSPRestBackend.rename_file_system.assert_called_once_with( + fakes.file_system['id'], self.fake_share_instance['id']) + rest.HSPRestBackend.get_file_system.assert_called_once_with( + self.fake_share_instance['id']) + + def test_manage_existing_wrong_share_id(self): + self.mock_object(rest.HSPRestBackend, "get_share", mock.Mock( + side_effect=exception.HSPItemNotFoundException(msg='fake'))) + + self.assertRaises(exception.ManageInvalidShare, + self._driver.manage_existing, + self.fake_share_instance, + 'option') + + rest.HSPRestBackend.get_share.assert_called_once_with( + name=self.fake_share_instance['id']) + + def test_unmanage(self): + self.mock_object(self.fake_private_storage, "get", + mock.Mock( + return_value='original_name')) + self.mock_object(self.fake_private_storage, "delete") + + self._driver.unmanage(self.fake_share_instance) + + self.assertTrue(self.mock_log.info.called) + + def test__update_share_stats(self): + mock__update_share_stats = self.mock_object( + manila.share.driver.ShareDriver, '_update_share_stats') + self.mock_object(self.fake_private_storage, 'get', mock.Mock( + return_value={'provisioned': 0} + )) + + self._driver._update_share_stats() + + rest.HSPRestBackend.get_cluster.assert_called_once_with() + mock__update_share_stats.assert_called_once_with(fakes.stats_data) + self.assertTrue(self.mock_log.info.called) diff --git a/manila/tests/share/drivers/hitachi/hsp/test_rest.py b/manila/tests/share/drivers/hitachi/hsp/test_rest.py new file mode 100644 index 0000000000..a8b02d11d5 --- /dev/null +++ b/manila/tests/share/drivers/hitachi/hsp/test_rest.py @@ -0,0 +1,344 @@ +# Copyright (c) 2016 Hitachi Data Systems, 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 json +import mock +import requests +import time + +from manila import exception +from manila.share.drivers.hitachi.hsp import rest +from manila import test +from manila.tests.share.drivers.hitachi.hsp import fakes + + +class FakeRequests(object): + status_code = 0 + headers = {} + content = "" + + def __init__(self, status_code, content='null'): + self.status_code = status_code + self.headers = {'location': 'fake_location'} + self.content = content + + def json(self): + return {'messages': [{'message': 'fake_msg'}]} + + +@ddt.ddt +class HitachiHSPRestTestCase(test.TestCase): + def setUp(self): + super(HitachiHSPRestTestCase, self).setUp() + self.hitachi_hsp_host = '172.24.47.190' + self.hitachi_hsp_username = 'hds_hnas_user' + self.hitachi_hsp_password = 'hds_hnas_password' + + self._driver = rest.HSPRestBackend(self.hitachi_hsp_host, + self.hitachi_hsp_username, + self.hitachi_hsp_password) + + @ddt.data(202, 500) + def test__send_post(self, code): + self.mock_object(requests, "post", mock.Mock( + return_value=FakeRequests(code))) + + if code == 202: + self.mock_object(rest.HSPRestBackend, "_wait_job_status", + mock.Mock()) + self._driver._send_post('fake_url') + + rest.HSPRestBackend._wait_job_status.assert_called_once_with( + 'fake_location', 'COMPLETE') + else: + self.assertRaises(exception.HSPBackendException, + self._driver._send_post, 'fake_url') + + @ddt.data({'code': 200, 'content': 'null'}, + {'code': 200, 'content': 'fake_content'}, + {'code': 500, 'content': 'null'}) + @ddt.unpack + def test__send_get(self, code, content): + self.mock_object(requests, "get", mock.Mock( + return_value=FakeRequests(code, content))) + + if code == 200: + result = self._driver._send_get('fake_url') + if content == 'null': + self.assertIsNone(result) + else: + self.assertEqual(FakeRequests(code, content).json(), result) + else: + self.assertRaises(exception.HSPBackendException, + self._driver._send_get, 'fake_url') + + @ddt.data(202, 500) + def test__send_delete(self, code): + self.mock_object(requests, "delete", mock.Mock( + return_value=FakeRequests(code))) + + if code == 202: + self.mock_object(rest.HSPRestBackend, "_wait_job_status", + mock.Mock()) + self._driver._send_delete('fake_url') + + rest.HSPRestBackend._wait_job_status.assert_called_once_with( + 'fake_location', 'COMPLETE') + else: + self.assertRaises(exception.HSPBackendException, + self._driver._send_delete, 'fake_url') + + def test_add_file_system(self): + url = "https://172.24.47.190/hspapi/file-systems/" + + payload = { + 'quota': fakes.file_system['properties']['quota'], + 'auto-access': False, + 'enabled': True, + 'description': '', + 'record-access-time': True, + 'tags': '', + 'space-hwm': 90, + 'space-lwm': 70, + 'name': fakes.file_system['properties']['name'], + } + + self.mock_object(rest.HSPRestBackend, "_send_post", mock.Mock()) + self._driver.add_file_system(fakes.file_system['properties']['name'], + fakes.file_system['properties']['quota']) + + rest.HSPRestBackend._send_post.assert_called_once_with( + url, payload=json.dumps(payload)) + + def test_get_file_system(self): + url = ("https://172.24.47.190/hspapi/file-systems/list?name=%s" % + fakes.file_system['properties']['name']) + + self.mock_object(rest.HSPRestBackend, "_send_get", mock.Mock( + return_value={'list': [fakes.file_system]})) + + result = self._driver.get_file_system( + fakes.file_system['properties']['name']) + + self.assertEqual(fakes.file_system, result) + + rest.HSPRestBackend._send_get.assert_called_once_with(url) + + def test_get_file_system_exception(self): + url = ("https://172.24.47.190/hspapi/file-systems/list?name=%s" % + fakes.file_system['properties']['name']) + + self.mock_object(rest.HSPRestBackend, "_send_get", + mock.Mock(return_value=None)) + + self.assertRaises(exception.HSPItemNotFoundException, + self._driver.get_file_system, + fakes.file_system['properties']['name']) + + rest.HSPRestBackend._send_get.assert_called_once_with(url) + + def test_delete_file_system(self): + url = ("https://172.24.47.190/hspapi/file-systems/%s" % + fakes.file_system['id']) + + self.mock_object(rest.HSPRestBackend, "_send_delete", mock.Mock()) + self._driver.delete_file_system(fakes.file_system['id']) + + rest.HSPRestBackend._send_delete.assert_called_once_with(url) + + def test_resize_file_system(self): + url = ("https://172.24.47.190/hspapi/file-systems/%s" % + fakes.file_system['id']) + new_size = 53687091200 + payload = {'quota': new_size} + + self.mock_object(rest.HSPRestBackend, "_send_post", mock.Mock()) + self._driver.resize_file_system(fakes.file_system['id'], new_size) + + rest.HSPRestBackend._send_post.assert_called_once_with( + url, payload=json.dumps(payload)) + + def test_rename_file_system(self): + url = ("https://172.24.47.190/hspapi/file-systems/%s" % + fakes.file_system['id']) + new_name = "fs_rename" + payload = {'name': new_name} + + self.mock_object(rest.HSPRestBackend, "_send_post", mock.Mock()) + + self._driver.rename_file_system(fakes.file_system['id'], new_name) + + rest.HSPRestBackend._send_post.assert_called_once_with( + url, payload=json.dumps(payload)) + + def test_add_share(self): + url = "https://172.24.47.190/hspapi/shares/" + payload = { + 'description': '', + 'type': 'NFS', + 'enabled': True, + 'tags': '', + 'name': fakes.share['name'], + 'file-system-id': fakes.share['properties']['file-system-id'], + } + + self.mock_object(rest.HSPRestBackend, "_send_post", mock.Mock()) + + self._driver.add_share(fakes.share['name'], + fakes.share['properties']['file-system-id']) + + rest.HSPRestBackend._send_post.assert_called_once_with( + url, payload=json.dumps(payload)) + + @ddt.data({'fs_id': None, + 'name': fakes.share['name'], + 'url': 'https://172.24.47.190/hspapi/shares/list?' + 'name=aa4a7710-f326-41fb-ad18-b4ad587fc87a'}, + {'fs_id': fakes.share['properties']['file-system-id'], + 'name': None, + 'url': 'https://172.24.47.190/hspapi/shares/list?' + 'file-system-id=33689245-1806-45d0-8507-0700b5f89750'}) + @ddt.unpack + def test_get_share(self, fs_id, name, url): + self.mock_object(rest.HSPRestBackend, "_send_get", + mock.Mock(return_value={'list': [fakes.share]})) + + result = self._driver.get_share(fs_id, name) + + self.assertEqual(fakes.share, result) + + rest.HSPRestBackend._send_get.assert_called_once_with(url) + + def test_get_share_exception(self): + url = ("https://172.24.47.190/hspapi/shares/list?" + "name=aa4a7710-f326-41fb-ad18-b4ad587fc87a") + + self.mock_object(rest.HSPRestBackend, "_send_get", mock.Mock( + return_value=None)) + + self.assertRaises(exception.HSPItemNotFoundException, + self._driver.get_share, None, fakes.share['name']) + + rest.HSPRestBackend._send_get.assert_called_once_with(url) + + def test_delete_share(self): + url = "https://172.24.47.190/hspapi/shares/%s" % fakes.share['id'] + + self.mock_object(rest.HSPRestBackend, "_send_delete", mock.Mock()) + + self._driver.delete_share(fakes.share['id']) + + rest.HSPRestBackend._send_delete.assert_called_once_with(url) + + def test_add_access_rule(self): + url = "https://172.24.47.190/hspapi/shares/%s/" % fakes.share['id'] + payload = { + "action": "add-access-rule", + "name": fakes.share['id'] + fakes.access_rule['access_to'], + "host-specification": fakes.access_rule['access_to'], + "read-write": fakes.access_rule['access_level'], + } + + self.mock_object(rest.HSPRestBackend, "_send_post", mock.Mock()) + + self._driver.add_access_rule(fakes.share['id'], + fakes.access_rule['access_to'], + fakes.access_rule['access_level']) + + rest.HSPRestBackend._send_post.assert_called_once_with( + url, payload=json.dumps(payload)) + + def test_delete_access_rule(self): + url = "https://172.24.47.190/hspapi/shares/%s/" % fakes.share['id'] + payload = { + "action": "delete-access-rule", + "name": fakes.share['id'] + fakes.access_rule['access_to'], + } + + self.mock_object(rest.HSPRestBackend, "_send_post", mock.Mock()) + + self._driver.delete_access_rule(fakes.share['id'], + fakes.access_rule['access_to']) + + rest.HSPRestBackend._send_post.assert_called_once_with( + url, payload=json.dumps(payload)) + + @ddt.data({'value': {'list': fakes.hsp_rules}, 'res': fakes.hsp_rules}, + {'value': None, 'res': []}) + @ddt.unpack + def test_get_access_rules(self, value, res): + url = ("https://172.24.47.190/hspapi/shares/%s/access-rules" % + fakes.share['id']) + + self.mock_object(rest.HSPRestBackend, "_send_get", mock.Mock( + return_value=value)) + + result = self._driver.get_access_rules(fakes.share['id']) + + self.assertEqual(res, result) + + rest.HSPRestBackend._send_get.assert_called_once_with(url) + + @ddt.data({'list': [fakes.hsp_cluster]}, None) + def test_get_clusters(self, value): + url = "https://172.24.47.190/hspapi/clusters/list" + + self.mock_object(rest.HSPRestBackend, "_send_get", mock.Mock( + return_value=value)) + + if value: + result = self._driver.get_cluster() + + self.assertEqual(fakes.hsp_cluster, result) + else: + self.assertRaises(exception.HSPBackendException, + self._driver.get_cluster) + + rest.HSPRestBackend._send_get.assert_called_once_with(url) + + @ddt.data('COMPLETE', 'ERROR', 'RUNNING') + def test__wait_job_status(self, stat): + url = "fake_job_url" + json = { + 'id': 'fake_id', + 'properties': {'completion-status': stat}, + 'messages': [{ + 'id': 'fake_id', + 'message': 'fake_msg' + }] + } + + self.mock_object(rest.HSPRestBackend, "_send_get", mock.Mock( + return_value=json)) + self.mock_object(time, "sleep") + + if stat == 'COMPLETE': + self._driver._wait_job_status(url, 'COMPLETE') + + rest.HSPRestBackend._send_get.assert_called_once_with(url) + elif stat == 'ERROR': + self.assertRaises(exception.HSPBackendException, + self._driver._wait_job_status, url, 'COMPLETE') + + rest.HSPRestBackend._send_get.assert_called_once_with(url) + else: + self.assertRaises(exception.HSPTimeoutException, + self._driver._wait_job_status, url, 'COMPLETE') + + rest.HSPRestBackend._send_get.assert_has_calls([ + mock.call(url), mock.call(url), mock.call(url), mock.call(url), + mock.call(url), + ]) diff --git a/releasenotes/notes/hsp-driver-e00aff5bc89d4b54.yaml b/releasenotes/notes/hsp-driver-e00aff5bc89d4b54.yaml new file mode 100644 index 0000000000..70061e893b --- /dev/null +++ b/releasenotes/notes/hsp-driver-e00aff5bc89d4b54.yaml @@ -0,0 +1,7 @@ +--- +prelude: > + Add Hitachi HSP driver. +features: + - Added new Hitachi HSP driver, that supports manage/unmanage and + shrinking of shares, along with all the minimum driver features. Does + not support snapshots. \ No newline at end of file