Remove unsupported NexentaEdge driver
This driver was marked as unsupported in the Ocata cycle due to no CI. CI issues have not been resolved, so this now removes the driver. Checking name: Nexenta Edge CI last seen: 2017-02-07 14:54:01 (0:30:45 old) last success: NOT FOUND success rate: 0% Change-Id: I61672882a8c77f430c85393d219174788d34efcc
This commit is contained in:
@@ -1,273 +0,0 @@
|
||||
#
|
||||
# Copyright 2015 Nexenta Systems, Inc.
|
||||
# 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.
|
||||
|
||||
import json
|
||||
import mock
|
||||
|
||||
from cinder import context
|
||||
from cinder import exception
|
||||
from cinder import test
|
||||
from cinder.volume import configuration as conf
|
||||
from cinder.volume.drivers.nexenta.nexentaedge import iscsi
|
||||
|
||||
NEDGE_URL = 'service/isc/iscsi'
|
||||
NEDGE_BUCKET = 'c/t/bk'
|
||||
NEDGE_SERVICE = 'isc'
|
||||
NEDGE_BLOCKSIZE = 4096
|
||||
NEDGE_CHUNKSIZE = 16384
|
||||
|
||||
MOCK_VOL = {
|
||||
'id': 'vol1',
|
||||
'name': 'vol1',
|
||||
'size': 1
|
||||
}
|
||||
MOCK_VOL2 = {
|
||||
'id': 'vol2',
|
||||
'name': 'vol2',
|
||||
'size': 1
|
||||
}
|
||||
MOCK_VOL3 = {
|
||||
'id': 'vol3',
|
||||
'name': 'vol3',
|
||||
'size': 2
|
||||
}
|
||||
MOCK_SNAP = {
|
||||
'id': 'snap1',
|
||||
'name': 'snap1',
|
||||
'volume_name': 'vol1'
|
||||
}
|
||||
NEW_VOL_SIZE = 2
|
||||
ISCSI_TARGET_NAME = 'iscsi_target_name'
|
||||
ISCSI_TARGET_STATUS = 'Target 1: ' + ISCSI_TARGET_NAME
|
||||
|
||||
|
||||
class TestNexentaEdgeISCSIDriver(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
def _safe_get(opt):
|
||||
return getattr(self.cfg, opt)
|
||||
super(TestNexentaEdgeISCSIDriver, self).setUp()
|
||||
self.context = context.get_admin_context()
|
||||
self.cfg = mock.Mock(spec=conf.Configuration)
|
||||
self.cfg.safe_get = mock.Mock(side_effect=_safe_get)
|
||||
self.cfg.trace_flags = 'fake_trace_flags'
|
||||
self.cfg.driver_data_namespace = 'fake_driver_data_namespace'
|
||||
self.cfg.nexenta_client_address = '0.0.0.0'
|
||||
self.cfg.nexenta_rest_address = '0.0.0.0'
|
||||
self.cfg.nexenta_rest_port = 8080
|
||||
self.cfg.nexenta_rest_protocol = 'http'
|
||||
self.cfg.nexenta_iscsi_target_portal_port = 3260
|
||||
self.cfg.nexenta_rest_user = 'admin'
|
||||
self.cfg.nexenta_rest_password = 'admin'
|
||||
self.cfg.nexenta_lun_container = NEDGE_BUCKET
|
||||
self.cfg.nexenta_iscsi_service = NEDGE_SERVICE
|
||||
self.cfg.nexenta_blocksize = NEDGE_BLOCKSIZE
|
||||
self.cfg.nexenta_chunksize = NEDGE_CHUNKSIZE
|
||||
self.cfg.replication_device = []
|
||||
|
||||
mock_exec = mock.Mock()
|
||||
mock_exec.return_value = ('', '')
|
||||
self.driver = iscsi.NexentaEdgeISCSIDriver(execute=mock_exec,
|
||||
configuration=self.cfg)
|
||||
self.api_patcher = mock.patch('cinder.volume.drivers.nexenta.'
|
||||
'nexentaedge.jsonrpc.'
|
||||
'NexentaEdgeJSONProxy.__call__')
|
||||
self.mock_api = self.api_patcher.start()
|
||||
|
||||
self.mock_api.return_value = {
|
||||
'data': {'value': ISCSI_TARGET_STATUS}
|
||||
}
|
||||
self.driver.do_setup(self.context)
|
||||
|
||||
self.addCleanup(self.api_patcher.stop)
|
||||
|
||||
def test_check_do_setup(self):
|
||||
self.assertEqual(ISCSI_TARGET_NAME, self.driver.target_name)
|
||||
|
||||
def test_check_do_setup__vip(self):
|
||||
first_vip = '/'.join((self.cfg.nexenta_client_address, '32'))
|
||||
vips = [
|
||||
[{'ip': first_vip}],
|
||||
[{'ip': '0.0.0.1/32'}]
|
||||
]
|
||||
|
||||
def my_side_effect(*args, **kwargs):
|
||||
if args[0] == 'service/isc/iscsi/status':
|
||||
return {'data': {'value': ISCSI_TARGET_STATUS}}
|
||||
else:
|
||||
return {'data': {'X-VIPS': json.dumps(vips)}}
|
||||
|
||||
self.mock_api.side_effect = my_side_effect
|
||||
self.driver.do_setup(self.context)
|
||||
self.assertEqual(self.driver.ha_vip, first_vip)
|
||||
|
||||
def test_check_do_setup__vip_not_in_xvips(self):
|
||||
first_vip = '1.2.3.4/32'
|
||||
vips = [
|
||||
[{'ip': first_vip}],
|
||||
[{'ip': '0.0.0.1/32'}]
|
||||
]
|
||||
|
||||
def my_side_effect(*args, **kwargs):
|
||||
if args[0] == 'service/isc/iscsi/status':
|
||||
return {'data': {'value': ISCSI_TARGET_STATUS}}
|
||||
else:
|
||||
return {'data': {'X-VIPS': json.dumps(vips)}}
|
||||
|
||||
self.mock_api.side_effect = my_side_effect
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
self.driver.do_setup, self.context)
|
||||
|
||||
def test_check_do_setup__vip_no_client_address(self):
|
||||
self.cfg.nexenta_client_address = None
|
||||
first_vip = '1.2.3.4/32'
|
||||
vips = [
|
||||
[{'ip': first_vip}]
|
||||
]
|
||||
|
||||
def my_side_effect(*args, **kwargs):
|
||||
if args[0] == 'service/isc/iscsi/status':
|
||||
return {'data': {'value': ISCSI_TARGET_STATUS}}
|
||||
else:
|
||||
return {'data': {'X-VIPS': json.dumps(vips)}}
|
||||
|
||||
self.mock_api.side_effect = my_side_effect
|
||||
self.driver.do_setup(self.context)
|
||||
self.assertEqual(self.driver.ha_vip, first_vip)
|
||||
|
||||
def test_check_do_setup__vip_no_client_address_2_xvips(self):
|
||||
self.cfg.nexenta_client_address = None
|
||||
first_vip = '1.2.3.4/32'
|
||||
vips = [
|
||||
[{'ip': first_vip}],
|
||||
[{'ip': '0.0.0.1/32'}]
|
||||
]
|
||||
|
||||
def my_side_effect(*args, **kwargs):
|
||||
if args[0] == 'service/isc/iscsi/status':
|
||||
return {'data': {'value': ISCSI_TARGET_STATUS}}
|
||||
else:
|
||||
return {'data': {'X-VIPS': json.dumps(vips)}}
|
||||
|
||||
self.mock_api.side_effect = my_side_effect
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
self.driver.do_setup, self.context)
|
||||
|
||||
def test_create_volume(self):
|
||||
self.driver.create_volume(MOCK_VOL)
|
||||
self.mock_api.assert_called_with(NEDGE_URL, {
|
||||
'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id'],
|
||||
'volSizeMB': MOCK_VOL['size'] * 1024,
|
||||
'blockSize': NEDGE_BLOCKSIZE,
|
||||
'chunkSize': NEDGE_CHUNKSIZE
|
||||
})
|
||||
|
||||
def test_create_volume__vip(self):
|
||||
self.driver.ha_vip = self.cfg.nexenta_client_address + '/32'
|
||||
self.driver.create_volume(MOCK_VOL)
|
||||
self.mock_api.assert_called_with(NEDGE_URL, {
|
||||
'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id'],
|
||||
'volSizeMB': MOCK_VOL['size'] * 1024,
|
||||
'blockSize': NEDGE_BLOCKSIZE,
|
||||
'chunkSize': NEDGE_CHUNKSIZE,
|
||||
'vip': self.cfg.nexenta_client_address + '/32'
|
||||
})
|
||||
|
||||
def test_create_volume_fail(self):
|
||||
self.mock_api.side_effect = RuntimeError
|
||||
self.assertRaises(RuntimeError, self.driver.create_volume, MOCK_VOL)
|
||||
|
||||
def test_delete_volume(self):
|
||||
self.mock_api.side_effect = exception.VolumeBackendAPIException(
|
||||
'No volume')
|
||||
self.driver.delete_volume(MOCK_VOL)
|
||||
self.mock_api.assert_called_with(NEDGE_URL, {
|
||||
'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id']
|
||||
})
|
||||
|
||||
def test_delete_volume_fail(self):
|
||||
self.mock_api.side_effect = RuntimeError
|
||||
self.assertRaises(RuntimeError, self.driver.delete_volume, MOCK_VOL)
|
||||
|
||||
def test_extend_volume(self):
|
||||
self.driver.extend_volume(MOCK_VOL, NEW_VOL_SIZE)
|
||||
self.mock_api.assert_called_with(NEDGE_URL + '/resize', {
|
||||
'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id'],
|
||||
'newSizeMB': NEW_VOL_SIZE * 1024
|
||||
})
|
||||
|
||||
def test_extend_volume_fail(self):
|
||||
self.mock_api.side_effect = RuntimeError
|
||||
self.assertRaises(RuntimeError, self.driver.extend_volume,
|
||||
MOCK_VOL, NEW_VOL_SIZE)
|
||||
|
||||
def test_create_snapshot(self):
|
||||
self.driver.create_snapshot(MOCK_SNAP)
|
||||
self.mock_api.assert_called_with(NEDGE_URL + '/snapshot', {
|
||||
'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id'],
|
||||
'snapName': MOCK_SNAP['id']
|
||||
})
|
||||
|
||||
def test_create_snapshot_fail(self):
|
||||
self.mock_api.side_effect = RuntimeError
|
||||
self.assertRaises(RuntimeError, self.driver.create_snapshot, MOCK_SNAP)
|
||||
|
||||
def test_delete_snapshot(self):
|
||||
self.driver.delete_snapshot(MOCK_SNAP)
|
||||
self.mock_api.assert_called_with(NEDGE_URL + '/snapshot', {
|
||||
'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id'],
|
||||
'snapName': MOCK_SNAP['id']
|
||||
})
|
||||
|
||||
def test_delete_snapshot_fail(self):
|
||||
self.mock_api.side_effect = RuntimeError
|
||||
self.assertRaises(RuntimeError, self.driver.delete_snapshot, MOCK_SNAP)
|
||||
|
||||
def test_create_volume_from_snapshot(self):
|
||||
self.driver.create_volume_from_snapshot(MOCK_VOL2, MOCK_SNAP)
|
||||
self.mock_api.assert_called_with(NEDGE_URL + '/snapshot/clone', {
|
||||
'objectPath': NEDGE_BUCKET + '/' + MOCK_SNAP['volume_name'],
|
||||
'clonePath': NEDGE_BUCKET + '/' + MOCK_VOL2['id'],
|
||||
'snapName': MOCK_SNAP['id']
|
||||
})
|
||||
|
||||
def test_create_volume_from_snapshot_fail(self):
|
||||
self.mock_api.side_effect = RuntimeError
|
||||
self.assertRaises(RuntimeError,
|
||||
self.driver.create_volume_from_snapshot,
|
||||
MOCK_VOL2, MOCK_SNAP)
|
||||
|
||||
def test_create_cloned_volume(self):
|
||||
self.driver.create_cloned_volume(MOCK_VOL2, MOCK_VOL)
|
||||
self.mock_api.assert_called_with(NEDGE_URL, {
|
||||
'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL2['id'],
|
||||
'volSizeMB': MOCK_VOL2['size'] * 1024,
|
||||
'blockSize': NEDGE_BLOCKSIZE,
|
||||
'chunkSize': NEDGE_CHUNKSIZE
|
||||
})
|
||||
|
||||
def test_create_cloned_volume_larger(self):
|
||||
self.driver.create_cloned_volume(MOCK_VOL3, MOCK_VOL)
|
||||
# ignore the clone call, this has been tested before
|
||||
self.mock_api.assert_called_with(NEDGE_URL + '/resize', {
|
||||
'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL3['id'],
|
||||
'newSizeMB': MOCK_VOL3['size'] * 1024
|
||||
})
|
||||
|
||||
def test_create_cloned_volume_fail(self):
|
||||
self.mock_api.side_effect = RuntimeError
|
||||
self.assertRaises(RuntimeError, self.driver.create_cloned_volume,
|
||||
MOCK_VOL2, MOCK_VOL)
|
||||
@@ -1,504 +0,0 @@
|
||||
# Copyright 2016 Nexenta Systems, Inc.
|
||||
# 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.
|
||||
|
||||
import socket
|
||||
|
||||
import mock
|
||||
from mock import patch
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import units
|
||||
|
||||
from cinder import context
|
||||
from cinder import exception
|
||||
from cinder import test
|
||||
from cinder.volume import configuration as conf
|
||||
from cinder.volume.drivers.nexenta.nexentaedge import jsonrpc
|
||||
from cinder.volume.drivers.nexenta.nexentaedge import nbd
|
||||
|
||||
|
||||
class FakeResponse(object):
|
||||
|
||||
def __init__(self, response):
|
||||
self.response = response
|
||||
super(FakeResponse, self).__init__()
|
||||
|
||||
def json(self):
|
||||
return self.response
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
|
||||
class RequestParams(object):
|
||||
def __init__(self, scheme, host, port, user, password):
|
||||
self.scheme = scheme.lower()
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.user = user
|
||||
self.password = password
|
||||
|
||||
def url(self, path=''):
|
||||
return '%s://%s:%s/%s' % (
|
||||
self.scheme, self.host, self.port, path)
|
||||
|
||||
def build_post_args(self, args):
|
||||
return jsonutils.dumps(args)
|
||||
|
||||
|
||||
class TestNexentaEdgeNBDDriver(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
def _safe_get(opt):
|
||||
return getattr(self.cfg, opt)
|
||||
super(TestNexentaEdgeNBDDriver, self).setUp()
|
||||
self.cfg = mock.Mock(spec=conf.Configuration)
|
||||
self.cfg.safe_get = mock.Mock(side_effect=_safe_get)
|
||||
self.cfg.trace_flags = 'fake_trace_flags'
|
||||
self.cfg.driver_data_namespace = 'fake_driver_data_namespace'
|
||||
self.cfg.nexenta_rest_protocol = 'http'
|
||||
self.cfg.nexenta_rest_address = '127.0.0.1'
|
||||
self.cfg.nexenta_rest_port = 8080
|
||||
self.cfg.nexenta_rest_user = 'admin'
|
||||
self.cfg.nexenta_rest_password = '0'
|
||||
self.cfg.nexenta_lun_container = 'cluster/tenant/bucket'
|
||||
self.cfg.nexenta_nbd_symlinks_dir = '/dev/disk/by-path'
|
||||
self.cfg.volume_dd_blocksize = 512
|
||||
self.cfg.nexenta_blocksize = 512
|
||||
self.cfg.nexenta_chunksize = 4096
|
||||
self.cfg.reserved_percentage = 0
|
||||
self.cfg.replication_device = []
|
||||
|
||||
self.ctx = context.get_admin_context()
|
||||
self.drv = nbd.NexentaEdgeNBDDriver(configuration=self.cfg)
|
||||
|
||||
session = mock.Mock()
|
||||
session.get = mock.Mock()
|
||||
session.post = mock.Mock()
|
||||
session.put = mock.Mock()
|
||||
session.delete = mock.Mock()
|
||||
self.drv.do_setup(self.ctx)
|
||||
self.drv.restapi.session = session
|
||||
self.mock_api = session
|
||||
|
||||
self.request_params = RequestParams(
|
||||
'http', self.cfg.nexenta_rest_address, self.cfg.nexenta_rest_port,
|
||||
self.cfg.nexenta_rest_user, self.cfg.nexenta_rest_password)
|
||||
|
||||
def test_check_do_setup__symlinks_dir_not_specified(self):
|
||||
self.drv.symlinks_dir = None
|
||||
self.assertRaises(
|
||||
exception.NexentaException, self.drv.check_for_setup_error)
|
||||
|
||||
def test_check_do_setup__symlinks_dir_doesnt_exist(self):
|
||||
self.drv.symlinks_dir = '/some/random/path'
|
||||
self.assertRaises(
|
||||
exception.NexentaException, self.drv.check_for_setup_error)
|
||||
|
||||
@patch('os.path.exists')
|
||||
def test_check_do_setup__empty_response(self, exists):
|
||||
self.mock_api.get.return_value = FakeResponse({})
|
||||
exists.return_value = True
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
self.drv.check_for_setup_error)
|
||||
|
||||
@patch('os.path.exists')
|
||||
def test_check_do_setup(self, exists):
|
||||
self.mock_api.get.return_value = FakeResponse({'response': 'OK'})
|
||||
exists.return_value = True
|
||||
self.drv.check_for_setup_error()
|
||||
self.mock_api.get.assert_any_call(
|
||||
self.request_params.url(self.drv.bucket_url + '/objects/'),
|
||||
timeout=jsonrpc.TIMEOUT)
|
||||
|
||||
def test_local_path__error(self):
|
||||
self.drv._get_nbd_number = lambda volume_: -1
|
||||
volume = {'name': 'volume'}
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
self.drv.local_path, volume)
|
||||
|
||||
def test_local_path(self):
|
||||
volume = {
|
||||
'name': 'volume',
|
||||
'host': 'myhost@backend#pool'
|
||||
}
|
||||
_get_host_info__response = {
|
||||
'stats': {
|
||||
'servers': {
|
||||
'host1': {
|
||||
'hostname': 'host1',
|
||||
'ipv6addr': 'fe80::fc16:3eff:fedb:bd69'},
|
||||
'host2': {
|
||||
'hostname': 'myhost',
|
||||
'ipv6addr': 'fe80::fc16:3eff:fedb:bd68'}
|
||||
}
|
||||
}
|
||||
}
|
||||
_get_nbd_devices__response = {
|
||||
'value': jsonutils.dumps([
|
||||
{
|
||||
'objectPath': '/'.join(
|
||||
(self.cfg.nexenta_lun_container, 'some_volume')),
|
||||
'number': 1
|
||||
},
|
||||
{
|
||||
'objectPath': '/'.join(
|
||||
(self.cfg.nexenta_lun_container, volume['name'])),
|
||||
'number': 2
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
def my_side_effect(*args, **kwargs):
|
||||
if args[0] == self.request_params.url('system/stats'):
|
||||
return FakeResponse({'response': _get_host_info__response})
|
||||
elif args[0].startswith(
|
||||
self.request_params.url('sysconfig/nbd/devices')):
|
||||
return FakeResponse({'response': _get_nbd_devices__response})
|
||||
else:
|
||||
raise Exception('Unexpected request')
|
||||
|
||||
self.mock_api.get.side_effect = my_side_effect
|
||||
self.drv.local_path(volume)
|
||||
|
||||
def test_local_path__host_not_found(self):
|
||||
volume = {
|
||||
'name': 'volume',
|
||||
'host': 'unknown-host@backend#pool'
|
||||
}
|
||||
_get_host_info__response = {
|
||||
'stats': {
|
||||
'servers': {
|
||||
'host1': {
|
||||
'hostname': 'host1',
|
||||
'ipv6addr': 'fe80::fc16:3eff:fedb:bd69'},
|
||||
'host2': {
|
||||
'hostname': 'myhost',
|
||||
'ipv6addr': 'fe80::fc16:3eff:fedb:bd68'}
|
||||
}
|
||||
}
|
||||
}
|
||||
_get_nbd_devices__response = {
|
||||
'value': jsonutils.dumps([
|
||||
{
|
||||
'objectPath': '/'.join(
|
||||
(self.cfg.nexenta_lun_container, 'some_volume')),
|
||||
'number': 1
|
||||
},
|
||||
{
|
||||
'objectPath': '/'.join(
|
||||
(self.cfg.nexenta_lun_container, volume['name'])),
|
||||
'number': 2
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
def my_side_effect(*args, **kwargs):
|
||||
if args[0] == self.request_params.url('system/stats'):
|
||||
return FakeResponse({'response': _get_host_info__response})
|
||||
elif args[0].startswith(
|
||||
self.request_params.url('sysconfig/nbd/devices')):
|
||||
return FakeResponse({'response': _get_nbd_devices__response})
|
||||
else:
|
||||
raise Exception('Unexpected request')
|
||||
|
||||
self.mock_api.get.side_effect = my_side_effect
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
self.drv.local_path, volume)
|
||||
|
||||
@patch('cinder.utils.execute')
|
||||
def test_create_volume(self, execute):
|
||||
self.mock_api.post.returning_value = FakeResponse({})
|
||||
volume = {
|
||||
'host': 'host@backend#pool info',
|
||||
'size': 1,
|
||||
'name': 'volume'
|
||||
}
|
||||
number = 5
|
||||
remote_url = ''
|
||||
self.drv._get_remote_url = lambda host_: remote_url
|
||||
self.drv._get_nbd_number = lambda volume_: number
|
||||
self.drv.create_volume(volume)
|
||||
self.mock_api.post.assert_called_with(
|
||||
self.request_params.url('nbd' + remote_url),
|
||||
data=self.request_params.build_post_args({
|
||||
'objectPath': '/'.join((self.cfg.nexenta_lun_container,
|
||||
volume['name'])),
|
||||
'volSizeMB': volume['size'] * units.Ki,
|
||||
'blockSize': self.cfg.nexenta_blocksize,
|
||||
'chunkSize': self.cfg.nexenta_chunksize}),
|
||||
timeout=jsonrpc.TIMEOUT)
|
||||
|
||||
def test_delete_volume(self):
|
||||
self.mock_api.delete.returning_value = FakeResponse({})
|
||||
volume = {
|
||||
'host': 'host@backend#pool info',
|
||||
'size': 1,
|
||||
'name': 'volume'
|
||||
}
|
||||
number = 5
|
||||
remote_url = ''
|
||||
self.drv._get_remote_url = lambda host_: remote_url
|
||||
self.drv._get_nbd_number = lambda volume_: number
|
||||
self.drv.delete_volume(volume)
|
||||
self.mock_api.delete.assert_called_with(
|
||||
self.request_params.url('nbd' + remote_url),
|
||||
data=self.request_params.build_post_args({
|
||||
'objectPath': '/'.join((self.cfg.nexenta_lun_container,
|
||||
volume['name'])),
|
||||
'number': number}),
|
||||
timeout=jsonrpc.TIMEOUT)
|
||||
|
||||
def test_delete_volume__not_found(self):
|
||||
self.mock_api.delete.returning_value = FakeResponse({})
|
||||
volume = {
|
||||
'host': 'host@backend#pool info',
|
||||
'size': 1,
|
||||
'name': 'volume'
|
||||
}
|
||||
remote_url = ''
|
||||
self.drv._get_remote_url = lambda host_: remote_url
|
||||
self.drv._get_nbd_number = lambda volume_: -1
|
||||
self.drv.delete_volume(volume)
|
||||
self.mock_api.delete.assert_not_called()
|
||||
|
||||
def test_extend_volume(self):
|
||||
self.mock_api.put.returning_value = FakeResponse({})
|
||||
volume = {
|
||||
'host': 'host@backend#pool info',
|
||||
'size': 1,
|
||||
'name': 'volume'
|
||||
}
|
||||
new_size = 5
|
||||
remote_url = ''
|
||||
self.drv._get_remote_url = lambda host_: remote_url
|
||||
self.drv.extend_volume(volume, new_size)
|
||||
self.mock_api.put.assert_called_with(
|
||||
self.request_params.url('nbd/resize' + remote_url),
|
||||
data=self.request_params.build_post_args({
|
||||
'objectPath': '/'.join((self.cfg.nexenta_lun_container,
|
||||
volume['name'])),
|
||||
'newSizeMB': new_size * units.Ki}),
|
||||
timeout=jsonrpc.TIMEOUT)
|
||||
|
||||
def test_create_snapshot(self):
|
||||
self.mock_api.post.returning_value = FakeResponse({})
|
||||
snapshot = {
|
||||
'name': 'dsfsdsdgfdf',
|
||||
'volume_name': 'volume'
|
||||
}
|
||||
self.drv.create_snapshot(snapshot)
|
||||
self.mock_api.post.assert_called_with(
|
||||
self.request_params.url('nbd/snapshot'),
|
||||
data=self.request_params.build_post_args({
|
||||
'objectPath': '/'.join((self.cfg.nexenta_lun_container,
|
||||
snapshot['volume_name'])),
|
||||
'snapName': snapshot['name']}),
|
||||
timeout=jsonrpc.TIMEOUT)
|
||||
|
||||
def test_delete_snapshot(self):
|
||||
self.mock_api.delete.returning_value = FakeResponse({})
|
||||
snapshot = {
|
||||
'name': 'dsfsdsdgfdf',
|
||||
'volume_name': 'volume'
|
||||
}
|
||||
self.drv.delete_snapshot(snapshot)
|
||||
self.mock_api.delete.assert_called_with(
|
||||
self.request_params.url('nbd/snapshot'),
|
||||
data=self.request_params.build_post_args({
|
||||
'objectPath': '/'.join((self.cfg.nexenta_lun_container,
|
||||
snapshot['volume_name'])),
|
||||
'snapName': snapshot['name']}),
|
||||
timeout=jsonrpc.TIMEOUT)
|
||||
|
||||
def test_create_volume_from_snapshot(self):
|
||||
self.mock_api.put.returning_value = FakeResponse({})
|
||||
snapshot = {
|
||||
'name': 'dsfsdsdgfdf',
|
||||
'volume_size': 1,
|
||||
'volume_name': 'volume'
|
||||
}
|
||||
volume = {
|
||||
'host': 'host@backend#pool info',
|
||||
'size': 2,
|
||||
'name': 'volume'
|
||||
}
|
||||
remote_url = ''
|
||||
self.drv._get_remote_url = lambda host_: remote_url
|
||||
self.drv.extend_volume = lambda v, s: None
|
||||
self.drv.create_volume_from_snapshot(volume, snapshot)
|
||||
self.mock_api.put.assert_called_with(
|
||||
self.request_params.url('nbd/snapshot/clone' + remote_url),
|
||||
data=self.request_params.build_post_args({
|
||||
'objectPath': '/'.join((self.cfg.nexenta_lun_container,
|
||||
snapshot['volume_name'])),
|
||||
'snapName': snapshot['name'],
|
||||
'clonePath': '/'.join((self.cfg.nexenta_lun_container,
|
||||
volume['name']))
|
||||
}),
|
||||
timeout=jsonrpc.TIMEOUT)
|
||||
|
||||
def test_create_cloned_volume(self):
|
||||
self.mock_api.post.returning_value = FakeResponse({})
|
||||
volume = {
|
||||
'host': 'host@backend#pool info',
|
||||
'size': 1,
|
||||
'name': 'volume'
|
||||
}
|
||||
src_vref = {
|
||||
'size': 1,
|
||||
'name': 'qwerty'
|
||||
}
|
||||
container = self.cfg.nexenta_lun_container
|
||||
remote_url = ''
|
||||
self.drv._get_remote_url = lambda host_: remote_url
|
||||
self.drv.create_cloned_volume(volume, src_vref)
|
||||
self.mock_api.post.assert_called_with(
|
||||
self.request_params.url('nbd' + remote_url),
|
||||
data=self.request_params.build_post_args({
|
||||
'objectPath': '/'.join((container, volume['name'])),
|
||||
'volSizeMB': src_vref['size'] * units.Ki,
|
||||
'blockSize': self.cfg.nexenta_blocksize,
|
||||
'chunkSize': self.cfg.nexenta_chunksize
|
||||
}),
|
||||
timeout=jsonrpc.TIMEOUT)
|
||||
|
||||
def test_create_cloned_volume_gt_src(self):
|
||||
self.mock_api.post.returning_value = FakeResponse({})
|
||||
volume = {
|
||||
'host': 'host@backend#pool info',
|
||||
'size': 2,
|
||||
'name': 'volume'
|
||||
}
|
||||
src_vref = {
|
||||
'size': 1,
|
||||
'name': 'qwerty'
|
||||
}
|
||||
container = self.cfg.nexenta_lun_container
|
||||
remote_url = ''
|
||||
self.drv._get_remote_url = lambda host_: remote_url
|
||||
self.drv.create_cloned_volume(volume, src_vref)
|
||||
self.mock_api.post.assert_called_with(
|
||||
self.request_params.url('nbd' + remote_url),
|
||||
data=self.request_params.build_post_args({
|
||||
'objectPath': '/'.join((container, volume['name'])),
|
||||
'volSizeMB': volume['size'] * units.Ki,
|
||||
'blockSize': self.cfg.nexenta_blocksize,
|
||||
'chunkSize': self.cfg.nexenta_chunksize
|
||||
}),
|
||||
timeout=jsonrpc.TIMEOUT)
|
||||
|
||||
def test_get_volume_stats(self):
|
||||
self.cfg.volume_backend_name = None
|
||||
self.mock_api.get.return_value = FakeResponse({
|
||||
'response': {
|
||||
'stats': {
|
||||
'summary': {
|
||||
'total_capacity': units.Gi,
|
||||
'total_available': units.Gi
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
location_info = '%(driver)s:%(host)s:%(bucket)s' % {
|
||||
'driver': self.drv.__class__.__name__,
|
||||
'host': socket.gethostname(),
|
||||
'bucket': self.cfg.nexenta_lun_container
|
||||
}
|
||||
expected = {
|
||||
'vendor_name': 'Nexenta',
|
||||
'driver_version': self.drv.VERSION,
|
||||
'storage_protocol': 'NBD',
|
||||
'reserved_percentage': self.cfg.reserved_percentage,
|
||||
'total_capacity_gb': 1,
|
||||
'free_capacity_gb': 1,
|
||||
'QoS_support': False,
|
||||
'volume_backend_name': self.drv.__class__.__name__,
|
||||
'location_info': location_info,
|
||||
'restapi_url': '%s://%s:%s/' % (
|
||||
'http', self.cfg.nexenta_rest_address,
|
||||
self.cfg.nexenta_rest_port)
|
||||
}
|
||||
|
||||
self.assertEqual(expected, self.drv.get_volume_stats())
|
||||
|
||||
@patch('cinder.image.image_utils.fetch_to_raw')
|
||||
def test_copy_image_to_volume(self, fetch_to_raw):
|
||||
volume = {
|
||||
'host': 'host@backend#pool info',
|
||||
'size': 1,
|
||||
'name': 'volume'
|
||||
}
|
||||
self.drv.local_path = lambda host: 'local_path'
|
||||
self.drv.copy_image_to_volume(self.ctx, volume, 'image_service',
|
||||
'image_id')
|
||||
fetch_to_raw.assert_called_with(
|
||||
self.ctx, 'image_service', 'image_id', 'local_path',
|
||||
self.cfg.volume_dd_blocksize, size=volume['size'])
|
||||
|
||||
@patch('cinder.image.image_utils.upload_volume')
|
||||
def test_copy_volume_to_image(self, upload_volume):
|
||||
volume = {
|
||||
'host': 'host@backend#pool info',
|
||||
'size': 1,
|
||||
'name': 'volume'
|
||||
}
|
||||
self.drv.local_path = lambda host: 'local_path'
|
||||
self.drv.copy_volume_to_image(self.ctx, volume, 'image_service',
|
||||
'image_meta')
|
||||
upload_volume.assert_called_with(
|
||||
self.ctx, 'image_service', 'image_meta', 'local_path')
|
||||
|
||||
def test_validate_connector(self):
|
||||
connector = {'host': 'host2'}
|
||||
r = {
|
||||
'stats': {
|
||||
'servers': {
|
||||
'host1': {'hostname': 'host1'},
|
||||
'host2': {'hostname': 'host2'}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.mock_api.get.return_value = FakeResponse({'response': r})
|
||||
self.drv.validate_connector(connector)
|
||||
self.mock_api.get.assert_called_with(
|
||||
self.request_params.url('system/stats'),
|
||||
timeout=jsonrpc.TIMEOUT)
|
||||
|
||||
def test_validate_connector__host_not_found(self):
|
||||
connector = {'host': 'host3'}
|
||||
r = {
|
||||
'stats': {
|
||||
'servers': {
|
||||
'host1': {'hostname': 'host1'},
|
||||
'host2': {'hostname': 'host2'}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.mock_api.get.return_value = FakeResponse({'response': r})
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
self.drv.validate_connector, connector)
|
||||
|
||||
def test_initialize_connection(self):
|
||||
connector = {'host': 'host'}
|
||||
volume = {
|
||||
'host': 'host@backend#pool info',
|
||||
'size': 1,
|
||||
'name': 'volume'
|
||||
}
|
||||
self.drv.local_path = lambda host: 'local_path'
|
||||
self.assertEqual({
|
||||
'driver_volume_type': 'local',
|
||||
'data': {'device_path': 'local_path'}},
|
||||
self.drv.initialize_connection(volume, connector))
|
||||
@@ -1,315 +0,0 @@
|
||||
# Copyright 2015 Nexenta Systems, Inc.
|
||||
# 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.
|
||||
|
||||
import json
|
||||
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import excutils
|
||||
from oslo_utils import units
|
||||
|
||||
from cinder import exception
|
||||
from cinder.i18n import _, _LE, _LI
|
||||
from cinder import interface
|
||||
from cinder.volume import driver
|
||||
from cinder.volume.drivers.nexenta.nexentaedge import jsonrpc
|
||||
from cinder.volume.drivers.nexenta import options
|
||||
from cinder.volume.drivers.nexenta import utils as nexenta_utils
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@interface.volumedriver
|
||||
class NexentaEdgeISCSIDriver(driver.ISCSIDriver):
|
||||
"""Executes volume driver commands on NexentaEdge cluster.
|
||||
|
||||
Version history:
|
||||
1.0.0 - Initial driver version.
|
||||
1.0.1 - Moved opts to options.py.
|
||||
1.0.2 - Added HA support.
|
||||
"""
|
||||
|
||||
VERSION = '1.0.2'
|
||||
|
||||
# ThirdPartySystems wiki page
|
||||
CI_WIKI_NAME = "Nexenta_Edge_CI"
|
||||
|
||||
# TODO(smcginnis) Either remove this if CI requirements are met, or
|
||||
# remove this driver in the Pike release per normal deprecation
|
||||
SUPPORTED = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NexentaEdgeISCSIDriver, self).__init__(*args, **kwargs)
|
||||
if self.configuration:
|
||||
self.configuration.append_config_values(
|
||||
options.NEXENTA_CONNECTION_OPTS)
|
||||
self.configuration.append_config_values(
|
||||
options.NEXENTA_ISCSI_OPTS)
|
||||
self.configuration.append_config_values(
|
||||
options.NEXENTA_DATASET_OPTS)
|
||||
self.configuration.append_config_values(
|
||||
options.NEXENTA_EDGE_OPTS)
|
||||
self.restapi_protocol = self.configuration.nexenta_rest_protocol
|
||||
self.restapi_host = self.configuration.nexenta_rest_address
|
||||
self.restapi_port = self.configuration.nexenta_rest_port
|
||||
self.restapi_user = self.configuration.nexenta_rest_user
|
||||
self.restapi_password = self.configuration.nexenta_rest_password
|
||||
self.iscsi_service = self.configuration.nexenta_iscsi_service
|
||||
self.bucket_path = self.configuration.nexenta_lun_container
|
||||
self.blocksize = self.configuration.nexenta_blocksize
|
||||
self.chunksize = self.configuration.nexenta_chunksize
|
||||
self.cluster, self.tenant, self.bucket = self.bucket_path.split('/')
|
||||
self.bucket_url = ('clusters/' + self.cluster + '/tenants/' +
|
||||
self.tenant + '/buckets/' + self.bucket)
|
||||
self.iscsi_target_port = (self.configuration.
|
||||
nexenta_iscsi_target_portal_port)
|
||||
self.target_vip = None
|
||||
self.ha_vip = None
|
||||
|
||||
@property
|
||||
def backend_name(self):
|
||||
backend_name = None
|
||||
if self.configuration:
|
||||
backend_name = self.configuration.safe_get('volume_backend_name')
|
||||
if not backend_name:
|
||||
backend_name = self.__class__.__name__
|
||||
return backend_name
|
||||
|
||||
def do_setup(self, context):
|
||||
def get_ip(host):
|
||||
hm = host[0 if len(host) == 1 else 1]['ip'].split('/', 1)
|
||||
return {
|
||||
'ip': hm[0],
|
||||
'mask': hm[1] if len(hm) > 1 else '32'
|
||||
}
|
||||
|
||||
if self.restapi_protocol == 'auto':
|
||||
protocol, auto = 'http', True
|
||||
else:
|
||||
protocol, auto = self.restapi_protocol, False
|
||||
|
||||
try:
|
||||
self.restapi = jsonrpc.NexentaEdgeJSONProxy(
|
||||
protocol, self.restapi_host, self.restapi_port, '/',
|
||||
self.restapi_user, self.restapi_password, auto=auto)
|
||||
|
||||
rsp = self.restapi.get('service/'
|
||||
+ self.iscsi_service + '/iscsi/status')
|
||||
data_keys = rsp['data'][list(rsp['data'].keys())[0]]
|
||||
self.target_name = data_keys.split('\n', 1)[0].split(' ')[2]
|
||||
|
||||
target_vip = self.configuration.safe_get(
|
||||
'nexenta_client_address')
|
||||
rsp = self.restapi.get('service/' + self.iscsi_service)
|
||||
if 'X-VIPS' in rsp['data']:
|
||||
vips = json.loads(rsp['data']['X-VIPS'])
|
||||
vips = [get_ip(host) for host in vips]
|
||||
if target_vip:
|
||||
found = False
|
||||
for host in vips:
|
||||
if target_vip == host['ip']:
|
||||
self.ha_vip = '/'.join((host['ip'], host['mask']))
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
raise exception.VolumeBackendAPIException(
|
||||
message=_("nexenta_client_address doesn't match "
|
||||
"any VIPs provided by service: {}"
|
||||
).format(
|
||||
", ".join([host['ip'] for host in vips])))
|
||||
else:
|
||||
if len(vips) == 1:
|
||||
target_vip = vips[0]['ip']
|
||||
self.ha_vip = '/'.join(
|
||||
(vips[0]['ip'], vips[0]['mask']))
|
||||
if not target_vip:
|
||||
LOG.error(_LE('No VIP configured for service %s'),
|
||||
self.iscsi_service)
|
||||
raise exception.VolumeBackendAPIException(
|
||||
message=_('No service VIP configured and '
|
||||
'no nexenta_client_address'))
|
||||
self.target_vip = target_vip
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Error verifying iSCSI service %(serv)s on '
|
||||
'host %(hst)s'), {'serv': self.iscsi_service,
|
||||
'hst': self.restapi_host})
|
||||
|
||||
def check_for_setup_error(self):
|
||||
try:
|
||||
self.restapi.get(self.bucket_url + '/objects/')
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Error verifying LUN container %(bkt)s'),
|
||||
{'bkt': self.bucket_path})
|
||||
|
||||
def _get_lun_number(self, volname):
|
||||
try:
|
||||
rsp = self.restapi.put(
|
||||
'service/' + self.iscsi_service + '/iscsi/number',
|
||||
{
|
||||
'objectPath': self.bucket_path + '/' + volname
|
||||
})
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Error retrieving LUN %(vol)s number'),
|
||||
{'vol': volname})
|
||||
|
||||
return rsp['data']
|
||||
|
||||
def _get_target_address(self, volname):
|
||||
return self.target_vip
|
||||
|
||||
def _get_provider_location(self, volume):
|
||||
return '%(host)s:%(port)s,1 %(name)s %(number)s' % {
|
||||
'host': self._get_target_address(volume['name']),
|
||||
'port': self.iscsi_target_port,
|
||||
'name': self.target_name,
|
||||
'number': self._get_lun_number(volume['name'])
|
||||
}
|
||||
|
||||
def create_volume(self, volume):
|
||||
data = {
|
||||
'objectPath': self.bucket_path + '/' + volume['name'],
|
||||
'volSizeMB': int(volume['size']) * units.Ki,
|
||||
'blockSize': self.blocksize,
|
||||
'chunkSize': self.chunksize
|
||||
}
|
||||
if self.ha_vip:
|
||||
data['vip'] = self.ha_vip
|
||||
try:
|
||||
self.restapi.post('service/' + self.iscsi_service + '/iscsi', data)
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Error creating volume'))
|
||||
|
||||
def delete_volume(self, volume):
|
||||
try:
|
||||
self.restapi.delete('service/' + self.iscsi_service +
|
||||
'/iscsi', {'objectPath': self.bucket_path +
|
||||
'/' + volume['name']})
|
||||
except exception.VolumeBackendAPIException:
|
||||
LOG.info(
|
||||
_LI('Volume was already deleted from appliance, skipping.'),
|
||||
resource=volume)
|
||||
|
||||
def extend_volume(self, volume, new_size):
|
||||
try:
|
||||
self.restapi.put('service/' + self.iscsi_service + '/iscsi/resize',
|
||||
{'objectPath': self.bucket_path +
|
||||
'/' + volume['name'],
|
||||
'newSizeMB': new_size * units.Ki})
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Error extending volume'))
|
||||
|
||||
def create_volume_from_snapshot(self, volume, snapshot):
|
||||
try:
|
||||
self.restapi.put(
|
||||
'service/' + self.iscsi_service + '/iscsi/snapshot/clone',
|
||||
{
|
||||
'objectPath': self.bucket_path + '/' +
|
||||
snapshot['volume_name'],
|
||||
'clonePath': self.bucket_path + '/' + volume['name'],
|
||||
'snapName': snapshot['name']
|
||||
})
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Error cloning volume'))
|
||||
|
||||
def create_snapshot(self, snapshot):
|
||||
try:
|
||||
self.restapi.post(
|
||||
'service/' + self.iscsi_service + '/iscsi/snapshot',
|
||||
{
|
||||
'objectPath': self.bucket_path + '/' +
|
||||
snapshot['volume_name'],
|
||||
'snapName': snapshot['name']
|
||||
})
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Error creating snapshot'))
|
||||
|
||||
def delete_snapshot(self, snapshot):
|
||||
try:
|
||||
self.restapi.delete(
|
||||
'service/' + self.iscsi_service + '/iscsi/snapshot',
|
||||
{
|
||||
'objectPath': self.bucket_path + '/' +
|
||||
snapshot['volume_name'],
|
||||
'snapName': snapshot['name']
|
||||
})
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Error deleting snapshot'))
|
||||
|
||||
def create_cloned_volume(self, volume, src_vref):
|
||||
vol_url = (self.bucket_url + '/objects/' +
|
||||
src_vref['name'] + '/clone')
|
||||
clone_body = {
|
||||
'tenant_name': self.tenant,
|
||||
'bucket_name': self.bucket,
|
||||
'object_name': volume['name']
|
||||
}
|
||||
try:
|
||||
self.restapi.post(vol_url, clone_body)
|
||||
self.restapi.post('service/' + self.iscsi_service + '/iscsi', {
|
||||
'objectPath': self.bucket_path + '/' + volume['name'],
|
||||
'volSizeMB': int(src_vref['size']) * units.Ki,
|
||||
'blockSize': self.blocksize,
|
||||
'chunkSize': self.chunksize
|
||||
})
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Error creating cloned volume'))
|
||||
if volume['size'] > src_vref['size']:
|
||||
self.extend_volume(volume, volume['size'])
|
||||
|
||||
def create_export(self, context, volume, connector=None):
|
||||
return {'provider_location': self._get_provider_location(volume)}
|
||||
|
||||
def ensure_export(self, context, volume):
|
||||
pass
|
||||
|
||||
def remove_export(self, context, volume):
|
||||
pass
|
||||
|
||||
def local_path(self, volume):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_volume_stats(self, refresh=False):
|
||||
resp = self.restapi.get('system/stats')
|
||||
summary = resp['stats']['summary']
|
||||
total = nexenta_utils.str2gib_size(summary['total_capacity'])
|
||||
free = nexenta_utils.str2gib_size(summary['total_available'])
|
||||
|
||||
location_info = '%(driver)s:%(host)s:%(bucket)s' % {
|
||||
'driver': self.__class__.__name__,
|
||||
'host': self._get_target_address(None),
|
||||
'bucket': self.bucket_path
|
||||
}
|
||||
return {
|
||||
'vendor_name': 'Nexenta',
|
||||
'driver_version': self.VERSION,
|
||||
'storage_protocol': 'iSCSI',
|
||||
'reserved_percentage': 0,
|
||||
'total_capacity_gb': total,
|
||||
'free_capacity_gb': free,
|
||||
'QoS_support': False,
|
||||
'volume_backend_name': self.backend_name,
|
||||
'location_info': location_info,
|
||||
'iscsi_target_portal_port': self.iscsi_target_port,
|
||||
'restapi_url': self.restapi.url
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
# Copyright 2015 Nexenta Systems, Inc.
|
||||
# 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.
|
||||
|
||||
import json
|
||||
import requests
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
from cinder import exception
|
||||
from cinder.i18n import _
|
||||
from cinder.utils import retry
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
TIMEOUT = 60
|
||||
|
||||
|
||||
class NexentaEdgeJSONProxy(object):
|
||||
|
||||
retry_exc_tuple = (
|
||||
requests.exceptions.ConnectionError,
|
||||
requests.exceptions.ConnectTimeout
|
||||
)
|
||||
|
||||
def __init__(self, protocol, host, port, path, user, password, auto=False,
|
||||
method=None, session=None):
|
||||
if session:
|
||||
self.session = session
|
||||
else:
|
||||
self.session = requests.Session()
|
||||
self.session.auth = (user, password)
|
||||
self.session.headers.update({'Content-Type': 'application/json'})
|
||||
self.protocol = protocol.lower()
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.path = path
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.auto = auto
|
||||
self.method = method
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return '%s://%s:%s/%s' % (
|
||||
self.protocol, self.host, self.port, self.path)
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name in ('get', 'post', 'put', 'delete'):
|
||||
return NexentaEdgeJSONProxy(
|
||||
self.protocol, self.host, self.port, self.path, self.user,
|
||||
self.password, self.auto, name, self.session)
|
||||
return super(NexentaEdgeJSONProxy, self).__getattr__(name)
|
||||
|
||||
def __hash__(self):
|
||||
return self.url.__hash__()
|
||||
|
||||
def __repr__(self):
|
||||
return 'HTTP JSON proxy: %s' % self.url
|
||||
|
||||
@retry(retry_exc_tuple, interval=1, retries=6)
|
||||
def __call__(self, *args):
|
||||
self.path = args[0]
|
||||
kwargs = {'timeout': TIMEOUT}
|
||||
data = None
|
||||
if len(args) > 1:
|
||||
data = json.dumps(args[1])
|
||||
kwargs['data'] = data
|
||||
|
||||
LOG.debug('Sending JSON data: %s, method: %s, data: %s',
|
||||
self.url, self.method, data)
|
||||
|
||||
func = getattr(self.session, self.method)
|
||||
if func:
|
||||
req = func(self.url, **kwargs)
|
||||
else:
|
||||
raise exception.VolumeDriverException(
|
||||
message=_('Unsupported method: %s') % self.method)
|
||||
|
||||
rsp = req.json()
|
||||
|
||||
LOG.debug('Got response: %s', rsp)
|
||||
if rsp.get('response') is None:
|
||||
raise exception.VolumeBackendAPIException(
|
||||
data=_('Error response: %s') % rsp)
|
||||
return rsp.get('response')
|
||||
@@ -1,350 +0,0 @@
|
||||
# Copyright 2016 Nexenta Systems, Inc.
|
||||
# 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.
|
||||
|
||||
import json
|
||||
import os
|
||||
import six
|
||||
import socket
|
||||
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import excutils
|
||||
from oslo_utils import units
|
||||
|
||||
from cinder import exception
|
||||
from cinder.i18n import _, _LE, _LI
|
||||
from cinder.image import image_utils
|
||||
from cinder import interface
|
||||
from cinder import utils as cinder_utils
|
||||
from cinder.volume import driver
|
||||
from cinder.volume.drivers.nexenta.nexentaedge import jsonrpc
|
||||
from cinder.volume.drivers.nexenta import options
|
||||
from cinder.volume.drivers.nexenta import utils as nexenta_utils
|
||||
from cinder.volume import utils as volutils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@interface.volumedriver
|
||||
class NexentaEdgeNBDDriver(driver.VolumeDriver):
|
||||
"""Executes commands relating to NBD Volumes.
|
||||
|
||||
Version history:
|
||||
1.0.0 - Initial driver version.
|
||||
"""
|
||||
|
||||
VERSION = '1.0.0'
|
||||
|
||||
# ThirdPartySystems wiki page
|
||||
CI_WIKI_NAME = "Nexenta_Edge_CI"
|
||||
|
||||
# TODO(smcginnis) Either remove this if CI requirements are met, or
|
||||
# remove this driver in the Pike release per normal deprecation
|
||||
SUPPORTED = False
|
||||
|
||||
def __init__(self, vg_obj=None, *args, **kwargs):
|
||||
LOG.debug('NexentaEdgeNBDDriver. Trying to initialize.')
|
||||
super(NexentaEdgeNBDDriver, self).__init__(*args, **kwargs)
|
||||
|
||||
if self.configuration:
|
||||
self.configuration.append_config_values(
|
||||
options.NEXENTA_CONNECTION_OPTS)
|
||||
self.configuration.append_config_values(
|
||||
options.NEXENTA_DATASET_OPTS)
|
||||
self.configuration.append_config_values(
|
||||
options.NEXENTA_EDGE_OPTS)
|
||||
self.restapi_protocol = self.configuration.nexenta_rest_protocol
|
||||
self.restapi_host = self.configuration.nexenta_rest_address
|
||||
self.restapi_port = self.configuration.nexenta_rest_port
|
||||
self.restapi_user = self.configuration.nexenta_rest_user
|
||||
self.restapi_password = self.configuration.nexenta_rest_password
|
||||
self.bucket_path = self.configuration.nexenta_lun_container
|
||||
self.blocksize = self.configuration.nexenta_blocksize
|
||||
self.chunksize = self.configuration.nexenta_chunksize
|
||||
self.cluster, self.tenant, self.bucket = self.bucket_path.split('/')
|
||||
self.bucket_url = ('clusters/' + self.cluster + '/tenants/' +
|
||||
self.tenant + '/buckets/' + self.bucket)
|
||||
self.hostname = socket.gethostname()
|
||||
self.symlinks_dir = self.configuration.nexenta_nbd_symlinks_dir
|
||||
self.reserved_percentage = self.configuration.reserved_percentage
|
||||
LOG.debug('NexentaEdgeNBDDriver. Initialized successfully.')
|
||||
|
||||
@property
|
||||
def backend_name(self):
|
||||
backend_name = None
|
||||
if self.configuration:
|
||||
backend_name = self.configuration.safe_get('volume_backend_name')
|
||||
if not backend_name:
|
||||
backend_name = self.__class__.__name__
|
||||
return backend_name
|
||||
|
||||
def do_setup(self, context):
|
||||
if self.restapi_protocol == 'auto':
|
||||
protocol, auto = 'http', True
|
||||
else:
|
||||
protocol, auto = self.restapi_protocol, False
|
||||
|
||||
self.restapi = jsonrpc.NexentaEdgeJSONProxy(
|
||||
protocol, self.restapi_host, self.restapi_port, '',
|
||||
self.restapi_user, self.restapi_password, auto=auto)
|
||||
|
||||
def check_for_setup_error(self):
|
||||
try:
|
||||
if not self.symlinks_dir:
|
||||
msg = _("nexenta_nbd_symlinks_dir option is not specified")
|
||||
raise exception.NexentaException(message=msg)
|
||||
if not os.path.exists(self.symlinks_dir):
|
||||
msg = _("NexentaEdge NBD symlinks directory doesn't exist")
|
||||
raise exception.NexentaException(message=msg)
|
||||
self.restapi.get(self.bucket_url + '/objects/')
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Error verifying container %(bkt)s'),
|
||||
{'bkt': self.bucket_path})
|
||||
|
||||
def _get_nbd_devices(self, host):
|
||||
try:
|
||||
rsp = self.restapi.get('sysconfig/nbd/devices' +
|
||||
self._get_remote_url(host))
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Error getting NBD list'))
|
||||
return json.loads(rsp['value'])
|
||||
|
||||
def _get_nbd_number(self, volume):
|
||||
host = volutils.extract_host(volume['host'], 'host')
|
||||
nbds = self._get_nbd_devices(host)
|
||||
for dev in nbds:
|
||||
if dev['objectPath'] == self.bucket_path + '/' + volume['name']:
|
||||
return dev['number']
|
||||
return -1
|
||||
|
||||
def _get_host_info(self, host):
|
||||
try:
|
||||
res = self.restapi.get('system/stats')
|
||||
servers = res['stats']['servers']
|
||||
for sid in servers:
|
||||
if host == sid or host == servers[sid]['hostname']:
|
||||
return servers[sid]
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Error getting host info'))
|
||||
raise exception.VolumeBackendAPIException(
|
||||
data=_('No %s hostname in NEdge cluster') % host)
|
||||
|
||||
def _get_remote_url(self, host):
|
||||
return '?remote=' + (
|
||||
six.text_type(self._get_host_info(host)['ipv6addr']))
|
||||
|
||||
def _get_symlink_path(self, number):
|
||||
return os.path.join(self.symlinks_dir, 'nbd' + six.text_type(number))
|
||||
|
||||
def local_path(self, volume):
|
||||
number = self._get_nbd_number(volume)
|
||||
if number == -1:
|
||||
msg = _('No NBD device for volume %s') % volume['name']
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
return self._get_symlink_path(number)
|
||||
|
||||
def create_volume(self, volume):
|
||||
LOG.debug('Create volume')
|
||||
host = volutils.extract_host(volume['host'], 'host')
|
||||
try:
|
||||
self.restapi.post('nbd' + self._get_remote_url(host), {
|
||||
'objectPath': self.bucket_path + '/' + volume['name'],
|
||||
'volSizeMB': int(volume['size']) * units.Ki,
|
||||
'blockSize': self.blocksize,
|
||||
'chunkSize': self.chunksize
|
||||
})
|
||||
number = self._get_nbd_number(volume)
|
||||
cinder_utils.execute(
|
||||
'ln', '--symbolic', '--force',
|
||||
'/dev/nbd' + six.text_type(number),
|
||||
self._get_symlink_path(number), run_as_root=True,
|
||||
check_exit_code=True)
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Error creating volume'))
|
||||
|
||||
def delete_volume(self, volume):
|
||||
LOG.debug('Delete volume')
|
||||
number = self._get_nbd_number(volume)
|
||||
if number == -1:
|
||||
LOG.info(_LI('Volume %(volume)s does not exist at %(path)s '
|
||||
'path') % {
|
||||
'volume': volume['name'],
|
||||
'path': self.bucket_path
|
||||
})
|
||||
return
|
||||
host = volutils.extract_host(volume['host'], 'host')
|
||||
try:
|
||||
self.restapi.delete('nbd' + self._get_remote_url(host), {
|
||||
'objectPath': self.bucket_path + '/' + volume['name'],
|
||||
'number': number
|
||||
})
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Error deleting volume'))
|
||||
|
||||
def extend_volume(self, volume, new_size):
|
||||
LOG.debug('Extend volume')
|
||||
host = volutils.extract_host(volume['host'], 'host')
|
||||
try:
|
||||
self.restapi.put('nbd/resize' + self._get_remote_url(host), {
|
||||
'objectPath': self.bucket_path + '/' + volume['name'],
|
||||
'newSizeMB': new_size * units.Ki
|
||||
})
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Error extending volume'))
|
||||
|
||||
def create_snapshot(self, snapshot):
|
||||
LOG.debug('Create snapshot')
|
||||
try:
|
||||
self.restapi.post('nbd/snapshot', {
|
||||
'objectPath': self.bucket_path + '/' + snapshot['volume_name'],
|
||||
'snapName': snapshot['name']
|
||||
})
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Error creating snapshot'))
|
||||
|
||||
def delete_snapshot(self, snapshot):
|
||||
LOG.debug('Delete snapshot')
|
||||
# There is no way to figure out whether a snapshot exists in current
|
||||
# version of the API. This REST function always reports OK even a
|
||||
# snapshot doesn't exist.
|
||||
try:
|
||||
self.restapi.delete('nbd/snapshot', {
|
||||
'objectPath': self.bucket_path + '/' + snapshot['volume_name'],
|
||||
'snapName': snapshot['name']
|
||||
})
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Error deleting snapshot'))
|
||||
|
||||
def create_volume_from_snapshot(self, volume, snapshot):
|
||||
LOG.debug('Create volume from snapshot')
|
||||
host = volutils.extract_host(volume['host'], 'host')
|
||||
remotehost = self._get_remote_url(host)
|
||||
try:
|
||||
self.restapi.put('nbd/snapshot/clone' + remotehost, {
|
||||
'objectPath': self.bucket_path + '/' + snapshot['volume_name'],
|
||||
'snapName': snapshot['name'],
|
||||
'clonePath': self.bucket_path + '/' + volume['name']
|
||||
})
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Error cloning snapshot'))
|
||||
if volume['size'] > snapshot['volume_size']:
|
||||
self.extend_volume(volume, volume['size'])
|
||||
|
||||
def create_cloned_volume(self, volume, src_vref):
|
||||
LOG.debug('Create cloned volume')
|
||||
vol_url = (self.bucket_url + '/objects/' +
|
||||
src_vref['name'] + '/clone')
|
||||
clone_body = {
|
||||
'tenant_name': self.tenant,
|
||||
'bucket_name': self.bucket,
|
||||
'object_name': volume['name']
|
||||
}
|
||||
host = volutils.extract_host(volume['host'], 'host')
|
||||
size = volume['size'] if volume['size'] > src_vref['size'] else (
|
||||
src_vref['size'])
|
||||
try:
|
||||
self.restapi.post(vol_url, clone_body)
|
||||
self.restapi.post('nbd' + self._get_remote_url(host), {
|
||||
'objectPath': self.bucket_path + '/' + volume['name'],
|
||||
'volSizeMB': int(size) * units.Ki,
|
||||
'blockSize': self.blocksize,
|
||||
'chunkSize': self.chunksize
|
||||
})
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Error creating cloned volume'))
|
||||
|
||||
def get_volume_stats(self, refresh=False):
|
||||
LOG.debug('Get volume stats')
|
||||
try:
|
||||
resp = self.restapi.get('system/stats')
|
||||
location_info = '%(driver)s:%(host)s:%(bucket)s' % {
|
||||
'driver': self.__class__.__name__,
|
||||
'host': self.hostname,
|
||||
'bucket': self.bucket_path
|
||||
}
|
||||
summary = resp['stats']['summary']
|
||||
total = nexenta_utils.str2gib_size(summary['total_capacity'])
|
||||
free = nexenta_utils.str2gib_size(summary['total_available'])
|
||||
return {
|
||||
'vendor_name': 'Nexenta',
|
||||
'driver_version': self.VERSION,
|
||||
'storage_protocol': 'NBD',
|
||||
'reserved_percentage': self.reserved_percentage,
|
||||
'total_capacity_gb': total,
|
||||
'free_capacity_gb': free,
|
||||
'QoS_support': False,
|
||||
'volume_backend_name': self.backend_name,
|
||||
'location_info': location_info,
|
||||
'restapi_url': self.restapi.url
|
||||
}
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Error creating snapshot'))
|
||||
|
||||
def copy_image_to_volume(self, context, volume, image_service, image_id):
|
||||
LOG.debug('Copy image to volume')
|
||||
image_utils.fetch_to_raw(context,
|
||||
image_service,
|
||||
image_id,
|
||||
self.local_path(volume),
|
||||
self.configuration.volume_dd_blocksize,
|
||||
size=volume['size'])
|
||||
|
||||
def copy_volume_to_image(self, context, volume, image_service, image_meta):
|
||||
LOG.debug('Copy volume to image')
|
||||
image_utils.upload_volume(context,
|
||||
image_service,
|
||||
image_meta,
|
||||
self.local_path(volume))
|
||||
|
||||
def ensure_export(self, context, volume):
|
||||
pass
|
||||
|
||||
def create_export(self, context, volume, connector, vg=None):
|
||||
pass
|
||||
|
||||
def remove_export(self, context, volume):
|
||||
pass
|
||||
|
||||
def validate_connector(self, connector):
|
||||
LOG.debug('Validate connector')
|
||||
try:
|
||||
res = self.restapi.get('system/stats')
|
||||
servers = res['stats']['servers']
|
||||
for sid in servers:
|
||||
if (connector['host'] == sid or
|
||||
connector['host'] == servers[sid]['hostname']):
|
||||
return
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_LE('Error retrieving cluster stats'))
|
||||
raise exception.VolumeBackendAPIException(
|
||||
data=_('No %s hostname in NEdge cluster') % connector['host'])
|
||||
|
||||
def initialize_connection(self, volume, connector, initiator_data=None):
|
||||
LOG.debug('Initialize connection')
|
||||
return {
|
||||
'driver_volume_type': 'local',
|
||||
'data': {'device_path': self.local_path(volume)},
|
||||
}
|
||||
Reference in New Issue
Block a user