]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add Oracle ZFS Storage Appliance ISCSI Driver
authorJuan Zuluaga <juan.c.zuluaga@oracle.com>
Thu, 10 Jul 2014 17:15:27 +0000 (13:15 -0400)
committerJuan Zuluaga <juan.c.zuluaga@oracle.com>
Mon, 25 Aug 2014 01:42:08 +0000 (21:42 -0400)
ZFSSA ISCSI Driver is designed for ZFS Storage Appliance product
line (ZS3-2, ZS3-4, ZS3-ES, 7420 and 7320).
It uses REST API to communicate out of band with the storage controller
to perform the following:
* Create/Delete Volume
* Extend Volume
* Create/Delete Snapshot
* Create Volume from Snapshot
* Delete Volume Snapshot
* Attach/Detach Volume
* Get Volume Stats
* Clone Volume

Update cinder.conf.sample to include ZFS Storage Appliance
properties.

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

Change-Id: I2925c3e8cbe6f9d7a81ca70fcac7709714f07962
Implements: blueprint oracle-zfssa-cinder-driver

cinder/tests/test_zfssa.py [new file with mode: 0644]
cinder/volume/drivers/zfssa/__init__.py [new file with mode: 0644]
cinder/volume/drivers/zfssa/restclient.py [new file with mode: 0644]
cinder/volume/drivers/zfssa/zfssaiscsi.py [new file with mode: 0644]
cinder/volume/drivers/zfssa/zfssarest.py [new file with mode: 0644]
etc/cinder/cinder.conf.sample

diff --git a/cinder/tests/test_zfssa.py b/cinder/tests/test_zfssa.py
new file mode 100644 (file)
index 0000000..c1bd144
--- /dev/null
@@ -0,0 +1,305 @@
+# 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()
diff --git a/cinder/volume/drivers/zfssa/__init__.py b/cinder/volume/drivers/zfssa/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/cinder/volume/drivers/zfssa/restclient.py b/cinder/volume/drivers/zfssa/restclient.py
new file mode 100644 (file)
index 0000000..e904075
--- /dev/null
@@ -0,0 +1,355 @@
+# 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)
diff --git a/cinder/volume/drivers/zfssa/zfssaiscsi.py b/cinder/volume/drivers/zfssa/zfssaiscsi.py
new file mode 100644 (file)
index 0000000..b92ba20
--- /dev/null
@@ -0,0 +1,385 @@
+# 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
diff --git a/cinder/volume/drivers/zfssa/zfssarest.py b/cinder/volume/drivers/zfssa/zfssarest.py
new file mode 100644 (file)
index 0000000..e64f8ed
--- /dev/null
@@ -0,0 +1,613 @@
+# 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
index 90ed37e7aef9e216f33002117eb8850c57f0dd1f..501842d637a9ec03ba2456677697ee2a3e013acc 100644 (file)
 #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
 #