]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add Oracle ZFSSA NFS Cinder Driver Support
authorkedar-vidvans <kedar.vidvans@oracle.com>
Tue, 9 Dec 2014 19:48:08 +0000 (14:48 -0500)
committerkedar-vidvans <kedar.vidvans@oracle.com>
Thu, 18 Dec 2014 14:28:25 +0000 (09:28 -0500)
This change will add a new Oracle ZFSSA NFS Driver to Cinder.
This driver supports all minimum features required by a driver for the kilo release.
This driver uses the base nfs driver methods for creating and deleting volumes.

Certification test results: https://bugs.launchpad.net/cinder/+bug/1400406

Change-Id: Id61fe3319172c8ff19ed421ead618e583a8d9263
Implements: blueprint oracle-zfssa-nfs-cinder-driver

cinder/exception.py
cinder/tests/test_zfssa.py
cinder/volume/drivers/zfssa/webdavclient.py [new file with mode: 0644]
cinder/volume/drivers/zfssa/zfssanfs.py [new file with mode: 0644]
cinder/volume/drivers/zfssa/zfssarest.py

index 3373752d7d43c85ffe994aa6aed4d808cae17152..6c9273af29890dc3fdb772c984a311ee1b2ffdf1 100755 (executable)
@@ -933,3 +933,10 @@ class ViolinBackendErrExists(CinderException):
 
 class ViolinBackendErrNotFound(CinderException):
     message = _("Backend reports: item not found")
+
+
+# ZFSSA NFS driver exception.
+class WebDAVClientError(CinderException):
+        message = _("The WebDAV request failed. Reason: %(msg)s, "
+                    "Return code/reason: %(code)s, Source Volume: %(src)s, "
+                    "Destination Volume: %(dst)s, Method: %(method)s.")
index 8021527ee1b6fd9487679508cc86c58b77f1a47a..9ce5e95bca7018af752d987c7dc099e3b3d7eed7 100644 (file)
@@ -26,11 +26,15 @@ from cinder import test
 from cinder.volume import configuration as conf
 from cinder.volume.drivers.zfssa import restclient as client
 from cinder.volume.drivers.zfssa import zfssaiscsi as iscsi
+from cinder.volume.drivers.zfssa import zfssanfs
 from cinder.volume.drivers.zfssa import zfssarest as rest
 
 
 LOG = logging.getLogger(__name__)
 
+nfs_logbias = 'latency'
+nfs_compression = 'off'
+
 
 class FakeZFSSA(object):
     """Fake ZFS SA"""
@@ -216,6 +220,83 @@ class FakeZFSSA(object):
         return ret
 
 
+class FakeNFSZFSSA(FakeZFSSA):
+    """Fake ZFS SA for the NFS Driver
+    """
+    def set_webdav(self, https_path, auth_str):
+        self.webdavclient = https_path
+
+    def create_share(self, pool, project, share, args):
+        out = {}
+        if not self.host and not self.user:
+            return out
+
+        out = {"logbias": nfs_logbias,
+               "compression": nfs_compression,
+               "status": "online",
+               "pool": pool,
+               "name": share,
+               "project": project,
+               "mountpoint": '/export/nfs_share'}
+
+        return out
+
+    def get_share(self, pool, project, share):
+        out = {}
+        if not self.host and not self.user:
+            return out
+
+        out = {"logbias": nfs_logbias,
+               "compression": nfs_compression,
+               "encryption": "off",
+               "status": "online",
+               "pool": pool,
+               "name": share,
+               "project": project,
+               "mountpoint": '/export/nfs_share'}
+
+        return out
+
+    def create_snapshot_of_volume_file(self, src_file="", dst_file=""):
+        out = {}
+        if not self.host and not self.user:
+            return out
+        out = {"status": 201}
+
+        return out
+
+    def delete_snapshot_of_volume_file(self, src_file=""):
+        out = {}
+        if not self.host and not self.user:
+            return out
+        out = {"status": 204}
+
+        return out
+
+    def create_volume_from_snapshot_file(self, src_file="", dst_file="",
+                                         method='COPY'):
+        out = {}
+        if not self.host and not self.user:
+            return out
+        out = {"status": 202}
+
+        return out
+
+    def modify_service(self, service, args):
+        out = {}
+        if not self.host and not self.user:
+            return out
+        out = {"service": {"<status>": "online"}}
+        return out
+
+    def enable_service(self, service):
+        out = {}
+        if not self.host and not self.user:
+            return out
+        out = {"service": {"<status>": "online"}}
+        return out
+
+
 class TestZFSSAISCSIDriver(test.TestCase):
 
     test_vol = {
@@ -360,3 +441,75 @@ class FakeAddIni2InitGrp(object):
         result = client.RestResult()
         result.status = client.Status.CREATED
         return result
+
+
+class TestZFSSANFSDriver(test.TestCase):
+
+    test_vol = {
+        'name': 'test-vol',
+        'size': 1
+    }
+
+    test_snap = {
+        'name': 'cindersnap',
+        'volume_name': test_vol['name'],
+        'volume_size': test_vol['size']
+    }
+
+    test_vol_snap = {
+        'name': 'cindersnapvol',
+        'size': test_vol['size']
+    }
+
+    def __init__(self, method):
+        super(TestZFSSANFSDriver, self).__init__(method)
+
+    @mock.patch.object(zfssanfs, 'factory_zfssa')
+    def setUp(self, _factory_zfssa):
+        super(TestZFSSANFSDriver, self).setUp()
+        self._create_fake_config()
+        _factory_zfssa.return_value = FakeNFSZFSSA()
+        self.drv = zfssanfs.ZFSSANFSDriver(configuration=self.configuration)
+        self.drv.do_setup({})
+
+    def _create_fake_config(self):
+        self.configuration = mock.Mock(spec=conf.Configuration)
+        self.configuration.san_ip = '1.1.1.1'
+        self.configuration.san_login = 'user'
+        self.configuration.san_password = 'passwd'
+        self.configuration.zfssa_data_ip = '2.2.2.2'
+        self.configuration.zfssa_https_port = '443'
+        self.configuration.zfssa_nfs_pool = 'pool'
+        self.configuration.zfssa_nfs_project = 'nfs_project'
+        self.configuration.zfssa_nfs_share = 'nfs_share'
+        self.configuration.zfssa_nfs_share_logbias = nfs_logbias
+        self.configuration.zfssa_nfs_share_compression = nfs_compression
+        self.configuration.zfssa_nfs_mount_options = ''
+        self.configuration.zfssa_rest_timeout = '30'
+        self.configuration.nfs_oversub_ratio = 1
+        self.configuration.nfs_used_ratio = 1
+
+    def test_create_delete_snapshot(self):
+        self.drv.create_snapshot(self.test_snap)
+        self.drv.delete_snapshot(self.test_snap)
+
+    def test_create_volume_from_snapshot(self):
+        self.drv.create_snapshot(self.test_snap)
+        with mock.patch.object(self.drv, '_ensure_shares_mounted'):
+            prov_loc = self.drv.create_volume_from_snapshot(self.test_vol_snap,
+                                                            self.test_snap,
+                                                            method='COPY')
+        self.assertEqual('2.2.2.2:/export/nfs_share',
+                         prov_loc['provider_location'])
+
+    def test_get_volume_stats(self):
+        self.drv._mounted_shares = ['nfs_share']
+        with mock.patch.object(self.drv, '_ensure_shares_mounted'):
+            with mock.patch.object(self.drv, '_get_share_capacity_info') as \
+                    mock_get_share_capacity_info:
+                mock_get_share_capacity_info.return_value = (1073741824,
+                                                             9663676416)
+                self.drv.get_volume_stats(refresh=True)
+
+    def tearDown(self):
+        super(TestZFSSANFSDriver, self).tearDown()
diff --git a/cinder/volume/drivers/zfssa/webdavclient.py b/cinder/volume/drivers/zfssa/webdavclient.py
new file mode 100644 (file)
index 0000000..9ca5fea
--- /dev/null
@@ -0,0 +1,132 @@
+# Copyright (c) 2014, Oracle and/or its affiliates. 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.
+"""
+ZFS Storage Appliance WebDAV Client
+"""
+
+import httplib
+import time
+import urllib2
+
+from cinder import exception
+from cinder.i18n import _, _LE
+from cinder.openstack.common import log
+
+LOG = log.getLogger(__name__)
+
+bad_gateway_err = _('Check the state of the http service. Also ensure that '
+                    'the https port number is the same as the one specified '
+                    'in cinder.conf.')
+
+WebDAVHTTPErrors = {
+    httplib.UNAUTHORIZED: _('User not authorized to perform WebDAV '
+                            'operations.'),
+    httplib.BAD_GATEWAY: bad_gateway_err,
+    httplib.FORBIDDEN: _('Check access permissions for the ZFS share assigned '
+                         'to this driver.'),
+    httplib.NOT_FOUND: _('The source volume for this WebDAV operation not '
+                         'found.'),
+    httplib.INSUFFICIENT_STORAGE: _('Not enough storage space in the ZFS '
+                                    'share to perform this operation.')
+}
+
+WebDAVErrors = {
+    'BadStatusLine': _('http service may have been abruptly disabled or put '
+                       'to maintenance state in the middle of this '
+                       'operation.'),
+    'Bad_Gateway': bad_gateway_err
+}
+
+
+class ZFSSAWebDAVClient(object):
+    def __init__(self, url, auth_str, **kwargs):
+        """Initialize WebDAV Client"""
+        self.https_path = url
+        self.auth_str = auth_str
+
+    def _lookup_error(self, error):
+        msg = ''
+        if error in httplib.responses:
+            msg = httplib.responses[error]
+
+        if error in WebDAVHTTPErrors:
+            msg = WebDAVHTTPErrors[error]
+        elif error in WebDAVErrors:
+            msg = WebDAVErrors[error]
+
+        return msg
+
+    def request(self, src_file="", dst_file="", method="", maxretries=10):
+        retry = 0
+        src_url = self.https_path + "/" + src_file
+        dst_url = self.https_path + "/" + dst_file
+        request = urllib2.Request(src_url)
+
+        if dst_file != "":
+            request.add_header('Destination', dst_url)
+
+        request.add_header("Authorization", "Basic %s" % self.auth_str)
+
+        request.get_method = lambda: method
+
+        LOG.debug('Sending WebDAV request:%s %s %s' % (method, src_url,
+                  dst_url))
+
+        while retry < maxretries:
+            try:
+                response = urllib2.urlopen(request, timeout=None)
+            except urllib2.HTTPError as err:
+                LOG.error(_LE('WebDAV returned with %(code)s error during '
+                              '%(method)s call.')
+                          % {'code': err.code,
+                             'method': method})
+
+                if err.code == httplib.INTERNAL_SERVER_ERROR:
+                    exception_msg = (_('WebDAV operation failed with '
+                                       'error code: %(code)s '
+                                       'reason: %(reason)s '
+                                       'Retry attempt %(retry)s in progress.')
+                                     % {'code': err.code,
+                                        'reason': err.reason,
+                                        'retry': retry})
+                    LOG.error(exception_msg)
+                    if retry < maxretries:
+                        retry += 1
+                        time.sleep(1)
+                        continue
+
+                msg = self._lookup_error(err.code)
+                raise exception.WebDAVClientError(msg=msg, code=err.code,
+                                                  src=src_file, dst=dst_file,
+                                                  method=method)
+
+            except httplib.BadStatusLine as err:
+                msg = self._lookup_error('BadStatusLine')
+                raise exception.WebDAVClientError(msg=msg,
+                                                  code='httplib.BadStatusLine',
+                                                  src=src_file, dst=dst_file,
+                                                  method=method)
+
+            except urllib2.URLError as err:
+                reason = ''
+                if getattr(err, 'reason'):
+                    reason = err.reason
+
+                msg = self._lookup_error('Bad_Gateway')
+                raise exception.WebDAVClientError(msg=msg,
+                                                  code=reason, src=src_file,
+                                                  dst=dst_file, method=method)
+
+            break
+        return response
diff --git a/cinder/volume/drivers/zfssa/zfssanfs.py b/cinder/volume/drivers/zfssa/zfssanfs.py
new file mode 100644 (file)
index 0000000..0c8266b
--- /dev/null
@@ -0,0 +1,301 @@
+# Copyright (c) 2014, Oracle and/or its affiliates. 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.
+"""
+ZFS Storage Appliance NFS Cinder Volume Driver
+"""
+import base64
+from datetime import datetime
+import errno
+
+from oslo.config import cfg
+from oslo.utils import excutils
+from oslo.utils import units
+
+from cinder import exception
+from cinder.i18n import _, _LE, _LI
+from cinder.openstack.common import log
+from cinder.volume.drivers import nfs
+from cinder.volume.drivers.san import san
+from cinder.volume.drivers.zfssa import zfssarest
+
+
+ZFSSA_OPTS = [
+    cfg.StrOpt('zfssa_data_ip',
+               help='Data path IP address'),
+    cfg.StrOpt('zfssa_https_port', default='443',
+               help='HTTPS port number'),
+    cfg.StrOpt('zfssa_nfs_mount_options', default='',
+               help='Options to be passed while mounting share over nfs'),
+    cfg.StrOpt('zfssa_nfs_pool', default='',
+               help='Storage pool name.'),
+    cfg.StrOpt('zfssa_nfs_project', default='NFSProject',
+               help='Project name.'),
+    cfg.StrOpt('zfssa_nfs_share', default='nfs_share',
+               help='Share name.'),
+    cfg.StrOpt('zfssa_nfs_share_compression', default='off',
+               help='Data compression-off, lzjb, gzip-2, gzip, gzip-9.'),
+    cfg.StrOpt('zfssa_nfs_share_logbias', default='latency',
+               help='Synchronous write bias-latency, throughput.'),
+    cfg.IntOpt('zfssa_rest_timeout',
+               help='REST connection timeout. (seconds)')
+]
+
+LOG = log.getLogger(__name__)
+
+CONF = cfg.CONF
+CONF.register_opts(ZFSSA_OPTS)
+
+
+def factory_zfssa():
+    return zfssarest.ZFSSANfsApi()
+
+
+class ZFSSANFSDriver(nfs.NfsDriver):
+    VERSION = '1.0.0'
+    volume_backend_name = 'ZFSSA_NFS'
+    protocol = driver_prefix = driver_volume_type = 'nfs'
+
+    def __init__(self, *args, **kwargs):
+        super(ZFSSANFSDriver, self).__init__(*args, **kwargs)
+        self.configuration.append_config_values(ZFSSA_OPTS)
+        self.configuration.append_config_values(san.san_opts)
+        self.zfssa = None
+        self._stats = None
+
+    def do_setup(self, context):
+        if not self.configuration.nfs_oversub_ratio > 0:
+            msg = _("NFS config 'nfs_oversub_ratio' invalid. Must be > 0: "
+                    "%s") % self.configuration.nfs_oversub_ratio
+            LOG.error(msg)
+            raise exception.NfsException(msg)
+
+        if ((not self.configuration.nfs_used_ratio > 0) and
+                (self.configuration.nfs_used_ratio <= 1)):
+            msg = _("NFS config 'nfs_used_ratio' invalid. Must be > 0 "
+                    "and <= 1.0: %s") % self.configuration.nfs_used_ratio
+            LOG.error(msg)
+            raise exception.NfsException(msg)
+
+        package = 'mount.nfs'
+        try:
+            self._execute(package, check_exit_code=False, run_as_root=True)
+        except OSError as exc:
+            if exc.errno == errno.ENOENT:
+                msg = _('%s is not installed') % package
+                raise exception.NfsException(msg)
+            else:
+                raise exc
+
+        lcfg = self.configuration
+        LOG.info(_LI('Connecting to host: %s.'), lcfg.san_ip)
+
+        host = lcfg.san_ip
+        user = lcfg.san_login
+        password = lcfg.san_password
+        https_port = lcfg.zfssa_https_port
+
+        credentials = ['san_ip', 'san_login', 'san_password', 'zfssa_data_ip']
+
+        for cred in credentials:
+            if not getattr(lcfg, cred, None):
+                exception_msg = _('%s not set in cinder.conf') % cred
+                LOG.error(exception_msg)
+                raise exception.CinderException(exception_msg)
+
+        self.zfssa = factory_zfssa()
+        self.zfssa.set_host(host, timeout=lcfg.zfssa_rest_timeout)
+
+        auth_str = base64.encodestring('%s:%s' % (user, password))[:-1]
+        self.zfssa.login(auth_str)
+
+        self.zfssa.create_project(lcfg.zfssa_nfs_pool, lcfg.zfssa_nfs_project,
+                                  compression=lcfg.zfssa_nfs_share_compression,
+                                  logbias=lcfg.zfssa_nfs_share_logbias)
+
+        share_args = {
+            'sharedav': 'rw',
+            'sharenfs': 'rw',
+            'root_permissions': '777',
+            'compression': lcfg.zfssa_nfs_share_compression,
+            'logbias': lcfg.zfssa_nfs_share_logbias
+        }
+
+        self.zfssa.create_share(lcfg.zfssa_nfs_pool, lcfg.zfssa_nfs_project,
+                                lcfg.zfssa_nfs_share, share_args)
+
+        share_details = self.zfssa.get_share(lcfg.zfssa_nfs_pool,
+                                             lcfg.zfssa_nfs_project,
+                                             lcfg.zfssa_nfs_share)
+
+        mountpoint = share_details['mountpoint']
+
+        self.mount_path = lcfg.zfssa_data_ip + ':' + mountpoint
+        https_path = 'https://' + lcfg.zfssa_data_ip + ':' + https_port + \
+            '/shares' + mountpoint
+
+        LOG.debug('NFS mount path: %s' % self.mount_path)
+        LOG.debug('WebDAV path to the share: %s' % https_path)
+
+        self.shares = {}
+        mnt_opts = self.configuration.zfssa_nfs_mount_options
+        self.shares[self.mount_path] = mnt_opts if len(mnt_opts) > 1 else None
+
+        # Initialize the WebDAV client
+        self.zfssa.set_webdav(https_path, auth_str)
+
+        # Edit http service so that WebDAV requests are always authenticated
+        args = {'https_port': https_port,
+                'require_login': True}
+
+        self.zfssa.modify_service('http', args)
+        self.zfssa.enable_service('http')
+
+    def _ensure_shares_mounted(self):
+        try:
+            self._ensure_share_mounted(self.mount_path)
+        except Exception as exc:
+            LOG.error(_LE('Exception during mounting %s.') % exc)
+
+        self._mounted_shares = [self.mount_path]
+        LOG.debug('Available shares %s' % self._mounted_shares)
+
+    def check_for_setup_error(self):
+        """Check that driver can login.
+
+        Check also for properly configured pool, project and share
+        Check that the http and nfs services are enabled
+        """
+        lcfg = self.configuration
+
+        self.zfssa.verify_pool(lcfg.zfssa_nfs_pool)
+        self.zfssa.verify_project(lcfg.zfssa_nfs_pool, lcfg.zfssa_nfs_project)
+        self.zfssa.verify_share(lcfg.zfssa_nfs_pool, lcfg.zfssa_nfs_project,
+                                lcfg.zfssa_nfs_share)
+        self.zfssa.verify_service('http')
+        self.zfssa.verify_service('nfs')
+
+    def create_snapshot(self, snapshot):
+        """Creates a snapshot of a volume."""
+        LOG.info(_LI('Creating snapshot: %s'), snapshot['name'])
+        lcfg = self.configuration
+        snap_name = self._create_snapshot_name()
+        self.zfssa.create_snapshot(lcfg.zfssa_nfs_pool, lcfg.zfssa_nfs_project,
+                                   lcfg.zfssa_nfs_share, snap_name)
+
+        src_file = snap_name + '/' + snapshot['volume_name']
+
+        try:
+            self.zfssa.create_snapshot_of_volume_file(src_file=src_file,
+                                                      dst_file=
+                                                      snapshot['name'])
+        except Exception:
+            with excutils.save_and_reraise_exception():
+                LOG.debug('Error thrown during snapshot: %s creation' %
+                          snapshot['name'])
+        finally:
+            self.zfssa.delete_snapshot(lcfg.zfssa_nfs_pool,
+                                       lcfg.zfssa_nfs_project,
+                                       lcfg.zfssa_nfs_share, snap_name)
+
+    def delete_snapshot(self, snapshot):
+        """Deletes a snapshot."""
+        LOG.info(_LI('Deleting snapshot: %s'), snapshot['name'])
+        self.zfssa.delete_snapshot_of_volume_file(src_file=snapshot['name'])
+
+    def create_volume_from_snapshot(self, volume, snapshot, method='COPY'):
+        LOG.info(_LI('Creatng volume from snapshot. volume: %s'),
+                 volume['name'])
+        LOG.info(_LI('Source Snapshot: %s'), snapshot['name'])
+
+        self._ensure_shares_mounted()
+        self.zfssa.create_volume_from_snapshot_file(src_file=snapshot['name'],
+                                                    dst_file=volume['name'],
+                                                    method=method)
+
+        volume['provider_location'] = self.mount_path
+
+        if volume['size'] != snapshot['volume_size']:
+            try:
+                self.extend_volume(volume, volume['size'])
+            except Exception:
+                vol_path = self.local_path(volume)
+                exception_msg = (_('Error in extending volume size: '
+                                   'Volume: %(volume)s '
+                                   'Vol_Size: %(vol_size)d with '
+                                   'Snapshot: %(snapshot)s '
+                                   'Snap_Size: %(snap_size)d')
+                                 % {'volume': volume['name'],
+                                    'vol_size': volume['size'],
+                                    'snapshot': snapshot['name'],
+                                    'snap_size': snapshot['volume_size']})
+                with excutils.save_and_reraise_exception():
+                    LOG.error(exception_msg)
+                    self._execute('rm', '-f', vol_path, run_as_root=True)
+
+        return {'provider_location': volume['provider_location']}
+
+    def create_cloned_volume(self, volume, src_vref):
+        """Creates a snapshot and then clones the snapshot into a volume."""
+        LOG.info(_LI('new cloned volume: %s'), volume['name'])
+        LOG.info(_LI('source volume for cloning: %s'), src_vref['name'])
+
+        snapshot = {'volume_name': src_vref['name'],
+                    'volume_id': src_vref['id'],
+                    'volume_size': src_vref['size'],
+                    'name': self._create_snapshot_name()}
+
+        self.create_snapshot(snapshot)
+        return self.create_volume_from_snapshot(volume, snapshot,
+                                                method='MOVE')
+
+    def _create_snapshot_name(self):
+        """Creates a snapshot name from the date and time."""
+        return 'cinder-zfssa-nfs-snapshot-%s' % datetime.now().isoformat()
+
+    def _get_share_capacity_info(self):
+        """Get available and used capacity info for the NFS share."""
+        lcfg = self.configuration
+        share_details = self.zfssa.get_share(lcfg.zfssa_nfs_pool,
+                                             lcfg.zfssa_nfs_project,
+                                             lcfg.zfssa_nfs_share)
+
+        free = share_details['space_available']
+        used = share_details['space_total']
+        return free, used
+
+    def _update_volume_stats(self):
+        """Get volume stats from zfssa"""
+        self._ensure_shares_mounted()
+        data = {}
+        backend_name = self.configuration.safe_get('volume_backend_name')
+        data['volume_backend_name'] = backend_name or self.__class__.__name__
+        data['vendor_name'] = 'Oracle'
+        data['driver_version'] = self.VERSION
+        data['storage_protocol'] = self.protocol
+
+        free, used = self._get_share_capacity_info()
+        capacity = float(free) + float(used)
+        ratio_used = used / capacity
+
+        data['QoS_support'] = False
+        data['reserved_percentage'] = 0
+
+        if ratio_used > self.configuration.nfs_used_ratio or \
+           ratio_used >= self.configuration.nfs_oversub_ratio:
+            data['reserved_percentage'] = 100
+
+        data['total_capacity_gb'] = float(capacity) / units.Gi
+        data['free_capacity_gb'] = float(free) / units.Gi
+
+        self._stats = data
index d8203666c248f9d57596b157a2547e83163b6973..6e6981bc692cd4798792c415002dfbf7e21edc7d 100644 (file)
@@ -20,6 +20,7 @@ from cinder import exception
 from cinder.i18n import _, _LE
 from cinder.openstack.common import log
 from cinder.volume.drivers.zfssa import restclient
+from cinder.volume.drivers.zfssa import webdavclient
 
 LOG = log.getLogger(__name__)
 
@@ -641,3 +642,250 @@ class ZFSSAApi(object):
                       "default initiator group.")
             groups.append('default')
         return groups
+
+
+class ZFSSANfsApi(ZFSSAApi):
+    """ZFSSA API proxy class for NFS driver"""
+    projects_path = '/api/storage/v1/pools/%s/projects'
+    project_path = projects_path + '/%s'
+
+    shares_path = project_path + '/filesystems'
+    share_path = shares_path + '/%s'
+    share_snapshots_path = share_path + '/snapshots'
+    share_snapshot_path = share_snapshots_path + '/%s'
+
+    services_path = '/api/service/v1/services/'
+
+    def __init__(self, *args, **kwargs):
+        super(ZFSSANfsApi, self).__init__(*args, **kwargs)
+        self.webdavclient = None
+
+    def set_webdav(self, https_path, auth_str):
+        self.webdavclient = webdavclient.ZFSSAWebDAVClient(https_path,
+                                                           auth_str)
+
+    def verify_share(self, pool, project, share):
+        """Checks whether the share exists"""
+        svc = self.share_path % (pool, project, share)
+        ret = self.rclient.get(svc)
+        if ret.status != restclient.Status.OK:
+            exception_msg = (_('Error Verifying '
+                               'share: %(share)s on '
+                               'Project: %(project)s and '
+                               'Pool: %(pool)s '
+                               'Return code: %(ret.status)d '
+                               'Message: %(ret.data)s.')
+                             % {'share': share,
+                                'project': project,
+                                'pool': pool,
+                                'ret.status': ret.status,
+                                'ret.data': ret.data})
+            LOG.error(exception_msg)
+            raise exception.VolumeBackendAPIException(data=exception_msg)
+
+    def create_snapshot(self, pool, project, share, snapshot):
+        """create snapshot of a share"""
+        svc = self.share_snapshots_path % (pool, project, share)
+
+        arg = {
+            'name': snapshot
+        }
+
+        ret = self.rclient.post(svc, arg)
+        if ret.status != restclient.Status.CREATED:
+            exception_msg = (_('Error Creating '
+                               'Snapshot: %(snapshot)s on'
+                               'share: %(share)s to '
+                               'Pool: %(pool)s '
+                               'Project: %(project)s  '
+                               'Return code: %(ret.status)d '
+                               'Message: %(ret.data)s.')
+                             % {'snapshot': snapshot,
+                                'share': share,
+                                'pool': pool,
+                                'project': project,
+                                'ret.status': ret.status,
+                                'ret.data': ret.data})
+            LOG.error(exception_msg)
+            raise exception.VolumeBackendAPIException(data=exception_msg)
+
+    def delete_snapshot(self, pool, project, share, snapshot):
+        """delete snapshot of a share"""
+        svc = self.share_snapshot_path % (pool, project, share, snapshot)
+
+        ret = self.rclient.delete(svc)
+        if ret.status != restclient.Status.NO_CONTENT:
+            exception_msg = (_('Error Deleting '
+                               'Snapshot: %(snapshot)s on '
+                               'Share: %(share)s to '
+                               'Pool: %(pool)s '
+                               'Project: %(project)s '
+                               'Return code: %(ret.status)d '
+                               'Message: %(ret.data)s.')
+                             % {'snapshot': snapshot,
+                                'share': share,
+                                'pool': pool,
+                                'project': project,
+                                'ret.status': ret.status,
+                                'ret.data': ret.data})
+            LOG.error(exception_msg)
+            raise exception.VolumeBackendAPIException(data=exception_msg)
+
+    def create_snapshot_of_volume_file(self, src_file="", dst_file=""):
+        src_file = '.zfs/snapshot/' + src_file
+        return self.webdavclient.request(src_file=src_file, dst_file=dst_file,
+                                         method='COPY')
+
+    def delete_snapshot_of_volume_file(self, src_file=""):
+        return self.webdavclient.request(src_file=src_file, method='DELETE')
+
+    def create_volume_from_snapshot_file(self, src_file="", dst_file="",
+                                         method='COPY'):
+        return self.webdavclient.request(src_file=src_file, dst_file=dst_file,
+                                         method=method)
+
+    def _change_service_state(self, service, state=''):
+        svc = self.services_path + service + '/' + state
+        ret = self.rclient.put(svc)
+        if ret.status != restclient.Status.ACCEPTED:
+            exception_msg = (_('Error Verifying '
+                               'Service: %(service)s '
+                               'Return code: %(ret.status)d '
+                               'Message: %(ret.data)s.')
+                             % {'service': service,
+                                'ret.status': ret.status,
+                                'ret.data': ret.data})
+
+            LOG.error(exception_msg)
+            raise exception.VolumeBackendAPIException(data=exception_msg)
+        data = json.loads(ret.data)['service']
+        LOG.debug('%s service state: %s' % (service, data))
+
+        status = 'online' if state == 'enable' else 'disabled'
+
+        if data['<status>'] != status:
+            exception_msg = (_('%(service)s Service is not %(status)s '
+                               'on storage appliance: %(host)s')
+                             % {'service': service,
+                                'status': status,
+                                'host': self.host})
+            LOG.error(exception_msg)
+            raise exception.VolumeBackendAPIException(data=exception_msg)
+
+    def enable_service(self, service):
+        self._change_service_state(service, state='enable')
+        self.verify_service(service)
+
+    def disable_service(self, service):
+        self._change_service_state(service, state='disable')
+        self.verify_service(service, status='offline')
+
+    def verify_service(self, service, status='online'):
+        """Checks whether a service is online or not"""
+        svc = self.services_path + service
+        ret = self.rclient.get(svc)
+
+        if ret.status != restclient.Status.OK:
+            exception_msg = (_('Error Verifying '
+                               'Service: %(service)s '
+                               'Return code: %(ret.status)d '
+                               'Message: %(ret.data)s.')
+                             % {'service': service,
+                                'ret.status': ret.status,
+                                'ret.data': ret.data})
+
+            LOG.error(exception_msg)
+            raise exception.VolumeBackendAPIException(data=exception_msg)
+
+        data = json.loads(ret.data)['service']
+
+        if data['<status>'] != status:
+            exception_msg = (_('%(service)s Service is not %(status)s '
+                               'on storage appliance: %(host)s')
+                             % {'service': service,
+                                'status': status,
+                                'host': self.host})
+            LOG.error(exception_msg)
+            raise exception.VolumeBackendAPIException(data=exception_msg)
+
+    def modify_service(self, service, edit_args=None):
+        """Edit service properties"""
+        if edit_args is None:
+            edit_args = {}
+
+        svc = self.services_path + service
+
+        ret = self.rclient.put(svc, edit_args)
+
+        if ret.status != restclient.Status.ACCEPTED:
+            exception_msg = (_('Error modifying '
+                               'Service: %(service)s '
+                               'Return code: %(ret.status)d '
+                               'Message: %(ret.data)s.')
+                             % {'service': service,
+                                'ret.status': ret.status,
+                                'ret.data': ret.data})
+
+            LOG.error(exception_msg)
+            raise exception.VolumeBackendAPIException(data=exception_msg)
+        data = json.loads(ret.data)['service']
+        LOG.debug('Modify %(service)s service '
+                  'return data: %(data)s'
+                  % {'service': service,
+                     'data': data})
+
+    def create_share(self, pool, project, share, args):
+        """Create a share in the specified pool and project"""
+        svc = self.share_path % (pool, project, share)
+        ret = self.rclient.get(svc)
+        if ret.status != restclient.Status.OK:
+            svc = self.shares_path % (pool, project)
+            args.update({'name': share})
+            ret = self.rclient.post(svc, args)
+            if ret.status != restclient.Status.CREATED:
+                exception_msg = (_('Error Creating '
+                                   'Share: %(name)s '
+                                   'Return code: %(ret.status)d '
+                                   'Message: %(ret.data)s.')
+                                 % {'name': share,
+                                    'ret.status': ret.status,
+                                    'ret.data': ret.data})
+                LOG.error(exception_msg)
+                raise exception.VolumeBackendAPIException(data=exception_msg)
+        else:
+            LOG.debug('Editing properties of a pre-existing share')
+            ret = self.rclient.put(svc, args)
+            if ret.status != restclient.Status.ACCEPTED:
+                exception_msg = (_('Error editing share: '
+                                   '%(share)s on '
+                                   'Pool: %(pool)s '
+                                   'Return code: %(ret.status)d '
+                                   'Message: %(ret.data)s .')
+                                 % {'share': share,
+                                    'pool': pool,
+                                    'ret.status': ret.status,
+                                    'ret.data': ret.data})
+                LOG.error(exception_msg)
+                raise exception.VolumeBackendAPIException(data=exception_msg)
+
+    def get_share(self, pool, project, share):
+        """return share properties"""
+        svc = self.share_path % (pool, project, share)
+        ret = self.rclient.get(svc)
+        if ret.status != restclient.Status.OK:
+            exception_msg = (_('Error Getting '
+                               'Share: %(share)s on '
+                               'Pool: %(pool)s '
+                               'Project: %(project)s '
+                               'Return code: %(ret.status)d '
+                               'Message: %(ret.data)s.')
+                             % {'share': share,
+                                'pool': pool,
+                                'project': project,
+                                'ret.status': ret.status,
+                                'ret.data': ret.data})
+            LOG.error(exception_msg)
+            raise exception.VolumeBackendAPIException(data=exception_msg)
+
+        val = json.loads(ret.data)
+        return val['filesystem']