From 7f95e011e3c6828f18556e1e56ea5eaae462043f Mon Sep 17 00:00:00 2001 From: Juan Zuluaga Date: Thu, 10 Jul 2014 13:15:27 -0400 Subject: [PATCH] Add Oracle ZFS Storage Appliance ISCSI Driver 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 | 305 +++++++++++ cinder/volume/drivers/zfssa/__init__.py | 0 cinder/volume/drivers/zfssa/restclient.py | 355 +++++++++++++ cinder/volume/drivers/zfssa/zfssaiscsi.py | 385 ++++++++++++++ cinder/volume/drivers/zfssa/zfssarest.py | 613 ++++++++++++++++++++++ etc/cinder/cinder.conf.sample | 55 ++ 6 files changed, 1713 insertions(+) create mode 100644 cinder/tests/test_zfssa.py create mode 100644 cinder/volume/drivers/zfssa/__init__.py create mode 100644 cinder/volume/drivers/zfssa/restclient.py create mode 100644 cinder/volume/drivers/zfssa/zfssaiscsi.py create mode 100644 cinder/volume/drivers/zfssa/zfssarest.py diff --git a/cinder/tests/test_zfssa.py b/cinder/tests/test_zfssa.py new file mode 100644 index 000000000..c1bd1441e --- /dev/null +++ b/cinder/tests/test_zfssa.py @@ -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 index 000000000..e69de29bb diff --git a/cinder/volume/drivers/zfssa/restclient.py b/cinder/volume/drivers/zfssa/restclient.py new file mode 100644 index 000000000..e904075c6 --- /dev/null +++ b/cinder/volume/drivers/zfssa/restclient.py @@ -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 index 000000000..b92ba2022 --- /dev/null +++ b/cinder/volume/drivers/zfssa/zfssaiscsi.py @@ -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 index 000000000..e64f8edd7 --- /dev/null +++ b/cinder/volume/drivers/zfssa/zfssarest.py @@ -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 diff --git a/etc/cinder/cinder.conf.sample b/etc/cinder/cinder.conf.sample index 90ed37e7a..501842d63 100644 --- a/etc/cinder/cinder.conf.sample +++ b/etc/cinder/cinder.conf.sample @@ -1992,6 +1992,61 @@ #zadara_vpsa_allow_nonexistent_delete=true +# +# Options defined in cinder.volume.drivers.zfssa.zfssaiscsi +# + +# Storage pool name. (string value) +#zfssa_pool= + +# Project name. (string value) +#zfssa_project= + +# 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= + +# Network interfaces of iSCSI targets. (comma separated) +# (string value) +#zfssa_target_interfaces= + + # # Options defined in cinder.volume.manager # -- 2.45.2