From da4759b6bcb099552db9a938f9068cc9d0899eb1 Mon Sep 17 00:00:00 2001 From: kedar-vidvans Date: Tue, 9 Dec 2014 14:48:08 -0500 Subject: [PATCH] Add Oracle ZFSSA NFS Cinder Driver Support 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 | 7 + cinder/tests/test_zfssa.py | 153 ++++++++++ cinder/volume/drivers/zfssa/webdavclient.py | 132 +++++++++ cinder/volume/drivers/zfssa/zfssanfs.py | 301 ++++++++++++++++++++ cinder/volume/drivers/zfssa/zfssarest.py | 248 ++++++++++++++++ 5 files changed, 841 insertions(+) create mode 100644 cinder/volume/drivers/zfssa/webdavclient.py create mode 100644 cinder/volume/drivers/zfssa/zfssanfs.py diff --git a/cinder/exception.py b/cinder/exception.py index 3373752d7..6c9273af2 100755 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -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.") diff --git a/cinder/tests/test_zfssa.py b/cinder/tests/test_zfssa.py index 8021527ee..9ce5e95bc 100644 --- a/cinder/tests/test_zfssa.py +++ b/cinder/tests/test_zfssa.py @@ -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": {"": "online"}} + return out + + def enable_service(self, service): + out = {} + if not self.host and not self.user: + return out + out = {"service": {"": "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 index 000000000..9ca5feae5 --- /dev/null +++ b/cinder/volume/drivers/zfssa/webdavclient.py @@ -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 index 000000000..0c8266b9f --- /dev/null +++ b/cinder/volume/drivers/zfssa/zfssanfs.py @@ -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 diff --git a/cinder/volume/drivers/zfssa/zfssarest.py b/cinder/volume/drivers/zfssa/zfssarest.py index d8203666c..6e6981bc6 100644 --- a/cinder/volume/drivers/zfssa/zfssarest.py +++ b/cinder/volume/drivers/zfssa/zfssarest.py @@ -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: + 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: + 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'] -- 2.45.2