--- /dev/null
+# 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.
+"""
+Unit tests for Oracle's ZFSSA Cinder volume driver
+"""
+
+import mock
+
+from cinder.openstack.common import log as logging
+from cinder.openstack.common import units
+from cinder import test
+from cinder.volume import configuration as conf
+from cinder.volume.drivers.zfssa import zfssaiscsi as iscsi
+
+
+LOG = logging.getLogger(__name__)
+
+
+class FakeZFSSA(object):
+ """Fake ZFS SA"""
+ def __init__(self):
+ self.user = None
+ self.host = None
+
+ def login(self, user):
+ self.user = user
+
+ def set_host(self, host):
+ self.host = host
+
+ def create_project(self, pool, project, compression, logbias):
+ out = {}
+ if not self.host or not self.user:
+ return out
+
+ out = {"status": "online",
+ "name": "pool",
+ "usage": {"available": 10,
+ "total": 10,
+ "dedupratio": 100,
+ "used": 1},
+ "peer": "00000000-0000-0000-0000-000000000000",
+ "owner": "host",
+ "asn": "11111111-2222-3333-4444-555555555555"}
+ return out
+
+ def create_initiator(self, init, initgrp, chapuser, chapsecret):
+ out = {}
+ if not self.host or not self.user:
+ return out
+ out = {"href": "fake_href",
+ "alias": "fake_alias",
+ "initiator": "fake_iqn.1993-08.org.fake:01:000000000000",
+ "chapuser": "",
+ "chapsecret": ""
+ }
+
+ return out
+
+ def add_to_initiatorgroup(self, init, initgrp):
+ out = {}
+ if not self.host or not self.user:
+ return out
+
+ out = {"href": "fake_href",
+ "name": "fake_initgrp",
+ "initiators": ["fake_iqn.1993-08.org.fake:01:000000000000"]
+ }
+ return out
+
+ def create_target(self, tgtalias, inter, tchapuser, tchapsecret):
+ out = {}
+ if not self.host or not self.user:
+ return out
+ out = {"href": "fake_href",
+ "alias": "fake_tgtgrp",
+ "iqn": "iqn.1986-03.com.sun:02:00000-aaaa-bbbb-cccc-ddddd",
+ "auth": "none",
+ "targetchapuser": "",
+ "targetchapsecret": "",
+ "interfaces": ["eth0"]
+ }
+
+ return out
+
+ def add_to_targetgroup(self, iqn, tgtgrp):
+ out = {}
+ if not self.host or not self.user:
+ return {}
+ out = {"href": "fake_href",
+ "name": "fake_tgtgrp",
+ "targets": ["iqn.1986-03.com.sun:02:00000-aaaa-bbbb-cccc-ddddd"]
+ }
+ return out
+
+ def get_lun(self, pool, project, lun):
+ ret = {
+ 'guid': '600144F0F8FBD5BD000053CE53AB0001',
+ 'number': 0,
+ 'initiatorgroup': 'fake_initgrp',
+ 'size': 1 * units.Gi
+ }
+ return ret
+
+ def get_target(self, target):
+ return 'iqn.1986-03.com.sun:02:00000-aaaa-bbbb-cccc-ddddd'
+
+ def create_lun(self, pool, project, lun, volsize, targetgroup,
+ volblocksize, sparse, compression, logbias):
+ out = {}
+ if not self.host and not self.user:
+ return out
+
+ out = {"logbias": logbias,
+ "compression": compression,
+ "status": "online",
+ "lunguid": "600144F0F8FBD5BD000053CE53AB0001",
+ "initiatorgroup": ["fake_initgrp"],
+ "volsize": volsize,
+ "pool": pool,
+ "volblocksize": volblocksize,
+ "name": lun,
+ "project": project,
+ "sparse": sparse,
+ "targetgroup": targetgroup}
+
+ return out
+
+ def delete_lun(self, pool, project, lun):
+ out = {}
+ if not self.host and not self.user:
+ return out
+ out = {"pool": pool,
+ "project": project,
+ "name": lun}
+
+ return out
+
+ def create_snapshot(self, pool, project, vol, snap):
+ out = {}
+ if not self.host and not self.user:
+ return {}
+ out = {"name": snap,
+ "numclones": 0,
+ "share": vol,
+ "project": project,
+ "pool": pool}
+
+ return out
+
+ def delete_snapshot(self, pool, project, vol, snap):
+ out = {}
+ if not self.host and not self.user:
+ return {}
+ out = {"name": snap,
+ "share": vol,
+ "project": project,
+ "pool": pool}
+
+ return out
+
+ def clone_snapshot(self, pool, project, pvol, snap, vol):
+ out = {}
+ if not self.host and not self.user:
+ return out
+ out = {"origin": {"project": project,
+ "snapshot": snap,
+ "share": pvol,
+ "pool": pool},
+ "logbias": "latency",
+ "assignednumber": 1,
+ "status": "online",
+ "lunguid": "600144F0F8FBD5BD000053CE67A50002",
+ "volsize": 1,
+ "pool": pool,
+ "name": vol,
+ "project": project}
+
+ return out
+
+ def set_lun_initiatorgroup(self, pool, project, vol, initgrp):
+ out = {}
+ if not self.host and not self.user:
+ return out
+ out = {"lunguid": "600144F0F8FBD5BD000053CE67A50002",
+ "pool": pool,
+ "name": vol,
+ "project": project,
+ "initiatorgroup": ["fake_initgrp"]}
+
+ return out
+
+ def has_clones(self, pool, project, vol, snapshot):
+ return False
+
+ def set_lun_props(self, pool, project, vol, **kargs):
+ out = {}
+ if not self.host and not self.user:
+ return out
+ out = {"pool": pool,
+ "name": vol,
+ "project": project,
+ "volsize": kargs['volsize']}
+
+ return out
+
+
+class TestZFSSAISCSIDriver(test.TestCase):
+
+ test_vol = {
+ 'name': 'cindervol',
+ 'size': 1
+ }
+
+ test_snap = {
+ 'name': 'cindersnap',
+ 'volume_name': test_vol['name']
+ }
+
+ test_vol_snap = {
+ 'name': 'cindersnapvol',
+ 'size': test_vol['size']
+ }
+
+ def __init__(self, method):
+ super(TestZFSSAISCSIDriver, self).__init__(method)
+
+ @mock.patch.object(iscsi, 'factory_zfssa')
+ def setUp(self, _factory_zfssa):
+ super(TestZFSSAISCSIDriver, self).setUp()
+ self._create_fake_config()
+ _factory_zfssa.return_value = FakeZFSSA()
+ self.drv = iscsi.ZFSSAISCSIDriver(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_pool = 'pool'
+ self.configuration.zfssa_project = 'project'
+ self.configuration.zfssa_lun_volblocksize = '8k'
+ self.configuration.zfssa_lun_sparse = 'false'
+ self.configuration.zfssa_lun_logbias = 'latency'
+ self.configuration.zfssa_lun_compression = 'off'
+ self.configuration.zfssa_initiator_group = 'test-init-grp1'
+ self.configuration.zfssa_initiator = \
+ 'iqn.1993-08.org.debian:01:daa02db2a827'
+ self.configuration.zfssa_initiator_user = ''
+ self.configuration.zfssa_initiator_password = ''
+ self.configuration.zfssa_target_group = 'test-target-grp1'
+ self.configuration.zfssa_target_user = ''
+ self.configuration.zfssa_target_password = ''
+ self.configuration.zfssa_target_portal = '1.1.1.1:3260'
+ self.configuration.zfssa_target_interfaces = 'e1000g0'
+
+ def test_create_delete_volume(self):
+ self.drv.create_volume(self.test_vol)
+ self.drv.delete_volume(self.test_vol)
+
+ def test_create_delete_snapshot(self):
+ self.drv.create_volume(self.test_vol)
+ self.drv.create_snapshot(self.test_snap)
+ self.drv.delete_snapshot(self.test_snap)
+ self.drv.delete_volume(self.test_vol)
+
+ def test_create_volume_from_snapshot(self):
+ self.drv.create_volume(self.test_vol)
+ self.drv.create_snapshot(self.test_snap)
+ self.drv.create_volume_from_snapshot(self.test_vol_snap,
+ self.test_snap)
+ self.drv.delete_volume(self.test_vol)
+
+ def test_create_export(self):
+ self.drv.create_volume(self.test_vol)
+ self.drv.create_export({}, self.test_vol)
+ self.drv.delete_volume(self.test_vol)
+
+ def test_remove_export(self):
+ self.drv.create_volume(self.test_vol)
+ self.drv.remove_export({}, self.test_vol)
+ self.drv.delete_volume(self.test_vol)
+
+ def test_get_volume_stats(self):
+ self.drv.get_volume_stats(refresh=False)
+
+ def test_extend_volume(self):
+ self.drv.create_volume(self.test_vol)
+ self.drv.extend_volume(self.test_vol, 3)
+ self.drv.delete_volume(self.test_vol)
+
+ def tearDown(self):
+ super(TestZFSSAISCSIDriver, self).tearDown()
--- /dev/null
+# 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 REST API Client Programmatic Interface
+"""
+
+import httplib
+import json
+import StringIO
+import time
+import urllib2
+
+from cinder.i18n import _
+from cinder.openstack.common import log
+
+LOG = log.getLogger(__name__)
+
+
+class Status(object):
+ """Result HTTP Status"""
+
+ def __init__(self):
+ pass
+
+ #: Request return OK
+ OK = httplib.OK
+
+ #: New resource created successfully
+ CREATED = httplib.CREATED
+
+ #: Command accepted
+ ACCEPTED = httplib.ACCEPTED
+
+ #: Command returned OK but no data will be returned
+ NO_CONTENT = httplib.NO_CONTENT
+
+ #: Bad Request
+ BAD_REQUEST = httplib.BAD_REQUEST
+
+ #: User is not authorized
+ UNAUTHORIZED = httplib.UNAUTHORIZED
+
+ #: The request is not allowed
+ FORBIDDEN = httplib.FORBIDDEN
+
+ #: The requested resource was not found
+ NOT_FOUND = httplib.NOT_FOUND
+
+ #: The request is not allowed
+ NOT_ALLOWED = httplib.METHOD_NOT_ALLOWED
+
+ #: Request timed out
+ TIMEOUT = httplib.REQUEST_TIMEOUT
+
+ #: Invalid request
+ CONFLICT = httplib.CONFLICT
+
+ #: Service Unavailable
+ BUSY = httplib.SERVICE_UNAVAILABLE
+
+
+class RestResult(object):
+ """Result from a REST API operation"""
+ def __init__(self, response=None, err=None):
+ """Initialize a RestResult containing the results from a REST call
+ :param response: HTTP response
+ """
+ self.response = response
+ self.error = err
+ self.data = ""
+ self.status = 0
+ if self.response:
+ self.status = self.response.getcode()
+ result = self.response.read()
+ while result:
+ self.data += result
+ result = self.response.read()
+
+ if self.error:
+ self.status = self.error.code
+ self.data = httplib.responses[self.status]
+
+ LOG.debug('Response code: %s' % self.status)
+ LOG.debug('Response data: %s' % self.data)
+
+ def get_header(self, name):
+ """Get an HTTP header with the given name from the results
+
+ :param name: HTTP header name
+ :return: The header value or None if no value is found
+ """
+ if self.response is None:
+ return None
+ info = self.response.info()
+ return info.getheader(name)
+
+
+class RestClientError(Exception):
+ """Exception for ZFS REST API client errors"""
+ def __init__(self, status, name="ERR_INTERNAL", message=None):
+
+ """Create a REST Response exception
+
+ :param status: HTTP response status
+ :param name: The name of the REST API error type
+ :param message: Descriptive error message returned from REST call
+ """
+ super(RestClientError, self).__init__(message)
+ self.code = status
+ self.name = name
+ self.msg = message
+ if status in httplib.responses:
+ self.msg = httplib.responses[status]
+
+ def __str__(self):
+ return "%d %s %s" % (self.code, self.name, self.msg)
+
+
+class RestClientURL(object):
+ """ZFSSA urllib2 client"""
+ def __init__(self, url, **kwargs):
+ """Initialize a REST client.
+
+ :param url: The ZFSSA REST API URL
+ :key session: HTTP Cookie value of x-auth-session obtained from a
+ normal BUI login.
+ :key timeout: Time in seconds to wait for command to complete.
+ (Default is 60 seconds)
+ """
+ self.url = url
+ self.local = kwargs.get("local", False)
+ self.base_path = kwargs.get("base_path", "/api")
+ self.timeout = kwargs.get("timeout", 60)
+ self.headers = None
+ if kwargs.get('session'):
+ self.headers['x-auth-session'] = kwargs.get('session')
+
+ self.headers = {"content-type": "application/json"}
+ self.do_logout = False
+ self.auth_str = None
+
+ def _path(self, path, base_path=None):
+ """build rest url path"""
+ if path.startswith("http://") or path.startswith("https://"):
+ return path
+ if base_path is None:
+ base_path = self.base_path
+ if not path.startswith(base_path) and not (
+ self.local and ("/api" + path).startswith(base_path)):
+ path = "%s%s" % (base_path, path)
+ if self.local and path.startswith("/api"):
+ path = path[4:]
+ return self.url + path
+
+ def _authorize(self):
+ """Performs authorization setting x-auth-session"""
+ self.headers['authorization'] = 'Basic %s' % self.auth_str
+ if 'x-auth-session' in self.headers:
+ del self.headers['x-auth-session']
+
+ try:
+ result = self.post("/access/v1")
+ del self.headers['authorization']
+ if result.status == httplib.CREATED:
+ self.headers['x-auth-session'] = \
+ result.get_header('x-auth-session')
+ self.do_logout = True
+ LOG.info(_('ZFSSA version: %s') %
+ result.get_header('x-zfssa-version'))
+
+ elif result.status == httplib.NOT_FOUND:
+ raise RestClientError(result.status, name="ERR_RESTError",
+ message="REST Not Available: \
+ Please Upgrade")
+
+ except RestClientError as err:
+ del self.headers['authorization']
+ raise err
+
+ def login(self, auth_str):
+ """Login to an appliance using a user name and password.
+
+ Start a session like what is done logging into the BUI. This is not a
+ requirement to run REST commands, since the protocol is stateless.
+ What is does is set up a cookie session so that some server side
+ caching can be done. If login is used remember to call logout when
+ finished.
+
+ :param auth_str: Authorization string (base64)
+ """
+ self.auth_str = auth_str
+ self._authorize()
+
+ def logout(self):
+ """Logout of an appliance"""
+ result = None
+ try:
+ result = self.delete("/access/v1", base_path="/api")
+ except RestClientError:
+ pass
+
+ self.headers.clear()
+ self.do_logout = False
+ return result
+
+ def islogin(self):
+ """return if client is login"""
+ return self.do_logout
+
+ @staticmethod
+ def mkpath(*args, **kwargs):
+ """Make a path?query string for making a REST request
+
+ :cmd_params args: The path part
+ :cmd_params kwargs: The query part
+ """
+ buf = StringIO.StringIO()
+ query = "?"
+ for arg in args:
+ buf.write("/")
+ buf.write(arg)
+ for k in kwargs:
+ buf.write(query)
+ if query == "?":
+ query = "&"
+ buf.write(k)
+ buf.write("=")
+ buf.write(kwargs[k])
+ return buf.getvalue()
+
+ def request(self, path, request, body=None, **kwargs):
+ """Make an HTTP request and return the results
+
+ :param path: Path used with the initiazed URL to make a request
+ :param request: HTTP request type (GET, POST, PUT, DELETE)
+ :param body: HTTP body of request
+ :key accept: Set HTTP 'Accept' header with this value
+ :key base_path: Override the base_path for this request
+ :key content: Set HTTP 'Content-Type' header with this value
+ """
+ out_hdrs = dict.copy(self.headers)
+ if kwargs.get("accept"):
+ out_hdrs['accept'] = kwargs.get("accept")
+
+ if body:
+ if isinstance(body, dict):
+ body = str(json.dumps(body))
+
+ if body and len(body):
+ out_hdrs['content-length'] = len(body)
+
+ zfssaurl = self._path(path, kwargs.get("base_path"))
+ req = urllib2.Request(zfssaurl, body, out_hdrs)
+ req.get_method = lambda: request
+ maxreqretries = kwargs.get("maxreqretries", 10)
+ retry = 0
+ response = None
+
+ LOG.debug('Request: %s %s' % (request, zfssaurl))
+ LOG.debug('Out headers: %s' % out_hdrs)
+ if body and body != '':
+ LOG.debug('Body: %s' % body)
+
+ while retry < maxreqretries:
+ try:
+ response = urllib2.urlopen(req, timeout=self.timeout)
+ except urllib2.HTTPError as err:
+ LOG.error(_('REST Not Available: %s') % err.code)
+ if err.code == httplib.SERVICE_UNAVAILABLE and \
+ retry < maxreqretries:
+ retry += 1
+ time.sleep(1)
+ LOG.error(_('Server Busy retry request: %s') % retry)
+ continue
+ if (err.code == httplib.UNAUTHORIZED or
+ err.code == httplib.INTERNAL_SERVER_ERROR) and \
+ '/access/v1' not in zfssaurl:
+ try:
+ LOG.error(_('Authorizing request: '
+ '%(zfssaurl)s'
+ 'retry: %(retry)d .')
+ % {'zfssaurl': zfssaurl,
+ 'retry': retry})
+ self._authorize()
+ req.add_header('x-auth-session',
+ self.headers['x-auth-session'])
+ except RestClientError:
+ pass
+ retry += 1
+ time.sleep(1)
+ continue
+
+ return RestResult(err=err)
+
+ except urllib2.URLError as err:
+ LOG.error(_('URLError: %s') % err.reason)
+ raise RestClientError(-1, name="ERR_URLError",
+ message=err.reason)
+
+ break
+
+ if response and response.getcode() == httplib.SERVICE_UNAVAILABLE and \
+ retry >= maxreqretries:
+ raise RestClientError(response.getcode(), name="ERR_HTTPError",
+ message="REST Not Available: Disabled")
+
+ return RestResult(response=response)
+
+ def get(self, path, **kwargs):
+ """Make an HTTP GET request
+
+ :param path: Path to resource.
+ """
+ return self.request(path, "GET", **kwargs)
+
+ def post(self, path, body="", **kwargs):
+ """Make an HTTP POST request
+
+ :param path: Path to resource.
+ :param body: Post data content
+ """
+ return self.request(path, "POST", body, **kwargs)
+
+ def put(self, path, body="", **kwargs):
+ """Make an HTTP PUT request
+
+ :param path: Path to resource.
+ :param body: Put data content
+ """
+ return self.request(path, "PUT", body, **kwargs)
+
+ def delete(self, path, **kwargs):
+ """Make an HTTP DELETE request
+
+ :param path: Path to resource that will be deleted.
+ """
+ return self.request(path, "DELETE", **kwargs)
+
+ def head(self, path, **kwargs):
+ """Make an HTTP HEAD request
+
+ :param path: Path to resource.
+ """
+ return self.request(path, "HEAD", **kwargs)
--- /dev/null
+# 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 Cinder Volume Driver
+"""
+import base64
+
+from oslo.config import cfg
+
+from cinder import exception
+from cinder.i18n import _
+from cinder.openstack.common import log
+from cinder.openstack.common import units
+from cinder.volume import driver
+from cinder.volume.drivers.san import san
+from cinder.volume.drivers.zfssa import zfssarest
+
+CONF = cfg.CONF
+LOG = log.getLogger(__name__)
+
+ZFSSA_OPTS = [
+ cfg.StrOpt('zfssa_pool',
+ help='Storage pool name.'),
+ cfg.StrOpt('zfssa_project',
+ help='Project name.'),
+ cfg.StrOpt('zfssa_lun_volblocksize', default='8k',
+ help='Block size: 512, 1k, 2k, 4k, 8k, 16k, 32k, 64k, 128k.'),
+ cfg.BoolOpt('zfssa_lun_sparse', default=False,
+ help='Flag to enable sparse (thin-provisioned): True, False.'),
+ cfg.StrOpt('zfssa_lun_compression', default='',
+ help='Data compression-off, lzjb, gzip-2, gzip, gzip-9.'),
+ cfg.StrOpt('zfssa_lun_logbias', default='',
+ help='Synchronous write bias-latency, throughput.'),
+ cfg.StrOpt('zfssa_initiator_group', default='',
+ help='iSCSI initiator group.'),
+ cfg.StrOpt('zfssa_initiator', default='',
+ help='iSCSI initiator IQNs. (comma separated)'),
+ cfg.StrOpt('zfssa_initiator_user', default='',
+ help='iSCSI initiator CHAP user.'),
+ cfg.StrOpt('zfssa_initiator_password', default='',
+ help='iSCSI initiator CHAP password.'),
+ cfg.StrOpt('zfssa_target_group', default='tgt-grp',
+ help='iSCSI target group name.'),
+ cfg.StrOpt('zfssa_target_user', default='',
+ help='iSCSI target CHAP user.'),
+ cfg.StrOpt('zfssa_target_password', default='',
+ help='iSCSI target CHAP password.'),
+ cfg.StrOpt('zfssa_target_portal',
+ help='iSCSI target portal (Data-IP:Port, w.x.y.z:3260).'),
+ cfg.StrOpt('zfssa_target_interfaces',
+ help='Network interfaces of iSCSI targets. (comma separated)')
+]
+
+CONF.register_opts(ZFSSA_OPTS)
+
+
+def factory_zfssa():
+ return zfssarest.ZFSSAApi()
+
+
+class ZFSSAISCSIDriver(driver.ISCSIDriver):
+ """ZFSSA Cinder volume driver"""
+
+ VERSION = '1.0.0'
+ protocol = 'iSCSI'
+
+ def __init__(self, *args, **kwargs):
+ super(ZFSSAISCSIDriver, 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 _get_target_alias(self):
+ """return target alias"""
+ return self.configuration.zfssa_target_group
+
+ def do_setup(self, context):
+ """Setup - create multiple elements.
+
+ Project, initiators, initiatorgroup, target and targetgroup.
+ """
+ lcfg = self.configuration
+ msg = (_('Connecting to host: %s.') % lcfg.san_ip)
+ LOG.info(msg)
+ self.zfssa = factory_zfssa()
+ self.zfssa.set_host(lcfg.san_ip)
+ auth_str = base64.encodestring('%s:%s' %
+ (lcfg.san_login,
+ lcfg.san_password))[:-1]
+ self.zfssa.login(auth_str)
+ self.zfssa.create_project(lcfg.zfssa_pool, lcfg.zfssa_project,
+ compression=lcfg.zfssa_lun_compression,
+ logbias=lcfg.zfssa_lun_logbias)
+
+ if (lcfg.zfssa_initiator != '' and
+ (lcfg.zfssa_initiator_group == '' or
+ lcfg.zfssa_initiator_group == 'default')):
+ msg = (_('zfssa_initiator: %(ini)s'
+ ' wont be used on '
+ 'zfssa_initiator_group= %(inigrp)s.')
+ % {'ini': lcfg.zfssa_initiator,
+ 'inigrp': lcfg.zfssa_initiator_group})
+
+ LOG.warning(msg)
+ # Setup initiator and initiator group
+ if (lcfg.zfssa_initiator != '' and
+ lcfg.zfssa_initiator_group != '' and
+ lcfg.zfssa_initiator_group != 'default'):
+ for initiator in lcfg.zfssa_initiator.split(','):
+ self.zfssa.create_initiator(initiator,
+ lcfg.zfssa_initiator_group + '-' +
+ initiator,
+ chapuser=
+ lcfg.zfssa_initiator_user,
+ chapsecret=
+ lcfg.zfssa_initiator_password)
+ self.zfssa.add_to_initiatorgroup(initiator,
+ lcfg.zfssa_initiator_group)
+ # Parse interfaces
+ interfaces = []
+ for interface in lcfg.zfssa_target_interfaces.split(','):
+ if interface == '':
+ continue
+ interfaces.append(interface)
+
+ # Setup target and target group
+ iqn = self.zfssa.create_target(
+ self._get_target_alias(),
+ interfaces,
+ tchapuser=lcfg.zfssa_target_user,
+ tchapsecret=lcfg.zfssa_target_password)
+
+ self.zfssa.add_to_targetgroup(iqn, lcfg.zfssa_target_group)
+
+ def check_for_setup_error(self):
+ """Check that driver can login.
+
+ Check also pool, project, initiators, initiatorgroup, target and
+ targetgroup.
+ """
+ lcfg = self.configuration
+
+ self.zfssa.verify_pool(lcfg.zfssa_pool)
+ self.zfssa.verify_project(lcfg.zfssa_pool, lcfg.zfssa_project)
+
+ if (lcfg.zfssa_initiator != '' and
+ lcfg.zfssa_initiator_group != '' and
+ lcfg.zfssa_initiator_group != 'default'):
+ for initiator in lcfg.zfssa_initiator.split(','):
+ self.zfssa.verify_initiator(initiator)
+
+ self.zfssa.verify_target(self._get_target_alias())
+
+ def _get_provider_info(self, volume):
+ """return provider information"""
+ lcfg = self.configuration
+ lun = self.zfssa.get_lun(lcfg.zfssa_pool,
+ lcfg.zfssa_project, volume['name'])
+ iqn = self.zfssa.get_target(self._get_target_alias())
+ loc = "%s %s %s" % (lcfg.zfssa_target_portal, iqn, lun['number'])
+ LOG.debug('_get_provider_info: provider_location: %s' % loc)
+ provider = {'provider_location': loc}
+ if lcfg.zfssa_target_user != '' and lcfg.zfssa_target_password != '':
+ provider['provider_auth'] = ('CHAP %s %s' %
+ lcfg.zfssa_target_user,
+ lcfg.zfssa_target_password)
+
+ return provider
+
+ def create_volume(self, volume):
+ """Create a volume on ZFSSA"""
+ LOG.debug('zfssa.create_volume: volume=' + volume['name'])
+ lcfg = self.configuration
+ volsize = str(volume['size']) + 'g'
+ self.zfssa.create_lun(lcfg.zfssa_pool,
+ lcfg.zfssa_project,
+ volume['name'],
+ volsize,
+ targetgroup=lcfg.zfssa_target_group,
+ volblocksize=lcfg.zfssa_lun_volblocksize,
+ sparse=lcfg.zfssa_lun_sparse,
+ compression=lcfg.zfssa_lun_compression,
+ logbias=lcfg.zfssa_lun_logbias)
+
+ return self._get_provider_info(volume)
+
+ def delete_volume(self, volume):
+ """Deletes a volume with the given volume['name']."""
+ LOG.debug('zfssa.delete_volume: name=' + volume['name'])
+ lcfg = self.configuration
+ lun2del = self.zfssa.get_lun(lcfg.zfssa_pool,
+ lcfg.zfssa_project,
+ volume['name'])
+ # Delete clone temp snapshot. see create_cloned_volume()
+ if 'origin' in lun2del and 'id' in volume:
+ if lun2del['nodestroy']:
+ self.zfssa.set_lun_props(lcfg.zfssa_pool,
+ lcfg.zfssa_project,
+ volume['name'],
+ nodestroy=False)
+
+ tmpsnap = 'tmp-snapshot-%s' % volume['id']
+ if lun2del['origin']['snapshot'] == tmpsnap:
+ self.zfssa.delete_snapshot(lcfg.zfssa_pool,
+ lcfg.zfssa_project,
+ lun2del['origin']['share'],
+ lun2del['origin']['snapshot'])
+ return
+
+ self.zfssa.delete_lun(pool=lcfg.zfssa_pool,
+ project=lcfg.zfssa_project,
+ lun=volume['name'])
+
+ def create_snapshot(self, snapshot):
+ """Creates a snapshot with the given snapshot['name'] of the
+ snapshot['volume_name']
+ """
+ LOG.debug('zfssa.create_snapshot: snapshot=' + snapshot['name'])
+ lcfg = self.configuration
+ self.zfssa.create_snapshot(lcfg.zfssa_pool,
+ lcfg.zfssa_project,
+ snapshot['volume_name'],
+ snapshot['name'])
+
+ def delete_snapshot(self, snapshot):
+ """Deletes a snapshot."""
+ LOG.debug('zfssa.delete_snapshot: snapshot=' + snapshot['name'])
+ lcfg = self.configuration
+ has_clones = self.zfssa.has_clones(lcfg.zfssa_pool,
+ lcfg.zfssa_project,
+ snapshot['volume_name'],
+ snapshot['name'])
+ if has_clones:
+ LOG.error(_('Snapshot %s: has clones') % snapshot['name'])
+ raise exception.SnapshotIsBusy(snapshot_name=snapshot['name'])
+
+ self.zfssa.delete_snapshot(lcfg.zfssa_pool,
+ lcfg.zfssa_project,
+ snapshot['volume_name'],
+ snapshot['name'])
+
+ def create_volume_from_snapshot(self, volume, snapshot):
+ """Creates a volume from a snapshot - clone a snapshot"""
+ LOG.debug('zfssa.create_volume_from_snapshot: volume=' +
+ volume['name'])
+ LOG.debug('zfssa.create_volume_from_snapshot: snapshot=' +
+ snapshot['name'])
+ if not self._verify_clone_size(snapshot, volume['size'] * units.Gi):
+ exception_msg = (_('Error verifying clone size on '
+ 'Volume clone: %(clone)s '
+ 'Size: %(size)d on'
+ 'Snapshot: %(snapshot)s')
+ % {'clone': volume['name'],
+ 'size': volume['size'],
+ 'snapshot': snapshot['name']})
+ LOG.error(exception_msg)
+ raise exception.InvalidInput(reason=exception_msg)
+
+ lcfg = self.configuration
+ self.zfssa.clone_snapshot(lcfg.zfssa_pool,
+ lcfg.zfssa_project,
+ snapshot['volume_name'],
+ snapshot['name'],
+ volume['name'])
+
+ def _update_volume_status(self):
+ """Retrieve status info from volume group."""
+ LOG.debug("Updating volume status")
+ self._stats = None
+ data = {}
+ data["volume_backend_name"] = self.__class__.__name__
+ data["vendor_name"] = 'Oracle'
+ data["driver_version"] = self.VERSION
+ data["storage_protocol"] = self.protocol
+
+ lcfg = self.configuration
+ (avail, total) = self.zfssa.get_pool_stats(lcfg.zfssa_pool)
+ if avail is None or total is None:
+ return
+
+ data['total_capacity_gb'] = int(total) / units.Gi
+ data['free_capacity_gb'] = int(avail) / units.Gi
+ data['reserved_percentage'] = 0
+ data['QoS_support'] = False
+ self._stats = data
+
+ def get_volume_stats(self, refresh=False):
+ """Get volume status.
+ If 'refresh' is True, run update the stats first.
+ """
+ if refresh:
+ self._update_volume_status()
+ return self._stats
+
+ def _export_volume(self, volume):
+ """Export the volume - set the initiatorgroup property."""
+ LOG.debug('_export_volume: volume name: %s' % volume['name'])
+ lcfg = self.configuration
+
+ self.zfssa.set_lun_initiatorgroup(lcfg.zfssa_pool,
+ lcfg.zfssa_project,
+ volume['name'],
+ lcfg.zfssa_initiator_group)
+ return self._get_provider_info(volume)
+
+ def create_export(self, context, volume):
+ """Driver entry point to get the export info for a new volume."""
+ LOG.debug('create_export: volume name: %s' % volume['name'])
+ return self._export_volume(volume)
+
+ def remove_export(self, context, volume):
+ """Driver entry point to remove an export for a volume."""
+ LOG.debug('remove_export: volume name: %s' % volume['name'])
+ lcfg = self.configuration
+ self.zfssa.set_lun_initiatorgroup(lcfg.zfssa_pool,
+ lcfg.zfssa_project,
+ volume['name'],
+ '')
+
+ def ensure_export(self, context, volume):
+ """Driver entry point to get the export info for an existing volume."""
+ LOG.debug('ensure_export: volume name: %s' % volume['name'])
+ return self._export_volume(volume)
+
+ def copy_image_to_volume(self, context, volume, image_service, image_id):
+ self.ensure_export(context, volume)
+ super(ZFSSAISCSIDriver, self).copy_image_to_volume(
+ context, volume, image_service, image_id)
+
+ def extend_volume(self, volume, new_size):
+ """Driver entry point to extent volume size."""
+ LOG.debug('extend_volume: volume name: %s' % volume['name'])
+ lcfg = self.configuration
+ self.zfssa.set_lun_props(lcfg.zfssa_pool,
+ lcfg.zfssa_project,
+ volume['name'],
+ volsize=new_size * units.Gi)
+
+ def create_cloned_volume(self, volume, src_vref):
+ """Create a clone of the specified volume."""
+ zfssa_snapshot = {'volume_name': src_vref['name'],
+ 'name': 'tmp-snapshot-%s' % volume['id']}
+ self.create_snapshot(zfssa_snapshot)
+ try:
+ self.create_volume_from_snapshot(volume, zfssa_snapshot)
+ except exception.VolumeBackendAPIException:
+ LOG.error(_('Clone Volume:'
+ '%(volume)s failed from source volume:'
+ '%(src_vref)s')
+ % {'volume': volume['name'],
+ 'src_vref': src_vref['name']})
+ # Cleanup snapshot
+ self.delete_snapshot(zfssa_snapshot)
+
+ def local_path(self, volume):
+ """Not implemented"""
+ pass
+
+ def backup_volume(self, context, backup, backup_service):
+ """Not implemented"""
+ pass
+
+ def restore_backup(self, context, backup, volume, backup_service):
+ """Not implemented"""
+ pass
+
+ def _verify_clone_size(self, snapshot, size):
+ """Check whether the clone size is the same as the parent volume"""
+ lcfg = self.configuration
+ lun = self.zfssa.get_lun(lcfg.zfssa_pool,
+ lcfg.zfssa_project,
+ snapshot['volume_name'])
+ return lun['size'] == size
--- /dev/null
+# 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 Proxy
+"""
+import json
+
+from cinder import exception
+from cinder.i18n import _
+from cinder.openstack.common import log
+from cinder.volume.drivers.zfssa import restclient
+
+LOG = log.getLogger(__name__)
+
+
+class ZFSSAApi(object):
+ """ZFSSA API proxy class"""
+
+ def __init__(self):
+ self.host = None
+ self.url = None
+ self.rclient = None
+
+ def __del__(self):
+ if self.rclient and self.rclient.islogin():
+ self.rclient.logout()
+
+ def _is_pool_owned(self, pdata):
+ """returns True if the pool's owner is the
+ same as the host.
+ """
+ svc = '/api/system/v1/version'
+ ret = self.rclient.get(svc)
+ if ret.status != restclient.Status.OK:
+ exception_msg = (_('Error getting version: '
+ 'svc: %(svc)s.'
+ 'Return code: %(ret.status)d '
+ 'Message: %(ret.data)s.')
+ % {'svc': svc,
+ 'ret.status': ret.status,
+ 'ret.data': ret.data})
+ LOG.error(exception_msg)
+ raise exception.VolumeBackendAPIException(data=exception_msg)
+
+ vdata = json.loads(ret.data)
+ return vdata['version']['asn'] == pdata['pool']['asn'] and \
+ vdata['version']['nodename'] == pdata['pool']['owner']
+
+ def set_host(self, host):
+ self.host = host
+ self.url = "https://" + self.host + ":215"
+ self.rclient = restclient.RestClientURL(self.url)
+
+ def login(self, auth_str):
+ """Login to the appliance"""
+ if self.rclient and not self.rclient.islogin():
+ self.rclient.login(auth_str)
+
+ def get_pool_stats(self, pool):
+ """Get space available and total properties of a pool
+ returns (avail, total).
+ """
+ svc = '/api/storage/v1/pools/' + pool
+ ret = self.rclient.get(svc)
+ if ret.status != restclient.Status.OK:
+ exception_msg = (_('Error Getting Pool Stats: '
+ 'Pool: %(pool)s '
+ 'Return code: %(ret.status)d '
+ 'Message: %(ret.data)s.')
+ % {'pool': pool,
+ 'ret.status': ret.status,
+ 'ret.data': ret.data})
+ LOG.error(exception_msg)
+ raise exception.InvalidVolume(reason=exception_msg)
+
+ val = json.loads(ret.data)
+
+ if not self._is_pool_owned(val):
+ exception_msg = (_('Error Pool ownership: '
+ 'Pool %(pool)s is not owned '
+ 'by %(host)s.')
+ % {'pool': pool,
+ 'host': self.host})
+ LOG.error(exception_msg)
+ raise exception.InvalidInput(reason=pool)
+
+ avail = val['pool']['usage']['available']
+ total = val['pool']['usage']['total']
+
+ return avail, total
+
+ def create_project(self, pool, project, compression=None, logbias=None):
+ """Create a project on a pool
+ Check first whether the pool exists.
+ """
+ self.verify_pool(pool)
+ svc = '/api/storage/v1/pools/' + pool + '/projects/' + project
+ ret = self.rclient.get(svc)
+ if ret.status != restclient.Status.OK:
+ svc = '/api/storage/v1/pools/' + pool + '/projects'
+ arg = {
+ 'name': project
+ }
+ if compression and compression != '':
+ arg.update({'compression': compression})
+ if logbias and logbias != '':
+ arg.update({'logbias': logbias})
+
+ ret = self.rclient.post(svc, arg)
+ if ret.status != restclient.Status.CREATED:
+ exception_msg = (_('Error Creating Project: '
+ '%(project)s on '
+ 'Pool: %(pool)s '
+ 'Return code: %(ret.status)d '
+ 'Message: %(ret.data)s .')
+ % {'project': project,
+ 'pool': pool,
+ 'ret.status': ret.status,
+ 'ret.data': ret.data})
+ LOG.error(exception_msg)
+ raise exception.VolumeBackendAPIException(data=exception_msg)
+
+ def create_initiator(self, initiator, alias, chapuser=None,
+ chapsecret=None):
+ """Create an iSCSI initiator."""
+
+ svc = '/api/san/v1/iscsi/initiators/alias=' + alias
+ ret = self.rclient.get(svc)
+ if ret.status != restclient.Status.OK:
+ svc = '/api/san/v1/iscsi/initiators'
+ arg = {
+ 'initiator': initiator,
+ 'alias': alias
+ }
+ if chapuser and chapuser != '' and chapsecret and chapsecret != '':
+ arg.update({'chapuser': chapuser,
+ 'chapsecret': chapsecret})
+
+ ret = self.rclient.post(svc, arg)
+ if ret.status != restclient.Status.CREATED:
+ exception_msg = (_('Error Creating Initator: '
+ '%(initiator)s on '
+ 'Alias: %(alias)s '
+ 'Return code: %(ret.status)d '
+ 'Message: %(ret.data)s .')
+ % {'initiator': initiator,
+ 'alias': alias,
+ 'ret.status': ret.status,
+ 'ret.data': ret.data})
+ LOG.error(exception_msg)
+ raise exception.VolumeBackendAPIException(data=exception_msg)
+
+ def add_to_initiatorgroup(self, initiator, initiatorgroup):
+ """Add an iSCSI initiator to initiatorgroup"""
+ svc = '/api/san/v1/iscsi/initiator-groups/' + initiatorgroup
+ ret = self.rclient.get(svc)
+ if ret.status != restclient.Status.OK:
+ svc = '/api/san/v1/iscsi/initiator-groups'
+ arg = {
+ 'name': initiatorgroup,
+ 'initiators': [initiator]
+ }
+ ret = self.rclient.post(svc, arg)
+ if ret.status != restclient.Status.CREATED:
+ exception_msg = (_('Error Adding Initator: '
+ '%(initiator)s on group'
+ 'InitiatorGroup: %(initiatorgroup)s '
+ 'Return code: %(ret.status)d '
+ 'Message: %(ret.data)s .')
+ % {'initiator': initiator,
+ 'initiatorgroup': initiatorgroup,
+ 'ret.status': ret.status,
+ 'ret.data': ret.data})
+ LOG.error(exception_msg)
+ raise exception.VolumeBackendAPIException(data=exception_msg)
+ else:
+ svc = '/api/san/v1/iscsi/initiator-groups/' + initiatorgroup
+ arg = {
+ 'initiators': [initiator]
+ }
+ ret = self.rclient.put(svc, arg)
+ if ret.status != restclient.Status.ACCEPTED:
+ exception_msg = (_('Error Adding Initator: '
+ '%(initiator)s on group'
+ 'InitiatorGroup: %(initiatorgroup)s '
+ 'Return code: %(ret.status)d '
+ 'Message: %(ret.data)s .')
+ % {'initiator': initiator,
+ 'initiatorgroup': initiatorgroup,
+ 'ret.status': ret.status,
+ 'ret.data': ret.data})
+ LOG.error(exception_msg)
+ raise exception.VolumeBackendAPIException(data=exception_msg)
+
+ def create_target(self, alias, interfaces=None, tchapuser=None,
+ tchapsecret=None):
+ """Create an iSCSI target.
+ interfaces: an array with network interfaces
+ tchapuser, tchapsecret: target's chapuser and chapsecret
+ returns target iqn
+ """
+ svc = '/api/san/v1/iscsi/targets/alias=' + alias
+ ret = self.rclient.get(svc)
+ if ret.status != restclient.Status.OK:
+ svc = '/api/san/v1/iscsi/targets'
+ arg = {
+ 'alias': alias
+ }
+
+ if tchapuser and tchapuser != '' and tchapsecret and \
+ tchapsecret != '':
+ arg.update({'targetchapuser': tchapuser,
+ 'targetchapsecret': tchapsecret,
+ 'auth': 'chap'})
+
+ if interfaces is not None and len(interfaces) > 0:
+ arg.update({'interfaces': interfaces})
+
+ ret = self.rclient.post(svc, arg)
+ if ret.status != restclient.Status.CREATED:
+ exception_msg = (_('Error Creating Target: '
+ '%(alias)s'
+ 'Return code: %(ret.status)d '
+ 'Message: %(ret.data)s .')
+ % {'alias': alias,
+ '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['target']['iqn']
+
+ def get_target(self, alias):
+ """Get an iSCSI target iqn."""
+ svc = '/api/san/v1/iscsi/targets/alias=' + alias
+ ret = self.rclient.get(svc)
+ if ret.status != restclient.Status.OK:
+ exception_msg = (_('Error Getting Target: '
+ '%(alias)s'
+ 'Return code: %(ret.status)d '
+ 'Message: %(ret.data)s .')
+ % {'alias': alias,
+ '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['target']['iqn']
+
+ def add_to_targetgroup(self, iqn, targetgroup):
+ """Add an iSCSI target to targetgroup."""
+ svc = '/api/san/v1/iscsi/target-groups/' + targetgroup
+ ret = self.rclient.get(svc)
+ if ret.status != restclient.Status.OK:
+ svccrt = '/api/san/v1/iscsi/target-groups'
+ arg = {
+ 'name': targetgroup,
+ 'targets': [iqn]
+ }
+
+ ret = self.rclient.post(svccrt, arg)
+ if ret.status != restclient.Status.CREATED:
+ exception_msg = (_('Error Creating TargetGroup: '
+ '%(targetgroup)s with'
+ 'IQN: %(iqn)s'
+ 'Return code: %(ret.status)d '
+ 'Message: %(ret.data)s .')
+ % {'targetgroup': targetgroup,
+ 'iqn': iqn,
+ 'ret.status': ret.status,
+ 'ret.data': ret.data})
+ LOG.error(exception_msg)
+ raise exception.VolumeBackendAPIException(data=exception_msg)
+
+ return
+
+ arg = {
+ 'targets': [iqn]
+ }
+
+ ret = self.rclient.put(svc, arg)
+ if ret.status != restclient.Status.ACCEPTED:
+ exception_msg = (_('Error Adding to TargetGroup: '
+ '%(targetgroup)s with'
+ 'IQN: %(iqn)s'
+ 'Return code: %(ret.status)d '
+ 'Message: %(ret.data)s.')
+ % {'targetgroup': targetgroup,
+ 'iqn': iqn,
+ 'ret.status': ret.status,
+ 'ret.data': ret.data})
+ LOG.error(exception_msg)
+ raise exception.VolumeBackendAPIException(data=exception_msg)
+
+ def verify_pool(self, pool):
+ """Checks whether pool exists."""
+ svc = '/api/storage/v1/pools/' + pool
+ ret = self.rclient.get(svc)
+ if ret.status != restclient.Status.OK:
+ exception_msg = (_('Error Verifying Pool: '
+ '%(pool)s '
+ 'Return code: %(ret.status)d '
+ 'Message: %(ret.data)s.')
+ % {'pool': pool,
+ 'ret.status': ret.status,
+ 'ret.data': ret.data})
+ LOG.error(exception_msg)
+ raise exception.VolumeBackendAPIException(data=exception_msg)
+
+ def verify_project(self, pool, project):
+ """Checks whether project exists."""
+ svc = '/api/storage/v1/pools/' + pool + '/projects/' + project
+ ret = self.rclient.get(svc)
+ if ret.status != restclient.Status.OK:
+ exception_msg = (_('Error Verifying '
+ 'Project: %(project)s on '
+ 'Pool: %(pool)s '
+ 'Return code: %(ret.status)d '
+ 'Message: %(ret.data)s.')
+ % {'project': project,
+ 'pool': pool,
+ 'ret.status': ret.status,
+ 'ret.data': ret.data})
+ LOG.error(exception_msg)
+ raise exception.VolumeBackendAPIException(data=exception_msg)
+
+ def verify_initiator(self, iqn):
+ """Check whether initiator iqn exists."""
+ svc = '/api/san/v1/iscsi/initiators/' + iqn
+ ret = self.rclient.get(svc)
+ if ret.status != restclient.Status.OK:
+ exception_msg = (_('Error Verifying '
+ 'Initiator: %(iqn)s '
+ 'Return code: %(ret.status)d '
+ 'Message: %(ret.data)s.')
+ % {'initiator': iqn,
+ 'ret.status': ret.status,
+ 'ret.data': ret.data})
+ LOG.error(exception_msg)
+ raise exception.VolumeBackendAPIException(data=exception_msg)
+
+ def verify_target(self, alias):
+ """Check whether target alias exists."""
+ svc = '/api/san/v1/iscsi/targets/alias=' + alias
+ ret = self.rclient.get(svc)
+ if ret.status != restclient.Status.OK:
+ exception_msg = (_('Error Verifying '
+ 'Target: %(alias)s '
+ 'Return code: %(ret.status)d '
+ 'Message: %(ret.data)s.')
+ % {'alias': alias,
+ 'ret.status': ret.status,
+ 'ret.data': ret.data})
+ LOG.error(exception_msg)
+ raise exception.VolumeBackendAPIException(data=exception_msg)
+
+ def create_lun(self, pool, project, lun, volsize, targetgroup,
+ volblocksize='8k', sparse=False, compression=None,
+ logbias=None):
+ """Create a LUN.
+ required - pool, project, lun, volsize, targetgroup.
+ optional - volblocksize, sparse, compression, logbias
+ """
+ svc = '/api/storage/v1/pools/' + pool + '/projects/' + \
+ project + '/luns'
+ arg = {
+ 'name': lun,
+ 'volsize': volsize,
+ 'targetgroup': targetgroup,
+ 'initiatorgroup': 'com.sun.ms.vss.hg.maskAll',
+ 'volblocksize': volblocksize,
+ 'sparse': sparse
+ }
+ if compression and compression != '':
+ arg.update({'compression': compression})
+ if logbias and logbias != '':
+ arg.update({'logbias': logbias})
+
+ ret = self.rclient.post(svc, arg)
+ if ret.status != restclient.Status.CREATED:
+ exception_msg = (_('Error Creating '
+ 'Volume: %(lun)s '
+ 'Size: %(size)s '
+ 'Return code: %(ret.status)d '
+ 'Message: %(ret.data)s.')
+ % {'lun': lun,
+ 'size': volsize,
+ 'ret.status': ret.status,
+ 'ret.data': ret.data})
+ LOG.error(exception_msg)
+ raise exception.VolumeBackendAPIException(data=exception_msg)
+
+ def get_lun(self, pool, project, lun):
+ """return iscsi lun properties."""
+ svc = '/api/storage/v1/pools/' + pool + '/projects/' + \
+ project + "/luns/" + lun
+ ret = self.rclient.get(svc)
+ if ret.status != restclient.Status.OK:
+ exception_msg = (_('Error Getting '
+ 'Volume: %(lun)s on '
+ 'Pool: %(pool)s '
+ 'Project: %(project)s '
+ 'Return code: %(ret.status)d '
+ 'Message: %(ret.data)s.')
+ % {'lun': lun,
+ '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)
+ ret = {
+ 'guid': val['lun']['lunguid'],
+ 'number': val['lun']['assignednumber'],
+ 'initiatorgroup': val['lun']['initiatorgroup'],
+ 'size': val['lun']['volsize'],
+ 'nodestroy': val['lun']['nodestroy']
+ }
+ if 'origin' in val['lun']:
+ ret.update({'origin': val['lun']['origin']})
+
+ return ret
+
+ def set_lun_initiatorgroup(self, pool, project, lun, initiatorgroup):
+ """Set the initiatorgroup property of a LUN."""
+ if initiatorgroup == '':
+ initiatorgroup = 'com.sun.ms.vss.hg.maskAll'
+
+ svc = '/api/storage/v1/pools/' + pool + '/projects/' + \
+ project + '/luns/' + lun
+ arg = {
+ 'initiatorgroup': initiatorgroup
+ }
+
+ ret = self.rclient.put(svc, arg)
+ if ret.status != restclient.Status.ACCEPTED:
+ exception_msg = (_('Error Setting '
+ 'Volume: %(lun)s to '
+ 'InitiatorGroup: %(initiatorgroup)s '
+ 'Pool: %(pool)s '
+ 'Project: %(project)s '
+ 'Return code: %(ret.status)d '
+ 'Message: %(ret.data)s.')
+ % {'lun': lun,
+ 'initiatorgroup': initiatorgroup,
+ 'pool': pool,
+ 'project': project,
+ 'ret.status': ret.status,
+ 'ret.data': ret.data})
+ LOG.error(exception_msg)
+
+ def delete_lun(self, pool, project, lun):
+ """delete iscsi lun."""
+ svc = '/api/storage/v1/pools/' + pool + '/projects/' + \
+ project + '/luns/' + lun
+
+ ret = self.rclient.delete(svc)
+ if ret.status != restclient.Status.NO_CONTENT:
+ exception_msg = (_('Error Deleting '
+ 'Volume: %(lun)s to '
+ 'Pool: %(pool)s '
+ 'Project: %(project)s '
+ 'Return code: %(ret.status)d '
+ 'Message: %(ret.data)s.')
+ % {'lun': lun,
+ 'pool': pool,
+ 'project': project,
+ 'ret.status': ret.status,
+ 'ret.data': ret.data})
+ LOG.error(exception_msg)
+
+ def create_snapshot(self, pool, project, lun, snapshot):
+ """create snapshot."""
+ svc = '/api/storage/v1/pools/' + pool + '/projects/' + \
+ project + '/luns/' + lun + '/snapshots'
+ arg = {
+ 'name': snapshot
+ }
+
+ ret = self.rclient.post(svc, arg)
+ if ret.status != restclient.Status.CREATED:
+ exception_msg = (_('Error Creating '
+ 'Snapshot: %(snapshot)s on'
+ 'Volume: %(lun)s to '
+ 'Pool: %(pool)s '
+ 'Project: %(project)s '
+ 'Return code: %(ret.status)d '
+ 'Message: %(ret.data)s.')
+ % {'snapshot': snapshot,
+ 'lun': lun,
+ '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, lun, snapshot):
+ """delete snapshot."""
+ svc = '/api/storage/v1/pools/' + pool + '/projects/' + \
+ project + '/luns/' + lun + '/snapshots/' + snapshot
+
+ ret = self.rclient.delete(svc)
+ if ret.status != restclient.Status.NO_CONTENT:
+ exception_msg = (_('Error Deleting '
+ 'Snapshot: %(snapshot)s on '
+ 'Volume: %(lun)s to '
+ 'Pool: %(pool)s '
+ 'Project: %(project)s '
+ 'Return code: %(ret.status)d '
+ 'Message: %(ret.data)s.')
+ % {'snapshot': snapshot,
+ 'lun': lun,
+ 'pool': pool,
+ 'project': project,
+ 'ret.status': ret.status,
+ 'ret.data': ret.data})
+ LOG.error(exception_msg)
+ raise exception.VolumeBackendAPIException(data=exception_msg)
+
+ def clone_snapshot(self, pool, project, lun, snapshot, clone):
+ """clone snapshot."""
+ svc = '/api/storage/v1/pools/' + pool + '/projects/' + \
+ project + '/luns/' + lun + '/snapshots/' + snapshot + '/clone'
+ arg = {
+ 'project': project,
+ 'share': clone,
+ 'nodestroy': True
+ }
+
+ ret = self.rclient.put(svc, arg)
+ if ret.status != restclient.Status.CREATED:
+ exception_msg = (_('Error Cloning '
+ 'Snapshot: %(snapshot)s on '
+ 'Volume: %(lun)s of '
+ 'Pool: %(pool)s '
+ 'Project: %(project)s '
+ 'Return code: %(ret.status)d '
+ 'Message: %(ret.data)s.')
+ % {'snapshot': snapshot,
+ 'lun': lun,
+ 'pool': pool,
+ 'project': project,
+ 'ret.status': ret.status,
+ 'ret.data': ret.data})
+ LOG.error(exception_msg)
+ raise exception.VolumeBackendAPIException(data=exception_msg)
+
+ def set_lun_props(self, pool, project, lun, **kargs):
+ """set lun properties."""
+ svc = '/api/storage/v1/pools/' + pool + '/projects/' + \
+ project + '/luns/' + lun
+ if kargs is None:
+ return
+
+ ret = self.rclient.put(svc, kargs)
+ if ret.status != restclient.Status.ACCEPTED:
+ exception_msg = (_('Error Setting props '
+ 'Props: %(props)s on '
+ 'Volume: %(lun)s of '
+ 'Pool: %(pool)s '
+ 'Project: %(project)s '
+ 'Return code: %(ret.status)d '
+ 'Message: %(ret.data)s.')
+ % {'props': kargs,
+ 'lun': lun,
+ 'pool': pool,
+ 'project': project,
+ 'ret.status': ret.status,
+ 'ret.data': ret.data})
+ LOG.error(exception_msg)
+ raise exception.VolumeBackendAPIException(data=exception_msg)
+
+ def has_clones(self, pool, project, lun, snapshot):
+ """Checks whether snapshot has clones or not."""
+ svc = '/api/storage/v1/pools/' + pool + '/projects/' + \
+ project + '/luns/' + lun + '/snapshots/' + snapshot
+
+ ret = self.rclient.get(svc)
+ if ret.status != restclient.Status.OK:
+ exception_msg = (_('Error Getting '
+ 'Snapshot: %(snapshot)s on '
+ 'Volume: %(lun)s to '
+ 'Pool: %(pool)s '
+ 'Project: %(project)s '
+ 'Return code: %(ret.status)d '
+ 'Message: %(ret.data)s.')
+ % {'snapshot': snapshot,
+ 'lun': lun,
+ '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['snapshot']['numclones'] != 0
#zadara_vpsa_allow_nonexistent_delete=true
+#
+# Options defined in cinder.volume.drivers.zfssa.zfssaiscsi
+#
+
+# Storage pool name. (string value)
+#zfssa_pool=<None>
+
+# Project name. (string value)
+#zfssa_project=<None>
+
+# Block size: 512, 1k, 2k, 4k, 8k, 16k, 32k, 64k, 128k.
+# (string value)
+#zfssa_lun_volblocksize=8k
+
+# Flag to enable sparse (thin-provisioned): True, False.
+# (boolean value)
+#zfssa_lun_sparse=false
+
+# Data compression-off, lzjb, gzip-2, gzip, gzip-9. (string
+# value)
+#zfssa_lun_compression=
+
+# Synchronous write bias-latency, throughput. (string value)
+#zfssa_lun_logbias=
+
+# iSCSI initiator group. (string value)
+#zfssa_initiator_group=
+
+# iSCSI initiator IQNs. (comma separated) (string value)
+#zfssa_initiator=
+
+# iSCSI initiator CHAP user. (string value)
+#zfssa_initiator_user=
+
+# iSCSI initiator CHAP password. (string value)
+#zfssa_initiator_password=
+
+# iSCSI target group name. (string value)
+#zfssa_target_group=tgt-grp
+
+# iSCSI target CHAP user. (string value)
+#zfssa_target_user=
+
+# iSCSI target CHAP password. (string value)
+#zfssa_target_password=
+
+# iSCSI target portal (Data-IP:Port, w.x.y.z:3260). (string
+# value)
+#zfssa_target_portal=<None>
+
+# Network interfaces of iSCSI targets. (comma separated)
+# (string value)
+#zfssa_target_interfaces=<None>
+
+
#
# Options defined in cinder.volume.manager
#