Implement replicate_volume & failover on SolidFire
This adds the Cheesecake Replication implementation for the SolidFire driver. We add the necessary methods for creating a replicated volume and performing a failover properly that updates the status of volumes on the failover host. We also make sure that things like delete, attach etc work in a failed over scenario. For SolidFire we are using the remote cluster mvip as the backend_id, here's an example replication_device entry for a cinder.conf: replication_device = backend_id:192.168.139.102,mvip:192.168.139.102,login:admin,password:admin Note that there's an independent fix from this patch series that needs to be applied in order for things to work properly: https://review.openstack.org/#/c/299723/ The last remaining task is to ensure we detect and deal with a restart when we're in a failed over state. Change-Id: I78244da226abce2544d0c3196fccdec50bf5b8e5
This commit is contained in:
parent
70fff3e904
commit
3f5e040e73
@ -108,7 +108,7 @@ class SolidFireVolumeTestCase(test.TestCase):
|
|||||||
'thinProvisioningPercent': 100}}}
|
'thinProvisioningPercent': 100}}}
|
||||||
return data
|
return data
|
||||||
|
|
||||||
elif method is 'GetClusterInfo' and version == '1.0':
|
elif method is 'GetClusterInfo':
|
||||||
results = {
|
results = {
|
||||||
'result':
|
'result':
|
||||||
{'clusterInfo':
|
{'clusterInfo':
|
||||||
@ -122,7 +122,13 @@ class SolidFireVolumeTestCase(test.TestCase):
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
elif method is 'GetClusterVersionInfo':
|
elif method is 'GetClusterVersionInfo':
|
||||||
return {'result': {'clusterAPIVersion': '8.0'}}
|
return {'id': None, 'result': {'softwareVersionInfo':
|
||||||
|
{'pendingVersion': '8.2.1.4',
|
||||||
|
'packageName': '',
|
||||||
|
'currentVersion': '8.2.1.4',
|
||||||
|
'nodeID': 0, 'startTime': ''},
|
||||||
|
'clusterVersion': '8.2.1.4',
|
||||||
|
'clusterAPIVersion': '8.2'}}
|
||||||
|
|
||||||
elif method is 'AddAccount' and version == '1.0':
|
elif method is 'AddAccount' and version == '1.0':
|
||||||
return {'result': {'accountID': 25}, 'id': 1}
|
return {'result': {'accountID': 25}, 'id': 1}
|
||||||
@ -192,6 +198,8 @@ class SolidFireVolumeTestCase(test.TestCase):
|
|||||||
return {'result': {}}
|
return {'result': {}}
|
||||||
elif method is 'GetClusterVersionInfo':
|
elif method is 'GetClusterVersionInfo':
|
||||||
return {'result': {'clusterAPIVersion': '8.0'}}
|
return {'result': {'clusterAPIVersion': '8.0'}}
|
||||||
|
elif method is 'StartVolumePairing':
|
||||||
|
return {'result': {'volumePairingKey': 'fake-pairing-key'}}
|
||||||
else:
|
else:
|
||||||
# Crap, unimplemented API call in Fake
|
# Crap, unimplemented API call in Fake
|
||||||
return None
|
return None
|
||||||
@ -221,7 +229,7 @@ class SolidFireVolumeTestCase(test.TestCase):
|
|||||||
def fake_get_cluster_version_info(self):
|
def fake_get_cluster_version_info(self):
|
||||||
return
|
return
|
||||||
|
|
||||||
def fake_get_model_info(self, account, vid):
|
def fake_get_model_info(self, account, vid, endpoint=None):
|
||||||
return {'fake': 'fake-model'}
|
return {'fake': 'fake-model'}
|
||||||
|
|
||||||
@mock.patch.object(solidfire.SolidFireDriver, '_issue_api_request')
|
@mock.patch.object(solidfire.SolidFireDriver, '_issue_api_request')
|
||||||
@ -849,6 +857,7 @@ class SolidFireVolumeTestCase(test.TestCase):
|
|||||||
self.assertEqual('UUID-a720b3c0-d1f0-11e1-9b23-0800200c9a66',
|
self.assertEqual('UUID-a720b3c0-d1f0-11e1-9b23-0800200c9a66',
|
||||||
sf_vol_object['name'])
|
sf_vol_object['name'])
|
||||||
|
|
||||||
|
@mock.patch.object(solidfire.SolidFireDriver, '_update_cluster_status')
|
||||||
@mock.patch.object(solidfire.SolidFireDriver, '_issue_api_request')
|
@mock.patch.object(solidfire.SolidFireDriver, '_issue_api_request')
|
||||||
@mock.patch.object(solidfire.SolidFireDriver, '_get_sfaccount')
|
@mock.patch.object(solidfire.SolidFireDriver, '_get_sfaccount')
|
||||||
@mock.patch.object(solidfire.SolidFireDriver, '_get_sf_volume')
|
@mock.patch.object(solidfire.SolidFireDriver, '_get_sf_volume')
|
||||||
@ -857,7 +866,8 @@ class SolidFireVolumeTestCase(test.TestCase):
|
|||||||
_mock_create_image_volume,
|
_mock_create_image_volume,
|
||||||
_mock_get_sf_volume,
|
_mock_get_sf_volume,
|
||||||
_mock_get_sfaccount,
|
_mock_get_sfaccount,
|
||||||
_mock_issue_api_request):
|
_mock_issue_api_request,
|
||||||
|
_mock_update_cluster_status):
|
||||||
fake_sf_vref = {
|
fake_sf_vref = {
|
||||||
'status': 'active', 'volumeID': 1,
|
'status': 'active', 'volumeID': 1,
|
||||||
'attributes': {
|
'attributes': {
|
||||||
@ -867,7 +877,8 @@ class SolidFireVolumeTestCase(test.TestCase):
|
|||||||
'image_name': 'fake-image',
|
'image_name': 'fake-image',
|
||||||
'image_created_at': '2014-12-17T00:16:23+00:00'}}}
|
'image_created_at': '2014-12-17T00:16:23+00:00'}}}
|
||||||
|
|
||||||
_mock_issue_api_request.return_value.side_effect = (
|
_mock_update_cluster_status.return_value = None
|
||||||
|
_mock_issue_api_request.side_effect = (
|
||||||
self.fake_issue_api_request)
|
self.fake_issue_api_request)
|
||||||
_mock_get_sfaccount.return_value = {'username': 'openstack-vtemplate',
|
_mock_get_sfaccount.return_value = {'username': 'openstack-vtemplate',
|
||||||
'accountID': 7777}
|
'accountID': 7777}
|
||||||
@ -881,10 +892,10 @@ class SolidFireVolumeTestCase(test.TestCase):
|
|||||||
image_service = 'null'
|
image_service = 'null'
|
||||||
|
|
||||||
sfv = solidfire.SolidFireDriver(configuration=self.configuration)
|
sfv = solidfire.SolidFireDriver(configuration=self.configuration)
|
||||||
_mock_issue_api_request.return_value = {'result': 'ok'}
|
|
||||||
sfv._verify_image_volume(self.ctxt, image_meta, image_service)
|
sfv._verify_image_volume(self.ctxt, image_meta, image_service)
|
||||||
self.assertTrue(_mock_create_image_volume.called)
|
self.assertTrue(_mock_create_image_volume.called)
|
||||||
|
|
||||||
|
@mock.patch.object(solidfire.SolidFireDriver, '_update_cluster_status')
|
||||||
@mock.patch.object(solidfire.SolidFireDriver, '_issue_api_request')
|
@mock.patch.object(solidfire.SolidFireDriver, '_issue_api_request')
|
||||||
@mock.patch.object(solidfire.SolidFireDriver, '_get_sfaccount')
|
@mock.patch.object(solidfire.SolidFireDriver, '_get_sfaccount')
|
||||||
@mock.patch.object(solidfire.SolidFireDriver, '_get_sf_volume')
|
@mock.patch.object(solidfire.SolidFireDriver, '_get_sf_volume')
|
||||||
@ -893,8 +904,11 @@ class SolidFireVolumeTestCase(test.TestCase):
|
|||||||
_mock_create_image_volume,
|
_mock_create_image_volume,
|
||||||
_mock_get_sf_volume,
|
_mock_get_sf_volume,
|
||||||
_mock_get_sfaccount,
|
_mock_get_sfaccount,
|
||||||
_mock_issue_api_request):
|
_mock_issue_api_request,
|
||||||
|
_mock_update_cluster_status):
|
||||||
|
|
||||||
|
_mock_issue_api_request.side_effect = self.fake_issue_api_request
|
||||||
|
_mock_update_cluster_status.return_value = None
|
||||||
_mock_get_sfaccount.return_value = {'username': 'openstack-vtemplate',
|
_mock_get_sfaccount.return_value = {'username': 'openstack-vtemplate',
|
||||||
'accountID': 7777}
|
'accountID': 7777}
|
||||||
_mock_get_sf_volume.return_value =\
|
_mock_get_sf_volume.return_value =\
|
||||||
@ -913,10 +927,7 @@ class SolidFireVolumeTestCase(test.TestCase):
|
|||||||
325355)}
|
325355)}
|
||||||
image_service = 'null'
|
image_service = 'null'
|
||||||
|
|
||||||
_mock_issue_api_request.return_value.side_effect = (
|
|
||||||
self.fake_issue_api_request)
|
|
||||||
sfv = solidfire.SolidFireDriver(configuration=self.configuration)
|
sfv = solidfire.SolidFireDriver(configuration=self.configuration)
|
||||||
_mock_issue_api_request.return_value = {'result': 'ok'}
|
|
||||||
|
|
||||||
sfv._verify_image_volume(self.ctxt, image_meta, image_service)
|
sfv._verify_image_volume(self.ctxt, image_meta, image_service)
|
||||||
self.assertFalse(_mock_create_image_volume.called)
|
self.assertFalse(_mock_create_image_volume.called)
|
||||||
@ -1001,12 +1012,13 @@ class SolidFireVolumeTestCase(test.TestCase):
|
|||||||
def test_configured_svip(self):
|
def test_configured_svip(self):
|
||||||
sfv = solidfire.SolidFireDriver(configuration=self.configuration)
|
sfv = solidfire.SolidFireDriver(configuration=self.configuration)
|
||||||
|
|
||||||
def _fake_get_volumes(account_id):
|
def _fake_get_volumes(account_id, endpoint=None):
|
||||||
return [{'volumeID': 1,
|
return [{'volumeID': 1,
|
||||||
'iqn': ''}]
|
'iqn': ''}]
|
||||||
|
|
||||||
def _fake_get_cluster_info():
|
def _fake_get_cluster_info():
|
||||||
return {'clusterInfo': {'svip': 1}}
|
return {'clusterInfo': {'svip': '10.10.10.10',
|
||||||
|
'mvip': '1.1.1.1'}}
|
||||||
|
|
||||||
with mock.patch.object(sfv,
|
with mock.patch.object(sfv,
|
||||||
'_get_volumes_by_sfaccount',
|
'_get_volumes_by_sfaccount',
|
||||||
@ -1116,7 +1128,7 @@ class SolidFireVolumeTestCase(test.TestCase):
|
|||||||
}
|
}
|
||||||
connector = {'initiator': 'iqn.2012-07.org.fake:01'}
|
connector = {'initiator': 'iqn.2012-07.org.fake:01'}
|
||||||
provider_id = testvol['provider_id']
|
provider_id = testvol['provider_id']
|
||||||
vol_id = int(sfv._parse_provider_id_string(provider_id)[0])
|
vol_id = int(provider_id.split()[0])
|
||||||
vag_id = 1
|
vag_id = 1
|
||||||
|
|
||||||
with mock.patch.object(sfv,
|
with mock.patch.object(sfv,
|
||||||
@ -1190,7 +1202,7 @@ class SolidFireVolumeTestCase(test.TestCase):
|
|||||||
'multiattach': False
|
'multiattach': False
|
||||||
}
|
}
|
||||||
provider_id = testvol['provider_id']
|
provider_id = testvol['provider_id']
|
||||||
vol_id = int(sfv._parse_provider_id_string(provider_id)[0])
|
vol_id = int(provider_id.split()[0])
|
||||||
connector = {'initiator': 'iqn.2012-07.org.fake:01'}
|
connector = {'initiator': 'iqn.2012-07.org.fake:01'}
|
||||||
vag_id = 1
|
vag_id = 1
|
||||||
vags = [{'attributes': {},
|
vags = [{'attributes': {},
|
||||||
@ -1731,3 +1743,49 @@ class SolidFireVolumeTestCase(test.TestCase):
|
|||||||
self.fail("Should have thrown Error")
|
self.fail("Should have thrown Error")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def test_set_rep_by_volume_type(self):
|
||||||
|
sfv = solidfire.SolidFireDriver(configuration=self.configuration)
|
||||||
|
sfv.cluster_pairs = [{'cluster_id': 'fake-id', 'cluster_mvip':
|
||||||
|
'fake-mvip'}]
|
||||||
|
ctxt = None
|
||||||
|
type_id = '290edb2a-f5ea-11e5-9ce9-5e5517507c66'
|
||||||
|
fake_type = {'extra_specs': {'replication': 'enabled'}}
|
||||||
|
with mock.patch.object(volume_types,
|
||||||
|
'get_volume_type',
|
||||||
|
return_value=fake_type):
|
||||||
|
self.assertEqual('fake-id', sfv._set_rep_by_volume_type(
|
||||||
|
ctxt,
|
||||||
|
type_id)['targets']['cluster_id'])
|
||||||
|
|
||||||
|
def test_replicate_volume(self):
|
||||||
|
sfv = solidfire.SolidFireDriver(configuration=self.configuration)
|
||||||
|
sfv.cluster_pairs = (
|
||||||
|
[{'uniqueID': 'lu9f', 'endpoint': {'passwd': 'admin', 'port':
|
||||||
|
443, 'url':
|
||||||
|
'https://192.168.139.102:443',
|
||||||
|
'svip': '10.10.8.134', 'mvip':
|
||||||
|
'192.168.139.102', 'login':
|
||||||
|
'admin'}, 'name':
|
||||||
|
'AutoTest2-6AjG-FOR-TEST-ONLY', 'clusterPairID': 33, 'uuid':
|
||||||
|
'9c499d4b-8fff-48b4-b875-27601d5d9889', 'svip': '10.10.23.2',
|
||||||
|
'mvipNodeID': 1, 'repCount': 1, 'encryptionAtRestState':
|
||||||
|
'disabled', 'attributes': {}, 'mvip': '192.168.139.102',
|
||||||
|
'ensemble': ['10.10.5.130'], 'svipNodeID': 1}])
|
||||||
|
|
||||||
|
with mock.patch.object(sfv,
|
||||||
|
'_issue_api_request',
|
||||||
|
self.fake_issue_api_request),\
|
||||||
|
mock.patch.object(sfv,
|
||||||
|
'_get_sfaccount_by_name',
|
||||||
|
return_value={'accountID': 1}),\
|
||||||
|
mock.patch.object(sfv,
|
||||||
|
'_do_volume_create',
|
||||||
|
return_value={'provider_id': '1 2 xxxx'}):
|
||||||
|
self.assertEqual({'provider_id': '1 2 xxxx'},
|
||||||
|
sfv._replicate_volume(
|
||||||
|
{'project_id': 1, 'volumeID': 1},
|
||||||
|
{'attributes': {}},
|
||||||
|
{'initiatorSecret': 'shhh',
|
||||||
|
'targetSecret': 'dont-tell'},
|
||||||
|
{}))
|
||||||
|
@ -185,6 +185,7 @@ class SolidFireDriver(san.SanISCSIDriver):
|
|||||||
self.volume_map = {}
|
self.volume_map = {}
|
||||||
self.cluster_pairs = []
|
self.cluster_pairs = []
|
||||||
self.replication_enabled = False
|
self.replication_enabled = False
|
||||||
|
self.failed_over = False
|
||||||
self.target_driver = SolidFireISCSI(solidfire_driver=self,
|
self.target_driver = SolidFireISCSI(solidfire_driver=self,
|
||||||
configuration=self.configuration)
|
configuration=self.configuration)
|
||||||
self._set_active_cluster_info()
|
self._set_active_cluster_info()
|
||||||
@ -221,6 +222,7 @@ class SolidFireDriver(san.SanISCSIDriver):
|
|||||||
with excutils.save_and_reraise_exception():
|
with excutils.save_and_reraise_exception():
|
||||||
LOG.error(_LE('Cluster pairing failed: %s'), ex.msg)
|
LOG.error(_LE('Cluster pairing failed: %s'), ex.msg)
|
||||||
LOG.debug(('Initialized Cluster pair with ID: %s'), pair_id)
|
LOG.debug(('Initialized Cluster pair with ID: %s'), pair_id)
|
||||||
|
remote_device['clusterPairID'] = pair_id
|
||||||
return pair_id
|
return pair_id
|
||||||
|
|
||||||
def _get_remote_cluster_info(self, remote_endpoint):
|
def _get_remote_cluster_info(self, remote_endpoint):
|
||||||
@ -244,15 +246,22 @@ class SolidFireDriver(san.SanISCSIDriver):
|
|||||||
remote_endpoint = self._build_endpoint_info(**rd)
|
remote_endpoint = self._build_endpoint_info(**rd)
|
||||||
remote_info = self._get_remote_cluster_info(remote_endpoint)
|
remote_info = self._get_remote_cluster_info(remote_endpoint)
|
||||||
remote_info['endpoint'] = remote_endpoint
|
remote_info['endpoint'] = remote_endpoint
|
||||||
|
if not remote_info['endpoint']['svip']:
|
||||||
|
remote_info['endpoint']['svip'] = remote_info['svip'] + ':3260'
|
||||||
|
|
||||||
for ep in existing_pairs:
|
for ep in existing_pairs:
|
||||||
if rd['backend_id'] == ep['mvip']:
|
if rd['backend_id'] == ep['mvip']:
|
||||||
remote_pair = ep
|
remote_pair = ep
|
||||||
LOG.debug("Found remote pair: %s", remote_pair)
|
LOG.debug("Found remote pair: %s", remote_pair)
|
||||||
|
remote_info['clusterPairID'] = ep['clusterPairID']
|
||||||
|
break
|
||||||
|
|
||||||
if not remote_pair:
|
if not remote_pair:
|
||||||
|
# NOTE(jdg): create_remote_pairing sets the
|
||||||
|
# clusterPairID in remote_info for us
|
||||||
self._create_remote_pairing(remote_info)
|
self._create_remote_pairing(remote_info)
|
||||||
self.cluster_pairs.append(remote_info)
|
self.cluster_pairs.append(remote_info)
|
||||||
|
LOG.debug("Setting replication_enabled to True.")
|
||||||
self.replication_enabled = True
|
self.replication_enabled = True
|
||||||
|
|
||||||
def _set_active_cluster_info(self):
|
def _set_active_cluster_info(self):
|
||||||
@ -270,9 +279,6 @@ class SolidFireDriver(san.SanISCSIDriver):
|
|||||||
self.active_cluster_info['svip'] = (
|
self.active_cluster_info['svip'] = (
|
||||||
self.configuration.get('sf_svip'))
|
self.configuration.get('sf_svip'))
|
||||||
|
|
||||||
def _parse_provider_id_string(self, id_string):
|
|
||||||
return tuple(id_string.split())
|
|
||||||
|
|
||||||
def _create_provider_id_string(self,
|
def _create_provider_id_string(self,
|
||||||
resource_id,
|
resource_id,
|
||||||
account_or_vol_id):
|
account_or_vol_id):
|
||||||
@ -359,10 +365,9 @@ class SolidFireDriver(san.SanISCSIDriver):
|
|||||||
kwargs.get('port', self.configuration.sf_api_port))
|
kwargs.get('port', self.configuration.sf_api_port))
|
||||||
endpoint['url'] = 'https://%s:%s' % (endpoint['mvip'],
|
endpoint['url'] = 'https://%s:%s' % (endpoint['mvip'],
|
||||||
endpoint['port'])
|
endpoint['port'])
|
||||||
|
endpoint['svip'] = kwargs.get('svip', self.configuration.sf_svip)
|
||||||
if not endpoint.get('mvip', None) and kwargs.get('backend_id', None):
|
if not endpoint.get('mvip', None) and kwargs.get('backend_id', None):
|
||||||
endpoint['mvip'] = kwargs.get('backend_id')
|
endpoint['mvip'] = kwargs.get('backend_id')
|
||||||
|
|
||||||
# TODO(jdg): consider a call to GetAPI and setting version
|
|
||||||
return endpoint
|
return endpoint
|
||||||
|
|
||||||
@retry(retry_exc_tuple, tries=6)
|
@retry(retry_exc_tuple, tries=6)
|
||||||
@ -396,18 +401,22 @@ class SolidFireDriver(san.SanISCSIDriver):
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def _get_volumes_by_sfaccount(self, account_id):
|
def _get_volumes_by_sfaccount(self, account_id, endpoint=None):
|
||||||
"""Get all volumes on cluster for specified account."""
|
"""Get all volumes on cluster for specified account."""
|
||||||
params = {'accountID': account_id}
|
params = {'accountID': account_id}
|
||||||
return self._issue_api_request(
|
return self._issue_api_request(
|
||||||
'ListVolumesForAccount', params)['result']['volumes']
|
'ListVolumesForAccount',
|
||||||
|
params,
|
||||||
|
endpoint=endpoint)['result']['volumes']
|
||||||
|
|
||||||
def _get_sfaccount_by_name(self, sf_account_name):
|
def _get_sfaccount_by_name(self, sf_account_name, endpoint=None):
|
||||||
"""Get SolidFire account object by name."""
|
"""Get SolidFire account object by name."""
|
||||||
sfaccount = None
|
sfaccount = None
|
||||||
params = {'username': sf_account_name}
|
params = {'username': sf_account_name}
|
||||||
try:
|
try:
|
||||||
data = self._issue_api_request('GetAccountByName', params)
|
data = self._issue_api_request('GetAccountByName',
|
||||||
|
params,
|
||||||
|
endpoint=endpoint)
|
||||||
if 'result' in data and 'account' in data['result']:
|
if 'result' in data and 'account' in data['result']:
|
||||||
LOG.debug('Found solidfire account: %s', sf_account_name)
|
LOG.debug('Found solidfire account: %s', sf_account_name)
|
||||||
sfaccount = data['result']['account']
|
sfaccount = data['result']['account']
|
||||||
@ -463,18 +472,23 @@ class SolidFireDriver(san.SanISCSIDriver):
|
|||||||
char_set = string.ascii_uppercase + string.digits
|
char_set = string.ascii_uppercase + string.digits
|
||||||
return ''.join(random.sample(char_set, length))
|
return ''.join(random.sample(char_set, length))
|
||||||
|
|
||||||
def _get_model_info(self, sfaccount, sf_volume_id):
|
def _get_model_info(self, sfaccount, sf_volume_id, endpoint=None):
|
||||||
"""Gets the connection info for specified account and volume."""
|
"""Gets the connection info for specified account and volume."""
|
||||||
|
if endpoint:
|
||||||
|
iscsi_portal = endpoint['svip']
|
||||||
|
else:
|
||||||
iscsi_portal = self.active_cluster_info['svip']
|
iscsi_portal = self.active_cluster_info['svip']
|
||||||
|
|
||||||
if ':' not in iscsi_portal:
|
if ':' not in iscsi_portal:
|
||||||
iscsi_portal += ':3260'
|
iscsi_portal += ':3260'
|
||||||
|
|
||||||
chap_secret = sfaccount['targetSecret']
|
chap_secret = sfaccount['targetSecret']
|
||||||
|
|
||||||
found_volume = False
|
found_volume = False
|
||||||
iteration_count = 0
|
iteration_count = 0
|
||||||
while not found_volume and iteration_count < 600:
|
while not found_volume and iteration_count < 600:
|
||||||
volume_list = self._get_volumes_by_sfaccount(
|
volume_list = self._get_volumes_by_sfaccount(
|
||||||
sfaccount['accountID'])
|
sfaccount['accountID'], endpoint=endpoint)
|
||||||
iqn = None
|
iqn = None
|
||||||
for v in volume_list:
|
for v in volume_list:
|
||||||
if v['volumeID'] == sf_volume_id:
|
if v['volumeID'] == sf_volume_id:
|
||||||
@ -590,10 +604,11 @@ class SolidFireDriver(san.SanISCSIDriver):
|
|||||||
params['attributes'] = attributes
|
params['attributes'] = attributes
|
||||||
return self._issue_api_request('ModifyVolume', params)
|
return self._issue_api_request('ModifyVolume', params)
|
||||||
|
|
||||||
def _do_volume_create(self, sf_account, params):
|
def _do_volume_create(self, sf_account, params, endpoint=None):
|
||||||
|
params['accountID'] = sf_account['accountID']
|
||||||
sf_volid = self._issue_api_request(
|
sf_volid = self._issue_api_request(
|
||||||
'CreateVolume', params)['result']['volumeID']
|
'CreateVolume', params, endpoint=endpoint)['result']['volumeID']
|
||||||
return self._get_model_info(sf_account, sf_volid)
|
return self._get_model_info(sf_account, sf_volid, endpoint=endpoint)
|
||||||
|
|
||||||
def _do_snapshot_create(self, params):
|
def _do_snapshot_create(self, params):
|
||||||
model_update = {}
|
model_update = {}
|
||||||
@ -864,7 +879,6 @@ class SolidFireDriver(san.SanISCSIDriver):
|
|||||||
# Check availability for creates
|
# Check availability for creates
|
||||||
sf_account = self._get_account_create_availability(sf_accounts)
|
sf_account = self._get_account_create_availability(sf_accounts)
|
||||||
if not sf_account:
|
if not sf_account:
|
||||||
# TODO(jdg): We're not doing tertiaries, so fail.
|
|
||||||
msg = _('Volumes/account exceeded on both primary and '
|
msg = _('Volumes/account exceeded on both primary and '
|
||||||
'secondary SolidFire accounts.')
|
'secondary SolidFire accounts.')
|
||||||
raise exception.SolidFireDriverException(msg)
|
raise exception.SolidFireDriverException(msg)
|
||||||
@ -1139,7 +1153,92 @@ class SolidFireDriver(san.SanISCSIDriver):
|
|||||||
params['name'] = vname
|
params['name'] = vname
|
||||||
params['attributes']['migration_uuid'] = volume['id']
|
params['attributes']['migration_uuid'] = volume['id']
|
||||||
params['attributes']['uuid'] = v
|
params['attributes']['uuid'] = v
|
||||||
return self._do_volume_create(sf_account, params)
|
model_update = self._do_volume_create(sf_account, params)
|
||||||
|
rep_settings = self._retrieve_replication_settings(volume)
|
||||||
|
if self.replication_enabled and rep_settings:
|
||||||
|
volume['volumeID'] = int(model_update['provider_id'].split()[0])
|
||||||
|
self._replicate_volume(volume, params,
|
||||||
|
sf_account, rep_settings)
|
||||||
|
return model_update
|
||||||
|
|
||||||
|
def _retrieve_replication_settings(self, volume):
|
||||||
|
rep_data = {}
|
||||||
|
ctxt = context.get_admin_context()
|
||||||
|
type_id = volume.get('volume_type_id', None)
|
||||||
|
if type_id is not None:
|
||||||
|
rep_data = self._set_rep_by_volume_type(ctxt, type_id)
|
||||||
|
return rep_data
|
||||||
|
|
||||||
|
def _set_rep_by_volume_type(self, ctxt, type_id):
|
||||||
|
rep_opts = {}
|
||||||
|
type_ref = volume_types.get_volume_type(ctxt, type_id)
|
||||||
|
specs = type_ref.get('extra_specs')
|
||||||
|
|
||||||
|
if specs.get('replication', 'disabled').lower() == 'enabled':
|
||||||
|
rep_opts['targets'] = specs.get(
|
||||||
|
'solidfire:replication_targets', self.cluster_pairs[0])
|
||||||
|
return rep_opts
|
||||||
|
|
||||||
|
def _replicate_volume(self, volume, src_params,
|
||||||
|
parent_sfaccount, rep_info):
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
# TODO(jdg): Right now we just go to first pair,
|
||||||
|
# need to add parsing of rep_info eventually
|
||||||
|
# in other words "rep_info" is not used yet!
|
||||||
|
tgt_endpoint = self.cluster_pairs[0]['endpoint']
|
||||||
|
LOG.debug("Replicating volume on remote cluster: %s", tgt_endpoint)
|
||||||
|
params['attributes'] = src_params['attributes']
|
||||||
|
params['username'] = self._get_sf_account_name(volume['project_id'])
|
||||||
|
try:
|
||||||
|
params['initiatorSecret'] = parent_sfaccount['initiatorSecret']
|
||||||
|
params['targetSecret'] = parent_sfaccount['targetSecret']
|
||||||
|
self._issue_api_request(
|
||||||
|
'AddAccount',
|
||||||
|
params,
|
||||||
|
endpoint=tgt_endpoint)['result']['accountID']
|
||||||
|
except exception.SolidFireAPIException as ex:
|
||||||
|
if 'xDuplicateUsername' not in ex.msg:
|
||||||
|
raise
|
||||||
|
|
||||||
|
remote_account = (
|
||||||
|
self._get_sfaccount_by_name(params['username'],
|
||||||
|
endpoint=tgt_endpoint))
|
||||||
|
|
||||||
|
# Create the volume on the remote cluster w/same params as original
|
||||||
|
params = src_params
|
||||||
|
params['accountID'] = remote_account['accountID']
|
||||||
|
LOG.debug("Create remote volume on: %(endpoint)s with account: "
|
||||||
|
"%(account)s",
|
||||||
|
{'endpoint': tgt_endpoint['url'], 'account': remote_account})
|
||||||
|
model_update = self._do_volume_create(
|
||||||
|
remote_account, params, endpoint=tgt_endpoint)
|
||||||
|
|
||||||
|
tgt_sfid = int(model_update['provider_id'].split()[0])
|
||||||
|
params = {'volumeID': tgt_sfid, 'access': 'replicationTarget'}
|
||||||
|
self._issue_api_request('ModifyVolume',
|
||||||
|
params,
|
||||||
|
'8.0',
|
||||||
|
endpoint=tgt_endpoint)
|
||||||
|
|
||||||
|
# Enable volume pairing
|
||||||
|
LOG.debug("Start volume pairing on volume ID: %s",
|
||||||
|
volume['volumeID'])
|
||||||
|
params = {'volumeID': volume['volumeID']}
|
||||||
|
rep_key = self._issue_api_request('StartVolumePairing',
|
||||||
|
params,
|
||||||
|
'8.0')['result']['volumePairingKey']
|
||||||
|
params = {'volumeID': tgt_sfid,
|
||||||
|
'volumePairingKey': rep_key}
|
||||||
|
LOG.debug("Issue CompleteVolumePairing request on remote: "
|
||||||
|
"%(endpoint)s, %(parameters)s",
|
||||||
|
{'endpoint': tgt_endpoint['url'], 'parameters': params})
|
||||||
|
self._issue_api_request('CompleteVolumePairing',
|
||||||
|
params,
|
||||||
|
'8.0',
|
||||||
|
endpoint=tgt_endpoint)
|
||||||
|
LOG.debug("Completed volume pairing.")
|
||||||
|
return model_update
|
||||||
|
|
||||||
def create_cloned_volume(self, volume, src_vref):
|
def create_cloned_volume(self, volume, src_vref):
|
||||||
"""Create a clone of an existing volume."""
|
"""Create a clone of an existing volume."""
|
||||||
@ -1174,6 +1273,19 @@ class SolidFireDriver(san.SanISCSIDriver):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if sf_vol is not None:
|
if sf_vol is not None:
|
||||||
|
for vp in sf_vol.get('volumePairs', []):
|
||||||
|
LOG.debug("Deleting paired volume on remote cluster...")
|
||||||
|
pair_id = vp['clusterPairID']
|
||||||
|
for cluster in self.cluster_pairs:
|
||||||
|
if cluster['clusterPairID'] == pair_id:
|
||||||
|
params = {'volumeID': vp['remoteVolumeID']}
|
||||||
|
LOG.debug("Issue Delete request on cluster: "
|
||||||
|
"%(remote)s with params: %(parameters)s",
|
||||||
|
{'remote': cluster['endpoint']['url'],
|
||||||
|
'parameters': params})
|
||||||
|
self._issue_api_request('DeleteVolume', params,
|
||||||
|
endpoint=cluster['endpoint'])
|
||||||
|
|
||||||
params = {'volumeID': sf_vol['volumeID']}
|
params = {'volumeID': sf_vol['volumeID']}
|
||||||
self._issue_api_request('DeleteVolume', params)
|
self._issue_api_request('DeleteVolume', params)
|
||||||
if volume.get('multiattach'):
|
if volume.get('multiattach'):
|
||||||
@ -1319,6 +1431,24 @@ class SolidFireDriver(san.SanISCSIDriver):
|
|||||||
model['status'] = 'available'
|
model['status'] = 'available'
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
def _map_sf_volumes(self, cinder_volumes, endpoint=None):
|
||||||
|
"""Get a list of SolidFire volumes.
|
||||||
|
|
||||||
|
Creates a list of SolidFire volumes based
|
||||||
|
on matching a list of cinder volume ID's,
|
||||||
|
also adds an 'cinder_id' key to match cinder.
|
||||||
|
"""
|
||||||
|
vols = self._issue_api_request(
|
||||||
|
'ListActiveVolumes', {},
|
||||||
|
endpoint=endpoint)['result']['volumes']
|
||||||
|
vlist = (
|
||||||
|
[sfvol for sfvol in vols for cv in cinder_volumes if cv['id'] in
|
||||||
|
sfvol['name']])
|
||||||
|
for v in vlist:
|
||||||
|
v['cinder_id'] = v['name'].split(
|
||||||
|
self.configuration.sf_volume_prefix)[1]
|
||||||
|
return vlist
|
||||||
|
|
||||||
# Required consistency group functions
|
# Required consistency group functions
|
||||||
def create_consistencygroup(self, ctxt, group):
|
def create_consistencygroup(self, ctxt, group):
|
||||||
# SolidFire does not have a viable means for storing consistency group
|
# SolidFire does not have a viable means for storing consistency group
|
||||||
@ -1452,9 +1582,12 @@ class SolidFireDriver(san.SanISCSIDriver):
|
|||||||
data["driver_version"] = self.VERSION
|
data["driver_version"] = self.VERSION
|
||||||
data["storage_protocol"] = 'iSCSI'
|
data["storage_protocol"] = 'iSCSI'
|
||||||
data['consistencygroup_support'] = True
|
data['consistencygroup_support'] = True
|
||||||
|
# TODO(jdg): should we have a "replication_status" that includes
|
||||||
|
# enabled, disabled, failed-over, error ?
|
||||||
data['replication_enabled'] = self.replication_enabled
|
data['replication_enabled'] = self.replication_enabled
|
||||||
if self.replication_enabled:
|
if self.replication_enabled:
|
||||||
data['replication'] = 'enabled'
|
data['replication'] = 'enabled'
|
||||||
|
data['active_cluster_mvip'] = self.active_cluster_info['mvip']
|
||||||
|
|
||||||
data['total_capacity_gb'] = (
|
data['total_capacity_gb'] = (
|
||||||
float(results['maxProvisionedSpace'] / units.Gi))
|
float(results['maxProvisionedSpace'] / units.Gi))
|
||||||
@ -1676,6 +1809,89 @@ class SolidFireDriver(san.SanISCSIDriver):
|
|||||||
self._issue_api_request('ModifyVolume',
|
self._issue_api_request('ModifyVolume',
|
||||||
params, version='5.0')
|
params, version='5.0')
|
||||||
|
|
||||||
|
def _failover_volume(self, remote_vol, remote):
|
||||||
|
"""Modify remote volume to R/W mode."""
|
||||||
|
self._issue_api_request(
|
||||||
|
'RemoveVolumePair',
|
||||||
|
{'volumeID': remote_vol['volumeID']},
|
||||||
|
endpoint=remote['endpoint'], version='7.0')
|
||||||
|
|
||||||
|
params = {'volumeID': remote_vol['volumeID'],
|
||||||
|
'access': 'readWrite'}
|
||||||
|
self._issue_api_request('ModifyVolume', params,
|
||||||
|
endpoint=remote['endpoint'])
|
||||||
|
|
||||||
|
def failover_host(self, context, volumes, secondary_id=None):
|
||||||
|
"""Failover to replication target."""
|
||||||
|
volume_updates = []
|
||||||
|
remote = None
|
||||||
|
|
||||||
|
if secondary_id:
|
||||||
|
for rc in self.cluster_pairs:
|
||||||
|
if rc['mvip'] == secondary_id:
|
||||||
|
remote = rc
|
||||||
|
break
|
||||||
|
if not remote:
|
||||||
|
LOG.error(_LE("SolidFire driver received failover_host "
|
||||||
|
"but was unable to find specified replication "
|
||||||
|
"pair with id: %s."), secondary_id)
|
||||||
|
raise exception.InvalidReplicationTarget
|
||||||
|
else:
|
||||||
|
remote = self.cluster_pairs[0]
|
||||||
|
|
||||||
|
if not remote or not self.replication_enabled:
|
||||||
|
LOG.error(_LE("SolidFire driver received failover_host "
|
||||||
|
"request, however replication is NOT "
|
||||||
|
"enabled, or there are no available "
|
||||||
|
"targets to fail-over to."))
|
||||||
|
raise exception.UnableToFailOver
|
||||||
|
|
||||||
|
remote_vols = self._map_sf_volumes(volumes,
|
||||||
|
endpoint=remote['endpoint'])
|
||||||
|
primary_vols = self._map_sf_volumes(volumes)
|
||||||
|
for v in volumes:
|
||||||
|
remote_vlist = filter(lambda sfv: sfv['cinder_id'] == v['id'],
|
||||||
|
remote_vols)
|
||||||
|
if len(remote_vlist) > 0:
|
||||||
|
remote_vol = remote_vlist[0]
|
||||||
|
self._failover_volume(remote_vol, remote)
|
||||||
|
primary_vol = filter(lambda sfv: sfv['cinder_id'] == v['id'],
|
||||||
|
primary_vols)[0]
|
||||||
|
if len(primary_vol['volumePairs']) > 0:
|
||||||
|
self._issue_api_request(
|
||||||
|
'RemoveVolumePair',
|
||||||
|
{'volumeID': primary_vol['volumeID']},
|
||||||
|
version='7.0')
|
||||||
|
iqn = remote_vol['iqn']
|
||||||
|
volume_updates.append(
|
||||||
|
{'volume_id': v['id'],
|
||||||
|
'updates': {
|
||||||
|
'provider_location': ('%s %s %s' %
|
||||||
|
(remote['endpoint']['svip'],
|
||||||
|
iqn,
|
||||||
|
0)),
|
||||||
|
'replication_status': 'failed-over'}})
|
||||||
|
else:
|
||||||
|
volume_updates.append({'volume_id': v['id'],
|
||||||
|
'updates': {'status': 'error', }})
|
||||||
|
|
||||||
|
# FIXME(jdg): This introduces a problem for us, up until now our driver
|
||||||
|
# has been pretty much stateless and has allowed customers to run
|
||||||
|
# active/active HA c-vol services with SolidFire. The introduction of
|
||||||
|
# the active_cluster and failed_over attributes is going to break that
|
||||||
|
# but for now that's going to be the trade off of using replciation
|
||||||
|
self.active_cluster_info = remote
|
||||||
|
self.failed_over = True
|
||||||
|
return remote['mvip'], volume_updates
|
||||||
|
|
||||||
|
def freeze_backend(self, context):
|
||||||
|
"""Freeze backend notification."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def thaw_backend(self, context):
|
||||||
|
"""Thaw backend notification."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SolidFireISCSI(iscsi_driver.SanISCSITarget):
|
class SolidFireISCSI(iscsi_driver.SanISCSITarget):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -1725,7 +1941,7 @@ class SolidFireISCSI(iscsi_driver.SanISCSITarget):
|
|||||||
if self.configuration.sf_enable_vag:
|
if self.configuration.sf_enable_vag:
|
||||||
iqn = connector['initiator']
|
iqn = connector['initiator']
|
||||||
provider_id = volume['provider_id']
|
provider_id = volume['provider_id']
|
||||||
vol_id = int(self._parse_provider_id_string(provider_id)[0])
|
vol_id = int(provider_id.split()[0])
|
||||||
|
|
||||||
# safe_create_vag may opt to reuse vs create a vag, so we need to
|
# safe_create_vag may opt to reuse vs create a vag, so we need to
|
||||||
# add our vol_id.
|
# add our vol_id.
|
||||||
@ -1746,7 +1962,7 @@ class SolidFireISCSI(iscsi_driver.SanISCSITarget):
|
|||||||
iqn = properties['initiator']
|
iqn = properties['initiator']
|
||||||
vag = self._get_vags_by_name(iqn)
|
vag = self._get_vags_by_name(iqn)
|
||||||
provider_id = volume['provider_id']
|
provider_id = volume['provider_id']
|
||||||
vol_id = int(self._parse_provider_id_string(provider_id)[0])
|
vol_id = int(provider_id.split()[0])
|
||||||
|
|
||||||
if vag and not volume['multiattach']:
|
if vag and not volume['multiattach']:
|
||||||
# Multiattach causes problems with removing volumes from VAGs.
|
# Multiattach causes problems with removing volumes from VAGs.
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Added v2.1 replication support to SolidFire driver.
|
Loading…
Reference in New Issue
Block a user