Merge "Add Oracle ZFS Storage Appliance ISCSI Driver"
This commit is contained in:
305
cinder/tests/test_zfssa.py
Normal file
305
cinder/tests/test_zfssa.py
Normal file
@@ -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()
|
||||||
0
cinder/volume/drivers/zfssa/__init__.py
Normal file
0
cinder/volume/drivers/zfssa/__init__.py
Normal file
355
cinder/volume/drivers/zfssa/restclient.py
Normal file
355
cinder/volume/drivers/zfssa/restclient.py
Normal file
@@ -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)
|
||||||
385
cinder/volume/drivers/zfssa/zfssaiscsi.py
Normal file
385
cinder/volume/drivers/zfssa/zfssaiscsi.py
Normal file
@@ -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
|
||||||
613
cinder/volume/drivers/zfssa/zfssarest.py
Normal file
613
cinder/volume/drivers/zfssa/zfssarest.py
Normal file
@@ -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
|
||||||
@@ -1998,6 +1998,61 @@
|
|||||||
#zadara_vpsa_allow_nonexistent_delete=true
|
#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
|
# Options defined in cinder.volume.manager
|
||||||
#
|
#
|
||||||
|
|||||||
Reference in New Issue
Block a user