Add the StorPool brick connector
Allow clients to attach and detach Cinder volumes stored on a StorPool cluster. Change-Id: I33cfd7ab9a1d201f6c49357cacb4a809453b7201 Implements: blueprint brick-add-storpool-driver
This commit is contained in:
parent
a5c60d94d8
commit
6faa0d3d35
@ -61,4 +61,5 @@ SHEEPDOG = "SHEEPDOG"
|
||||
VMDK = "VMDK"
|
||||
GPFS = "GPFS"
|
||||
VERITAS_HYPERSCALE = "VERITAS_HYPERSCALE"
|
||||
STORPOOL = "STORPOOL"
|
||||
NVME = "NVME"
|
||||
|
@ -63,6 +63,7 @@ connector_list = [
|
||||
'os_brick.initiator.windows.fibre_channel.WindowsFCConnector',
|
||||
'os_brick.initiator.windows.smbfs.WindowsSMBFSConnector',
|
||||
'os_brick.initiator.connectors.vrtshyperscale.HyperScaleConnector',
|
||||
'os_brick.initiator.connectors.storpool.StorPoolConnector',
|
||||
'os_brick.initiator.connectors.nvme.NVMeConnector',
|
||||
]
|
||||
|
||||
@ -111,6 +112,8 @@ _connector_mapping_linux = {
|
||||
'os_brick.initiator.connectors.gpfs.GPFSConnector',
|
||||
initiator.VERITAS_HYPERSCALE:
|
||||
'os_brick.initiator.connectors.vrtshyperscale.HyperScaleConnector',
|
||||
initiator.STORPOOL:
|
||||
'os_brick.initiator.connectors.storpool.StorPoolConnector',
|
||||
initiator.NVME:
|
||||
'os_brick.initiator.connectors.nvme.NVMeConnector',
|
||||
}
|
||||
|
222
os_brick/initiator/connectors/storpool.py
Normal file
222
os_brick/initiator/connectors/storpool.py
Normal file
@ -0,0 +1,222 @@
|
||||
# Copyright (c) 2015 - 2017 StorPool
|
||||
# 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.
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
import six
|
||||
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import importutils
|
||||
|
||||
from os_brick import exception
|
||||
from os_brick.initiator.connectors import base
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
spopenstack = importutils.try_import('storpool.spopenstack')
|
||||
|
||||
|
||||
class StorPoolConnector(base.BaseLinuxConnector):
|
||||
""""Connector class to attach/detach StorPool volumes."""
|
||||
|
||||
def __init__(self, root_helper, driver=None,
|
||||
*args, **kwargs):
|
||||
|
||||
super(StorPoolConnector, self).__init__(root_helper, driver=driver,
|
||||
*args, **kwargs)
|
||||
|
||||
if spopenstack is not None:
|
||||
try:
|
||||
self._attach = spopenstack.AttachDB(log=LOG)
|
||||
except Exception as e:
|
||||
raise exception.BrickException(
|
||||
'Could not initialize the StorPool API bindings: %s' % (e))
|
||||
else:
|
||||
self._attach = None
|
||||
|
||||
@staticmethod
|
||||
def get_connector_properties(root_helper, *args, **kwargs):
|
||||
"""The StorPool connector properties."""
|
||||
return {}
|
||||
|
||||
def connect_volume(self, connection_properties):
|
||||
"""Connect to a volume.
|
||||
|
||||
:param connection_properties: The dictionary that describes all
|
||||
of the target volume attributes;
|
||||
it needs to contain the StorPool
|
||||
'client_id' and the common 'volume' and
|
||||
'access_mode' values.
|
||||
:type connection_properties: dict
|
||||
:returns: dict
|
||||
"""
|
||||
client_id = connection_properties.get('client_id', None)
|
||||
if client_id is None:
|
||||
raise exception.BrickException(
|
||||
'Invalid StorPool connection data, no client ID specified.')
|
||||
volume_id = connection_properties.get('volume', None)
|
||||
if volume_id is None:
|
||||
raise exception.BrickException(
|
||||
'Invalid StorPool connection data, no volume ID specified.')
|
||||
volume = self._attach.volumeName(volume_id)
|
||||
mode = connection_properties.get('access_mode', None)
|
||||
if mode is None or mode not in ('rw', 'ro'):
|
||||
raise exception.BrickException(
|
||||
'Invalid access_mode specified in the connection data.')
|
||||
req_id = 'brick-%s-%s' % (client_id, volume_id)
|
||||
self._attach.add(req_id, {
|
||||
'volume': volume,
|
||||
'type': 'brick',
|
||||
'id': req_id,
|
||||
'rights': 1 if mode == 'ro' else 2,
|
||||
'volsnap': False
|
||||
})
|
||||
self._attach.sync(req_id, None)
|
||||
return {'type': 'block', 'path': '/dev/storpool/' + volume}
|
||||
|
||||
def disconnect_volume(self, connection_properties, device_info,
|
||||
force=False, ignore_errors=False):
|
||||
"""Disconnect a volume from the local host.
|
||||
|
||||
The connection_properties are the same as from connect_volume.
|
||||
The device_info is returned from connect_volume.
|
||||
|
||||
:param connection_properties: The dictionary that describes all
|
||||
of the target volume attributes;
|
||||
it needs to contain the StorPool
|
||||
'client_id' and the common 'volume'
|
||||
values.
|
||||
:type connection_properties: dict
|
||||
:param device_info: historical difference, but same as connection_props
|
||||
:type device_info: dict
|
||||
:param force: Whether to forcefully disconnect even if flush fails.
|
||||
For StorPool, this parameter is ignored, the volume is
|
||||
always detached.
|
||||
:type force: bool
|
||||
:param ignore_errors: When force is True, this will decide whether to
|
||||
ignore errors or raise an exception once finished
|
||||
the operation. Default is False.
|
||||
For StorPool, this parameter is ignored,
|
||||
no exception is raised except on
|
||||
unexpected errors.
|
||||
:type ignore_errors: bool
|
||||
"""
|
||||
client_id = connection_properties.get('client_id', None)
|
||||
if client_id is None:
|
||||
raise exception.BrickException(
|
||||
'Invalid StorPool connection data, no client ID specified.')
|
||||
volume_id = connection_properties.get('volume', None)
|
||||
if volume_id is None:
|
||||
raise exception.BrickException(
|
||||
'Invalid StorPool connection data, no volume ID specified.')
|
||||
volume = self._attach.volumeName(volume_id)
|
||||
req_id = 'brick-%s-%s' % (client_id, volume_id)
|
||||
self._attach.sync(req_id, volume)
|
||||
self._attach.remove(req_id)
|
||||
|
||||
def get_search_path(self):
|
||||
return '/dev/storpool'
|
||||
|
||||
def get_volume_paths(self, connection_properties):
|
||||
"""Return the list of existing paths for a volume.
|
||||
|
||||
The job of this method is to find out what paths in
|
||||
the system are associated with a volume as described
|
||||
by the connection_properties.
|
||||
|
||||
:param connection_properties: The dictionary that describes all
|
||||
of the target volume attributes;
|
||||
it needs to contain 'volume' and
|
||||
'device_path' values.
|
||||
:type connection_properties: dict
|
||||
"""
|
||||
volume_id = connection_properties.get('volume', None)
|
||||
if volume_id is None:
|
||||
raise exception.BrickException(
|
||||
'Invalid StorPool connection data, no volume ID specified.')
|
||||
volume = self._attach.volumeName(volume_id)
|
||||
path = '/dev/storpool/' + volume
|
||||
dpath = connection_properties.get('device_path', None)
|
||||
if dpath is not None and dpath != path:
|
||||
raise exception.BrickException(
|
||||
'Internal error: StorPool volume path {path} does not '
|
||||
'match device path {dpath}',
|
||||
{path: path, dpath: dpath})
|
||||
return [path]
|
||||
|
||||
def get_all_available_volumes(self, connection_properties=None):
|
||||
"""Return all volumes that exist in the search directory.
|
||||
|
||||
At connect_volume time, a Connector looks in a specific
|
||||
directory to discover a volume's paths showing up.
|
||||
This method's job is to return all paths in the directory
|
||||
that connect_volume uses to find a volume.
|
||||
|
||||
This method is used in coordination with get_volume_paths()
|
||||
to verify that volumes have gone away after disconnect_volume
|
||||
has been called.
|
||||
|
||||
:param connection_properties: The dictionary that describes all
|
||||
of the target volume attributes.
|
||||
Unused for the StorPool connector.
|
||||
:type connection_properties: dict
|
||||
"""
|
||||
names = []
|
||||
prefix = self._attach.volumeName('')
|
||||
prefixlen = len(prefix)
|
||||
if os.path.isdir('/dev/storpool'):
|
||||
files = os.listdir('/dev/storpool')
|
||||
for entry in files:
|
||||
full = '/dev/storpool/' + entry
|
||||
if entry.startswith(prefix) and os.path.islink(full) and \
|
||||
not os.path.isdir(full):
|
||||
names.append(entry[prefixlen:])
|
||||
return names
|
||||
|
||||
def _get_device_size(self, device):
|
||||
"""Get the size in bytes of a volume."""
|
||||
(out, _err) = self._execute('blockdev', '--getsize64',
|
||||
device, run_as_root=True,
|
||||
root_helper=self._root_helper)
|
||||
var = six.text_type(out)
|
||||
if var.isnumeric():
|
||||
return int(var)
|
||||
else:
|
||||
return None
|
||||
|
||||
def extend_volume(self, connection_properties):
|
||||
"""Update the attached volume's size.
|
||||
|
||||
This method will attempt to update the local hosts's
|
||||
volume after the volume has been extended on the remote
|
||||
system. The new volume size in bytes will be returned.
|
||||
If there is a failure to update, then None will be returned.
|
||||
|
||||
:param connection_properties: The volume connection properties.
|
||||
:returns: new size of the volume.
|
||||
"""
|
||||
# The StorPool client (storpool_block service) running on this host
|
||||
# should have picked up the change already, so it is enough to query
|
||||
# the actual disk device to see if its size is correct.
|
||||
#
|
||||
# TODO(pp): query the API to see if this is really the case
|
||||
volume_id = connection_properties.get('volume', None)
|
||||
if volume_id is None:
|
||||
raise exception.BrickException(
|
||||
'Invalid StorPool connection data, no volume ID specified.')
|
||||
volume = self._attach.volumeName(volume_id)
|
||||
path = '/dev/storpool/' + volume
|
||||
return self._get_device_size(path)
|
152
os_brick/tests/initiator/connectors/test_storpool.py
Normal file
152
os_brick/tests/initiator/connectors/test_storpool.py
Normal file
@ -0,0 +1,152 @@
|
||||
# Copyright (c) 2015 - 2017 StorPool
|
||||
# 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 mock
|
||||
|
||||
|
||||
from os_brick import exception
|
||||
from os_brick.initiator.connectors import storpool as connector
|
||||
from os_brick.tests.initiator import test_connector
|
||||
|
||||
|
||||
def volumeNameExt(vid):
|
||||
return 'os--volume--{id}'.format(id=vid)
|
||||
|
||||
|
||||
class MockStorPoolADB(object):
|
||||
def __init__(self, log):
|
||||
self.requests = {}
|
||||
self.attached = {}
|
||||
|
||||
def api(self):
|
||||
pass
|
||||
|
||||
def add(self, req_id, req):
|
||||
if req_id in self.requests:
|
||||
raise Exception('Duplicate MockStorPool request added')
|
||||
self.requests[req_id] = req
|
||||
|
||||
def remove(self, req_id):
|
||||
req = self.requests.get(req_id, None)
|
||||
if req is None:
|
||||
raise Exception('Unknown MockStorPool request removed')
|
||||
elif req['volume'] in self.attached:
|
||||
raise Exception('Removing attached MockStorPool volume')
|
||||
del self.requests[req_id]
|
||||
|
||||
def sync(self, req_id, detached):
|
||||
req = self.requests.get(req_id, None)
|
||||
if req is None:
|
||||
raise Exception('Unknown MockStorPool request synced')
|
||||
volume = req.get('volume', None)
|
||||
if volume is None:
|
||||
raise Exception('MockStorPool request without volume')
|
||||
|
||||
if detached is None:
|
||||
if volume in self.attached:
|
||||
raise Exception('Duplicate MockStorPool request synced')
|
||||
self.attached[volume] = req
|
||||
else:
|
||||
if volume != detached:
|
||||
raise Exception(
|
||||
'Mismatched volumes on a MockStorPool request removal')
|
||||
elif detached not in self.attached:
|
||||
raise Exception('MockStorPool request not attached yet')
|
||||
del self.attached[detached]
|
||||
|
||||
def volumeName(self, vid):
|
||||
return volumeNameExt(vid)
|
||||
|
||||
|
||||
spopenstack = mock.Mock()
|
||||
spopenstack.AttachDB = MockStorPoolADB
|
||||
connector.spopenstack = spopenstack
|
||||
|
||||
|
||||
class StorPoolConnectorTestCase(test_connector.ConnectorTestCase):
|
||||
def volumeName(self, vid):
|
||||
return volumeNameExt(vid)
|
||||
|
||||
def execute(self, *cmd, **kwargs):
|
||||
if cmd[0] == 'blockdev':
|
||||
self.assertEqual(len(cmd), 3)
|
||||
self.assertEqual(cmd[1], '--getsize64')
|
||||
self.assertEqual(cmd[2], '/dev/storpool/' +
|
||||
self.volumeName(self.fakeProp['volume']))
|
||||
return (str(self.fakeSize), None)
|
||||
raise Exception("Unrecognized command passed to " +
|
||||
type(self).__name__ + ".execute(): " +
|
||||
str.join(", ", map(lambda s: "'" + s + "'", cmd)))
|
||||
|
||||
def setUp(self):
|
||||
super(StorPoolConnectorTestCase, self).setUp()
|
||||
|
||||
self.fakeProp = {
|
||||
'volume': 'sp-vol-1',
|
||||
'client_id': 1,
|
||||
'access_mode': 'rw',
|
||||
}
|
||||
self.fakeConnection = None
|
||||
self.fakeSize = 1024 * 1024 * 1024
|
||||
|
||||
self.connector = connector.StorPoolConnector(
|
||||
None, execute=self.execute)
|
||||
self.adb = self.connector._attach
|
||||
|
||||
def test_connect_volume(self):
|
||||
self.assertNotIn(self.volumeName(self.fakeProp['volume']),
|
||||
self.adb.attached)
|
||||
conn = self.connector.connect_volume(self.fakeProp)
|
||||
self.assertIn('type', conn)
|
||||
self.assertIn('path', conn)
|
||||
self.assertIn(self.volumeName(self.fakeProp['volume']),
|
||||
self.adb.attached)
|
||||
|
||||
self.assertEqual(self.connector.get_search_path(), '/dev/storpool')
|
||||
paths = self.connector.get_volume_paths(self.fakeProp)
|
||||
self.assertEqual(len(paths), 1)
|
||||
self.assertEqual(paths[0],
|
||||
"/dev/storpool/" +
|
||||
self.volumeName(self.fakeProp['volume']))
|
||||
self.fakeConnection = conn
|
||||
|
||||
def test_disconnect_volume(self):
|
||||
if self.fakeConnection is None:
|
||||
self.test_connect_volume()
|
||||
self.assertIn(self.volumeName(self.fakeProp['volume']),
|
||||
self.adb.attached)
|
||||
self.connector.disconnect_volume(self.fakeProp, None)
|
||||
self.assertNotIn(self.volumeName(self.fakeProp['volume']),
|
||||
self.adb.attached)
|
||||
|
||||
def test_connect_exceptions(self):
|
||||
"""Raise exceptions on missing connection information"""
|
||||
fake = self.fakeProp
|
||||
for key in fake.keys():
|
||||
c = dict(fake)
|
||||
del c[key]
|
||||
self.assertRaises(exception.BrickException,
|
||||
self.connector.connect_volume, c)
|
||||
if key != 'access_mode':
|
||||
self.assertRaises(exception.BrickException,
|
||||
self.connector.disconnect_volume, c, None)
|
||||
|
||||
def test_extend_volume(self):
|
||||
if self.fakeConnection is None:
|
||||
self.test_connect_volume()
|
||||
|
||||
self.fakeSize += 1024 * 1024 * 1024
|
||||
newSize = self.connector.extend_volume(self.fakeProp)
|
||||
self.assertEqual(newSize, self.fakeSize)
|
Loading…
Reference in New Issue
Block a user