# under the License.
import collections
+import six
+import sys
import mock
from oslo_utils import importutils
return defaults
-class mock_dm_utils(object):
+class mock_dm_consts(object):
- @staticmethod
- def dict_to_aux_props(x):
- return x
+ TQ_GET_PATH = "get_path"
+ NODE_ADDR = "addr"
-class mock_dm_const(object):
+ CSTATE_PREFIX = "cstate:"
+ TSTATE_PREFIX = "tstate:"
- TQ_GET_PATH = "get_path"
+ FLAG_UPD_POOL = "upd_pool"
+ FLAG_UPDATE = "update"
+ FLAG_DRBDCTRL = "drbdctrl"
+ FLAG_STORAGE = "storage"
+ FLAG_EXTERNAL = "external"
+ FLAG_DEPLOY = "deploy"
+
+ FLAG_DISKLESS = "diskless"
+ FLAG_CONNECT = "connect"
+ FLAG_UPD_CON = "upd_con"
+ FLAG_RECONNECT = "reconnect"
+ FLAG_OVERWRITE = "overwrite"
+ FLAG_DISCARD = "discard"
+ FLAG_UPD_CONFIG = "upd_config"
+ FLAG_STANDBY = "standby"
+ FLAG_QIGNORE = "qignore"
+
+ AUX_PROP_PREFIX = "aux:"
+
+ BOOL_TRUE = "true"
+ BOOL_FALSE = "true"
class mock_dm_exc(object):
DM_ENOENT = 2
DM_ERROR = 1000
- pass
+
+class mock_dm_utils(object):
+
+ @staticmethod
+ def _aux_prop_name(key):
+ if six.text_type(key).startswith(mock_dm_consts.AUX_PROP_PREFIX):
+ return key[len(mock_dm_consts.AUX_PROP_PREFIX):]
+ else:
+ return None
+
+ @staticmethod
+ def aux_props_to_dict(props):
+ aux_props = {}
+ for (key, val) in props.items():
+ aux_key = mock_dm_utils._aux_prop_name(key)
+ if aux_key is not None:
+ aux_props[aux_key] = val
+ return aux_props
+
+ @staticmethod
+ def dict_to_aux_props(props):
+ aux_props = {}
+ for (key, val) in props.items():
+ aux_key = mock_dm_consts.AUX_PROP_PREFIX + six.text_type(key)
+ aux_props[aux_key] = six.text_type(val)
+ return aux_props
+
+
+def public_keys(c):
+ return [n for n in c.__dict__.keys() if not n.startswith("_")]
-import sys
sys.modules['dbus'] = mock_dbus
sys.modules['drbdmanage'] = collections.namedtuple(
'module', ['consts', 'exceptions', 'utils'])
sys.modules['drbdmanage.utils'] = collections.namedtuple(
- 'module', ['dict_to_aux_props'])
+ 'module', public_keys(mock_dm_utils))
sys.modules['drbdmanage.consts'] = collections.namedtuple(
- 'module', [])
+ 'module', public_keys(mock_dm_consts))
sys.modules['drbdmanage.exceptions'] = collections.namedtuple(
- 'module', ['DM_EEXIST'])
+ 'module', public_keys(mock_dm_exc))
+import cinder.volume.drivers.drbdmanagedrv as drv
-from cinder.volume.drivers import drbdmanagedrv
+drv.dbus = mock_dbus
+drv.dm_const = mock_dm_consts
+drv.dm_utils = mock_dm_utils
+drv.dm_exc = mock_dm_exc
def create_configuration(object):
def list_resources(self, res, serial, prop, req):
self.calls.append(["list_resources", res, prop, req])
- if 'cinder-id' in prop and prop['cinder-id'].startswith("deadbeef"):
+ if ('aux:cinder-id' in prop and
+ prop['aux:cinder-id'].startswith("deadbeef")):
return ([[mock_dm_exc.DM_ENOENT, "none", []]],
[])
else:
def list_volumes(self, res, ser, prop, req):
self.calls.append(["list_volumes", res, ser, prop, req])
- if 'cinder-id' in prop and prop['cinder-id'].startswith("deadbeef"):
+ if ('aux:cinder-id' in prop and
+ prop['aux:cinder-id'].startswith("deadbeef")):
return ([[mock_dm_exc.DM_SUCCESS, "none", []]],
[])
else:
def text_query(self, cmd):
self.calls.append(["text_query", cmd])
- if cmd[0] == mock_dm_const.TQ_GET_PATH:
+ if cmd[0] == mock_dm_consts.TQ_GET_PATH:
return ([(mock_dm_exc.DM_SUCCESS, "ack", [])], ['/dev/drbd0'])
return ([(mock_dm_exc.DM_ERROR, 'unknown command', [])], [])
def list_assignments(self, nodes, res, ser, prop, req):
self.calls.append(["list_assignments", nodes, res, ser, prop, req])
- if 'cinder-id' in prop and prop['cinder-id'].startswith("deadbeef"):
+ if ('aux:cinder-id' in prop and
+ prop['aux:cinder-id'].startswith("deadbeef")):
return ([[mock_dm_exc.DM_SUCCESS, "none", []]],
[])
else:
def list_snapshots(self, res, sn, serial, prop, req):
self.calls.append(["list_snapshots", res, sn, serial, prop, req])
- if 'cinder-id' in prop and prop['cinder-id'].startswith("deadbeef"):
+ if ('aux:cinder-id' in prop and
+ prop['aux:cinder-id'].startswith("deadbeef")):
return ([[mock_dm_exc.DM_SUCCESS, "none", []]],
[])
else:
self.calls.append(["assign", host, resource, props])
return [[mock_dm_exc.DM_SUCCESS, "ack", []]]
+ def create_node(self, name, prop):
+ self.calls.append(["create_node", name, prop])
+ if name.startswith('EXIST'):
+ return [(mock_dm_exc.DM_EEXIST, "none", [])]
+ else:
+ return [(mock_dm_exc.DM_SUCCESS, "ack", [])]
-class DrbdManageTestCase(test.TestCase):
+
+class DrbdManageIscsiTestCase(test.TestCase):
def setUp(self):
self.ctxt = context.get_admin_context()
self.configuration.san_is_local = True
self.configuration.reserved_percentage = 1
- super(DrbdManageTestCase, self).setUp()
+ super(DrbdManageIscsiTestCase, self).setUp()
self.stubs.Set(importutils, 'import_object',
self.fake_import_object)
- self.stubs.Set(drbdmanagedrv.DrbdManageDriver,
+ self.stubs.Set(drv.DrbdManageBaseDriver,
'call_or_reconnect',
self.fake_issue_dbus_call)
- self.stubs.Set(drbdmanagedrv.DrbdManageDriver,
+ self.stubs.Set(drv.DrbdManageBaseDriver,
'dbus_connect',
self.fake_issue_dbus_connect)
-
- sys.modules['cinder.volume.drivers.drbdmanagedrv'].dm_const \
- = mock_dm_const
- sys.modules['cinder.volume.drivers.drbdmanagedrv'].dm_utils \
- = mock_dm_utils
- sys.modules['cinder.volume.drivers.drbdmanagedrv'].dm_exc \
- = mock_dm_exc
+ self.stubs.Set(drv.DrbdManageBaseDriver,
+ '_wait_for_node_assignment',
+ self.fake_wait_node_assignment)
self.configuration.safe_get = lambda x: 'fake'
def fake_issue_dbus_call(self, fn, *args):
return fn(*args)
+ def fake_wait_node_assignment(self, *args, **kwargs):
+ return True
+
def fake_issue_dbus_connect(self):
self.odm = DrbdManageFakeDriver()
def call_or_reconnect(self, method, *params):
return method(*params)
+ def fake_is_external_node(self, name):
+ return False
+
# Tests per se
def test_create_volume(self):
'volume_type_id': 'drbdmanage',
'created_at': timeutils.utcnow()}
- dmd = drbdmanagedrv.DrbdManageDriver(configuration=self.configuration)
+ dmd = drv.DrbdManageIscsiDriver(configuration=self.configuration)
dmd.drbdmanage_devs_on_controller = False
dmd.odm = DrbdManageFakeDriver()
dmd.create_volume(testvol)
'volume_type_id': 'drbdmanage',
'created_at': timeutils.utcnow()}
- dmd = drbdmanagedrv.DrbdManageDriver(configuration=self.configuration)
+ dmd = drv.DrbdManageIscsiDriver(configuration=self.configuration)
dmd.drbdmanage_devs_on_controller = True
dmd.odm = DrbdManageFakeDriver()
dmd.create_volume(testvol)
'volume_type_id': 'drbdmanage',
'created_at': timeutils.utcnow()}
- dmd = drbdmanagedrv.DrbdManageDriver(configuration=self.configuration)
+ dmd = drv.DrbdManageIscsiDriver(configuration=self.configuration)
dmd.odm = DrbdManageFakeDriver()
dmd.delete_volume(testvol)
self.assertEqual("list_volumes", dmd.odm.calls[0][0])
- self.assertEqual(testvol['id'], dmd.odm.calls[0][3]["cinder-id"])
+ self.assertEqual(testvol['id'], dmd.odm.calls[0][3]["aux:cinder-id"])
self.assertEqual("remove_volume", dmd.odm.calls[1][0])
def test_local_path(self):
'volume_type_id': 'drbdmanage',
'created_at': timeutils.utcnow()}
- dmd = drbdmanagedrv.DrbdManageDriver(configuration=self.configuration)
+ dmd = drv.DrbdManageIscsiDriver(configuration=self.configuration)
dmd.odm = DrbdManageFakeDriver()
data = dmd.local_path(testvol)
self.assertTrue(data.startswith("/dev/drbd"))
testsnap = {'id': 'ca253fd0-8068-11e4-98c0-5254008ea111',
'volume_id': 'ba253fd0-8068-11e4-98c0-5254008ea111'}
- dmd = drbdmanagedrv.DrbdManageDriver(configuration=self.configuration)
+ dmd = drv.DrbdManageIscsiDriver(configuration=self.configuration)
dmd.odm = DrbdManageFakeDriver()
dmd.create_snapshot(testsnap)
self.assertEqual("list_volumes", dmd.odm.calls[0][0])
def test_delete_snapshot(self):
testsnap = {'id': 'ca253fd0-8068-11e4-98c0-5254008ea111'}
- dmd = drbdmanagedrv.DrbdManageDriver(configuration=self.configuration)
+ dmd = drv.DrbdManageIscsiDriver(configuration=self.configuration)
dmd.odm = DrbdManageFakeDriver()
dmd.delete_snapshot(testsnap)
self.assertEqual("list_snapshots", dmd.odm.calls[0][0])
'volume_type_id': 'drbdmanage',
'created_at': timeutils.utcnow()}
- dmd = drbdmanagedrv.DrbdManageDriver(configuration=self.configuration)
+ dmd = drv.DrbdManageIscsiDriver(configuration=self.configuration)
dmd.odm = DrbdManageFakeDriver()
dmd.extend_volume(testvol, 5)
self.assertEqual("list_volumes", dmd.odm.calls[0][0])
- self.assertEqual(testvol['id'], dmd.odm.calls[0][3]["cinder-id"])
+ self.assertEqual(testvol['id'], dmd.odm.calls[0][3]["aux:cinder-id"])
self.assertEqual("resize_volume", dmd.odm.calls[1][0])
self.assertEqual("res", dmd.odm.calls[1][1])
self.assertEqual(2, dmd.odm.calls[1][2])
newvol = {'id': 'ca253fd0-8068-11e4-98c0-5254008ea111'}
- dmd = drbdmanagedrv.DrbdManageDriver(configuration=self.configuration)
+ dmd = drv.DrbdManageIscsiDriver(configuration=self.configuration)
dmd.odm = DrbdManageFakeDriver()
dmd.create_cloned_volume(newvol, srcvol)
self.assertEqual("list_volumes", dmd.odm.calls[0][0])
self.assertEqual("list_snapshots", dmd.odm.calls[5][0])
self.assertEqual("remove_snapshot", dmd.odm.calls[6][0])
self.assertEqual("remove_snapshot", dmd.odm.calls[6][0])
+
+
+class DrbdManageDrbdTestCase(DrbdManageIscsiTestCase):
+
+ def setUp(self):
+ super(DrbdManageDrbdTestCase, self).setUp()
+
+ self.stubs.Set(drv.DrbdManageDrbdDriver,
+ '_is_external_node',
+ self.fake_is_external_node)
+
+ def test_drbd_create_export(self):
+ volume = {'project_id': 'testprjid',
+ 'name': 'testvol',
+ 'size': 1,
+ 'id': 'ba253fd0-8068-11e4-98c0-5254008ea111',
+ 'volume_type_id': 'drbdmanage',
+ 'created_at': timeutils.utcnow()}
+
+ connector = {'host': 'node99',
+ 'ip': '127.0.0.99'}
+
+ dmd = drv.DrbdManageDrbdDriver(configuration=self.configuration)
+ dmd.odm = DrbdManageFakeDriver()
+
+ x = dmd.create_export({}, volume, connector)
+ self.assertEqual("list_volumes", dmd.odm.calls[0][0])
+ self.assertEqual("create_node", dmd.odm.calls[1][0])
+ self.assertEqual("assign", dmd.odm.calls[2][0])
+ # local_path
+ self.assertEqual("list_volumes", dmd.odm.calls[3][0])
+ self.assertEqual("text_query", dmd.odm.calls[4][0])
+ self.assertEqual("local", x["driver_volume_type"])
"""
This driver connects Cinder to an installed DRBDmanage instance, see
- http://oss.linbit.com/drbdmanage/
- http://git.linbit.com/drbdmanage.git/
+ http://drbd.linbit.com/users-guide-9.0/ch-openstack.html
for more details.
"""
+
+import eventlet
import six
import socket
+import time
import uuid
from oslo_config import cfg
from cinder import exception
-from cinder.i18n import _, _LW, _LI
+from cinder.i18n import _, _LW, _LI, _LE
from cinder.volume import driver
try:
import drbdmanage.exceptions as dm_exc
import drbdmanage.utils as dm_utils
except ImportError:
+ # Used for the tests, when no DRBDmanage is installed
dbus = None
dm_const = None
dm_exc = None
LOG = logging.getLogger(__name__)
drbd_opts = [
- cfg.StrOpt('drbdmanage_redundancy',
- default='1',
+ cfg.IntOpt('drbdmanage_redundancy',
+ default=1,
help='Number of nodes that should replicate the data.'),
cfg.BoolOpt('drbdmanage_devs_on_controller',
default=True,
AUX_PROP_CINDER_VOL_ID = "cinder-id"
+AUX_PROP_TEMP_CLIENT = "cinder-is-temp-client"
DM_VN_PREFIX = 'CV_' # sadly 2CV isn't allowed by DRBDmanage
DM_SN_PREFIX = 'SN_'
-class DrbdManageDriver(driver.VolumeDriver):
+# Need to be set later, so that the tests can fake
+CS_DEPLOYED = None
+CS_DISKLESS = None
+CS_UPD_CON = None
+
+
+class DrbdManageBaseDriver(driver.VolumeDriver):
"""Cinder driver that uses DRBDmanage for storage."""
- VERSION = '1.0.0'
+ VERSION = '1.1.0'
drbdmanage_dbus_name = 'org.drbd.drbdmanaged'
drbdmanage_dbus_interface = '/interface'
def __init__(self, *args, **kwargs):
self.empty_list = dbus.Array([], signature="a(s)")
self.empty_dict = dbus.Array([], signature="a(ss)")
- super(DrbdManageDriver, self).__init__(*args, **kwargs)
+
+ super(DrbdManageBaseDriver, self).__init__(*args, **kwargs)
+
self.configuration.append_config_values(drbd_opts)
if not self.drbdmanage_dbus_name:
self.drbdmanage_dbus_name = 'org.drbd.drbdmanaged'
True))
self.dm_control_vol = ".drbdctrl"
- # Copied from the LVM driver, see
- # I43190d1dac33748fe55fa00f260f32ab209be656
- target_driver = self.target_mapping[
- self.configuration.safe_get('iscsi_helper')]
+ self.backend_name = self.configuration.safe_get(
+ 'volume_backend_name') or 'drbdmanage'
- LOG.debug('Attempting to initialize DRBD driver with the '
- 'following target_driver: %s',
- target_driver)
-
- self.target_driver = importutils.import_object(
- target_driver,
- configuration=self.configuration,
- db=self.db,
- executor=self._execute)
+ # needed as per pep8:
+ # F841 local variable 'CS_DEPLOYED' is assigned to but never used
+ global CS_DEPLOYED, CS_DISKLESS, CS_UPD_CON
+ CS_DEPLOYED = dm_const.CSTATE_PREFIX + dm_const.FLAG_DEPLOY
+ CS_DISKLESS = dm_const.CSTATE_PREFIX + dm_const.FLAG_DISKLESS
+ CS_UPD_CON = dm_const.CSTATE_PREFIX + dm_const.FLAG_UPD_CON
def dbus_connect(self):
self.odm = dbus.SystemBus().get_object(self.drbdmanage_dbus_name,
def do_setup(self, context):
"""Any initialization the volume driver does while starting."""
- super(DrbdManageDriver, self).do_setup(context)
+ super(DrbdManageBaseDriver, self).do_setup(context)
self.dbus_connect()
def check_for_setup_error(self):
except ValueError:
return None
+ def _wait_for_node_assignment(self, res_name, vol_nr, nodenames,
+ filter_props=None, timeout=90,
+ check_vol_deployed=True):
+ """Return True as soon as one assignment matches the filter."""
+
+ if not filter_props:
+ filter_props = self.empty_dict
+
+ end_time = time.time() + timeout
+
+ retry = 0
+ while time.time() < end_time:
+ res, assgs = self.call_or_reconnect(self.odm.list_assignments,
+ nodenames, [res_name], 0,
+ filter_props, self.empty_list)
+ self._check_result(res)
+
+ if len(assgs) > 0:
+ for assg in assgs:
+ vols = assg[3]
+
+ for v_nr, v_prop in vols:
+ if (v_nr == vol_nr):
+ if not check_vol_deployed:
+ # no need to check
+ return True
+
+ if v_prop[CS_DEPLOYED] == dm_const.BOOL_TRUE:
+ return True
+
+ retry += 1
+ # Not yet
+ LOG.warning(_LW('Try #%(try)d: Volume "%(res)s"/%(vol)d '
+ 'not yet deployed on "%(host)s", waiting.'),
+ {'try': retry, 'host': nodenames,
+ 'res': res_name, 'vol': vol_nr})
+
+ eventlet.sleep(min(0.5 + retry / 5, 2))
+
+ # Timeout
+ return False
+
def _priv_hash_from_volume(self, volume):
return dm_utils.dict_to_aux_props({
AUX_PROP_CINDER_VOL_ID: volume['id'],
LOG.debug("volume %(uuid)s is %(res)s/%(nr)d; %(rprop)s, %(vprop)s",
{'uuid': v_uuid, 'res': r_name, 'nr': v_nr,
- 'rprop': r_props, 'vprop': v_props})
+ 'rprop': dict(r_props), 'vprop': dict(v_props)})
return r_name, v_nr, r_props, v_props
return r_name, s_name, s_props
def _resource_name_volnr_for_volume(self, volume, empty_ok=False):
- res, vol, _, _ = self._res_and_vl_data_for_volume(volume, empty_ok)
+ res, vol, __, __ = self._res_and_vl_data_for_volume(volume, empty_ok)
return res, vol
def local_path(self, volume):
dres,
str(dvol)])
self._check_result(res)
+
if len(data) == 1:
return data[0]
+
message = _('Got bad path information from DRBDmanage! (%s)') % data
raise exception.VolumeBackendAPIException(data=message)
# deploy gave an error on a previous try (like ENOSPC).
# Still, there might or might not be the volume in the resource -
# we have to check that explicitly.
- (_, drbd_vol) = self._resource_name_volnr_for_volume(volume,
- empty_ok=True)
+ (__, drbd_vol) = self._resource_name_volnr_for_volume(volume,
+ empty_ok=True)
if not drbd_vol:
props = self._priv_hash_from_volume(volume)
# TODO(PM): properties - redundancy, etc
0, True)
self._check_result(res)
+ # TODO(pm): CG
+ self._wait_for_node_assignment(dres, 0, self.empty_list)
+
if self.drbdmanage_devs_on_controller:
- # FIXME: Consistency groups, vol#
+ # TODO(pm): CG
res = self.call_or_reconnect(self.odm.assign,
socket.gethostname(),
dres,
self.empty_dict)
self._check_result(res, ignore=[dm_exc.DM_EEXIST])
- return 0
+ return {}
def delete_volume(self, volume):
"""Deletes a resource."""
data["vendor_name"] = 'Open Source'
data["driver_version"] = self.VERSION
- data["storage_protocol"] = self.target_driver.protocol
# This has to match the name set in the cinder volume driver spec,
# so keep it lowercase
- data["volume_backend_name"] = "drbdmanage"
+ data["volume_backend_name"] = self.backend_name
data["pools"] = []
res, free, total = self.call_or_reconnect(self.odm.cluster_free_query,
self.drbdmanage_redundancy)
self._check_result(res)
- location_info = ('DrbdManageDriver:%(cvol)s:%(dbus)s' %
+ location_info = ('DrbdManageBaseDriver:%(cvol)s:%(dbus)s' %
{'cvol': self.dm_control_vol,
'dbus': self.drbdmanage_dbus_name})
+ # add volumes
+ res, rl = self.call_or_reconnect(self.odm.list_volumes,
+ self.empty_list,
+ 0,
+ self.empty_dict,
+ self.empty_list)
+ self._check_result(res)
+ total_volumes = 0
+ for res in rl:
+ total_volumes += len(res[2])
+
# TODO(PM): multiple DRBDmanage instances and/or multiple pools
single_pool = {}
single_pool.update(dict(
total_capacity_gb=self._vol_size_to_cinder(total),
reserved_percentage=self.configuration.reserved_percentage,
location_info=location_info,
+ total_volumes=total_volumes,
+ filter_function=self.get_filter_function(),
+ goodness_function=self.get_goodness_function(),
QoS_support=False))
data["pools"].append(single_pool)
self._stats = data
-
- def get_volume_stats(self, refresh=True):
- """Get volume status."""
-
- self._update_volume_stats()
return self._stats
def extend_volume(self, volume, new_size):
[dres],
0,
self.empty_dict,
- self.empty_dict)
+ self.empty_list)
self._check_result(res)
nodes = [d[0] for d in data]
dres, sname, True)
return self._check_result(res, ignore=[dm_exc.DM_ENOENT])
- # ####### Interface methods for DataPath (Target Driver) ########
+
+# Class with iSCSI interface methods
+
+class DrbdManageIscsiDriver(DrbdManageBaseDriver):
+ """Cinder driver that uses the iSCSI protocol. """
+
+ def __init__(self, *args, **kwargs):
+ super(DrbdManageIscsiDriver, self).__init__(*args, **kwargs)
+ target_driver = self.target_mapping[
+ self.configuration.safe_get('iscsi_helper')]
+
+ LOG.debug('Attempting to initialize DRBD driver with the '
+ 'following target_driver: %s',
+ target_driver)
+
+ self.target_driver = importutils.import_object(
+ target_driver,
+ configuration=self.configuration,
+ db=self.db,
+ executor=self._execute)
+
+ def get_volume_stats(self, refresh=False):
+ """Get volume status."""
+
+ self._update_volume_stats()
+ self._stats["storage_protocol"] = "iSCSI"
+ return self._stats
def ensure_export(self, context, volume):
volume_path = self.local_path(volume)
return self.target_driver.validate_connector(connector)
def terminate_connection(self, volume, connector, **kwargs):
+ return self.target_driver.terminate_connection(volume,
+ connector,
+ **kwargs)
return None
+
+# for backwards compatibility keep the old class name, too
+DrbdManageDriver = DrbdManageIscsiDriver
+
+
+# Class with DRBD transport mode
+class DrbdManageDrbdDriver(DrbdManageBaseDriver):
+ """Cinder driver that uses the DRBD protocol. """
+
+ def __init__(self, *args, **kwargs):
+ super(DrbdManageDrbdDriver, self).__init__(*args, **kwargs)
+
+ def get_volume_stats(self, refresh=False):
+ """Get volume status."""
+
+ self._update_volume_stats()
+ self._stats["storage_protocol"] = "DRBD"
+ return self._stats
+
+ def _return_local_access(self, nodename, volume,
+ dres=None, volume_path=None):
+
+ if not volume_path:
+ volume_path = self.local_path(volume)
+
+ return {
+ 'driver_volume_type': 'local',
+ 'data': {
+ "device_path": volume_path
+ }
+ }
+
+ def _return_drbdadm_config(self, volume, nodename,
+ dres=None, volume_path=None):
+
+ if not dres:
+ dres, dvol = self._resource_name_volnr_for_volume(volume)
+
+ res, data = self.call_or_reconnect(
+ self.odm.text_query,
+ ['export_conf_split_up', nodename, dres])
+ self._check_result(res)
+
+ config = six.text_type(data.pop(0))
+ subst_data = {}
+ while len(data):
+ k = data.pop(0)
+ subst_data[k] = data.pop(0)
+
+ if not volume_path:
+ volume_path = self.local_path(volume)
+
+ return {
+ 'driver_volume_type': 'drbd',
+ 'data': {
+ 'provider_location': ' '.join('drbd', nodename),
+ 'device': volume_path,
+ # TODO(pm): consistency groups
+ 'devices': [volume_path],
+ 'provider_auth': subst_data['shared-secret'],
+ 'config': config,
+ 'name': dres,
+ }
+ }
+
+ def _is_external_node(self, nodename):
+ """Return whether the given node is an "external" node."""
+
+ # If the node accessing the data (the "initiator" in iSCSI speak,
+ # "client" or "target" otherwise) is marked as an FLAG_EXTERNAL
+ # node, it does not have DRBDmanage active - and that means
+ # we have to send the necessary DRBD configuration.
+ #
+ # If DRBDmanage is running there, just pushing the (client)
+ # assignment is enough to make the local path available.
+
+ res, nodes = self.call_or_reconnect(self.odm.list_nodes,
+ [nodename], 0,
+ self.empty_dict,
+ [dm_const.FLAG_EXTERNAL])
+ self._check_result(res)
+
+ if len(nodes) != 1:
+ msg = _('Expected exactly one node called "%s"') % nodename
+ LOG.error(msg)
+ raise exception.VolumeDriverException(message=msg)
+
+ __, nodeattr = nodes[0]
+
+ return getattr(nodeattr, dm_const.FLAG_EXTERNAL,
+ dm_const.BOOL_FALSE) == dm_const.BOOL_TRUE
+
+ def _return_connection_data(self, nodename, volume, dres=None):
+ if self._is_external_node(nodename):
+ return self._return_drbdadm_config(nodename, volume, dres=dres)
+ else:
+ return self._return_local_access(nodename, volume)
+
+ def create_export(self, context, volume, connector):
+ dres, dvol = self._resource_name_volnr_for_volume(volume)
+
+ nodename = connector["host"]
+
+ # Ensure the node is known to DRBDmanage.
+ # Note that this does *not* mean that DRBDmanage has to
+ # be installed on it!
+ # This is just so that DRBD allows the IP to connect.
+ node_prop = {
+ dm_const.NODE_ADDR: connector["ip"],
+ dm_const.FLAG_DRBDCTRL: dm_const.BOOL_FALSE,
+ dm_const.FLAG_STORAGE: dm_const.BOOL_FALSE,
+ dm_const.FLAG_EXTERNAL: dm_const.BOOL_TRUE,
+ }
+ res = self.call_or_reconnect(
+ self.odm.create_node, nodename, node_prop)
+ self._check_result(res, ignore=[dm_exc.DM_EEXIST])
+
+ # Ensure the data is accessible, by creating an assignment.
+ assg_prop = {
+ dm_const.FLAG_DISKLESS: dm_const.BOOL_TRUE,
+ }
+ # If we create the assignment here, it's temporary -
+ # and has to be removed later on again.
+ assg_prop.update(dm_utils.aux_props_to_dict({
+ AUX_PROP_TEMP_CLIENT: dm_const.BOOL_TRUE,
+ }))
+
+ res = self.call_or_reconnect(
+ self.odm.assign, nodename, dres, assg_prop)
+ self._check_result(res, ignore=[dm_exc.DM_EEXIST])
+
+ # Wait for DRBDmanage to have completed that action.
+
+ # A DRBDmanage controlled node will set the cstate:deploy flag;
+ # an external node will not be available to change it, so we have
+ # to wait for the storage nodes to remove the upd_con flag
+ # (ie. they're now ready to receive the connection).
+ if self._is_external_node(nodename):
+ self._wait_for_node_assignment(
+ dres, dvol, [],
+ check_vol_deployed=False,
+ filter_props={
+ # must be deployed
+ CS_DEPLOYED: dm_const.BOOL_TRUE,
+ # must be a storage node (not diskless),
+ CS_DISKLESS: dm_const.BOOL_FALSE,
+ # connection must be available, no need for updating
+ CS_UPD_CON: dm_const.BOOL_FALSE,
+ })
+ else:
+ self._wait_for_node_assignment(
+ dres, dvol, [nodename],
+ check_vol_deployed=True,
+ filter_props={
+ CS_DEPLOYED: dm_const.BOOL_TRUE,
+ })
+
+ return self._return_connection_data(nodename, volume)
+
+ def ensure_export(self, context, volume):
+
+ fields = context['provider_location'].split(" ")
+ nodename = fields[1]
+
+ return self._return_connection_data(nodename, volume)
+
+ def initialize_connection(self, volume, connector):
+
+ nodename = connector["host"]
+
+ return self._return_connection_data(nodename, volume)
+
+ def terminate_connection(self, volume, connector,
+ force=False, **kwargs):
+ dres, dvol = self._resource_name_volnr_for_volume(
+ volume, empty_ok=True)
+ if not dres:
+ return
+
+ nodename = connector["host"]
+
+ # If the DRBD volume is diskless on that node, we remove it;
+ # if it has local storage, we keep it.
+ res, data = self.call_or_reconnect(
+ self.odm.list_assignments,
+ [nodename], [dres], 0,
+ self.empty_list, self.empty_list)
+ self._check_result(res, ignore=[dm_exc.DM_ENOENT])
+
+ if len(data) < 1:
+ # already removed?!
+ LOG.info(_LI('DRBD connection for %s already removed'),
+ volume['id'])
+ elif len(data) == 1:
+ __, __, props, __ = data[0]
+ my_props = dm_utils.dict_to_aux_props(props)
+ diskless = getattr(props,
+ dm_const.FLAG_DISKLESS,
+ dm_const.BOOL_FALSE)
+ temp_cli = getattr(my_props,
+ AUX_PROP_TEMP_CLIENT,
+ dm_const.BOOL_FALSE)
+ # If diskless assigned,
+ if ((diskless == dm_const.BOOL_TRUE) and
+ (temp_cli == dm_const.BOOL_TRUE)):
+ # remove the assignment
+
+ # TODO(pm): does it make sense to relay "force" here?
+ # What are the semantics?
+
+ # TODO(pm): consistency groups shouldn't really
+ # remove until *all* volumes are detached
+
+ res = self.call_or_reconnect(self.odm.unassign,
+ nodename, dres, force)
+ self._check_result(res, ignore=[dm_exc.DM_ENOENT])
+ else:
+ # more than one assignment?
+ LOG.error(_LE("DRBDmanage: too many assignments returned."))
+ return
+
+ def remove_export(self, context, volume):
+ pass