+++ /dev/null
-# Copyright (c) 2012 Zadara Storage, Inc.
-# Copyright (c) 2012 OpenStack Foundation
-# 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.
-"""
-Tests for Zadara VPSA volume driver
-"""
-
-import copy
-import httplib
-
-from oslo_log import log as logging
-
-from cinder import exception
-from cinder import test
-from cinder.volume import configuration as conf
-from cinder.volume.drivers import zadara
-
-LOG = logging.getLogger("cinder.volume.driver")
-
-DEFAULT_RUNTIME_VARS = {
- 'status': 200,
- 'user': 'test',
- 'password': 'test_password',
- 'access_key': '0123456789ABCDEF',
- 'volumes': [],
- 'servers': [],
- 'controllers': [('active_ctrl', {'display-name': 'test_ctrl'})],
- 'counter': 1000,
-
- 'login': """
- <hash>
- <user>
- <updated-at type="datetime">2012-04-30...</updated-at>
- <access-key>%s</access-key>
- <id type="integer">1</id>
- <created-at type="datetime">2012-02-21...</created-at>
- <email>jsmith@example.com</email>
- <username>jsmith</username>
- </user>
- <status type="integer">0</status>
- </hash>""",
-
- 'good': """
- <hash>
- <status type="integer">0</status>
- </hash>""",
-
- 'bad_login': """
- <hash>
- <status type="integer">5</status>
- <status-msg>Some message...</status-msg>
- </hash>""",
-
- 'bad_volume': """
- <hash>
- <status type="integer">10081</status>
- <status-msg>Virtual volume xxx not found</status-msg>
- </hash>""",
-
- 'bad_server': """
- <hash>
- <status type="integer">10086</status>
- <status-msg>Server xxx not found</status-msg>
- </hash>""",
-
- 'server_created': """
- <create-server-response>
- <server-name>%s</server-name>
- <status type='integer'>0</status>
- </create-server-response>""",
-}
-
-RUNTIME_VARS = None
-
-
-class FakeRequest(object):
- def __init__(self, method, url, body):
- self.method = method
- self.url = url
- self.body = body
- self.status = RUNTIME_VARS['status']
-
- def read(self):
- ops = {'POST': [('/api/users/login.xml', self._login),
- ('/api/volumes.xml', self._create_volume),
- ('/api/servers.xml', self._create_server),
- ('/api/servers/*/volumes.xml', self._attach),
- ('/api/volumes/*/detach.xml', self._detach),
- ('/api/volumes/*/expand.xml', self._expand),
- ('/api/consistency_groups/*/snapshots.xml',
- self._create_snapshot),
- ('/api/consistency_groups/*/clone.xml',
- self._create_clone)],
- 'DELETE': [('/api/volumes/*', self._delete),
- ('/api/snapshots/*', self._delete_snapshot)],
- 'GET': [('/api/volumes.xml', self._list_volumes),
- ('/api/pools.xml', self._list_pools),
- ('/api/vcontrollers.xml', self._list_controllers),
- ('/api/servers.xml', self._list_servers),
- ('/api/consistency_groups/*/snapshots.xml',
- self._list_vol_snapshots),
- ('/api/volumes/*/servers.xml',
- self._list_vol_attachments)]
- }
-
- ops_list = ops[self.method]
- modified_url = self.url.split('?')[0]
- for (templ_url, func) in ops_list:
- if self._compare_url(modified_url, templ_url):
- result = func()
- return result
-
- def _compare_url(self, url, template_url):
- items = url.split('/')
- titems = template_url.split('/')
- for (i, titem) in enumerate(titems):
- if titem != '*' and titem != items[i]:
- return False
- return True
-
- def _get_parameters(self, data):
- items = data.split('&')
- params = {}
- for item in items:
- if item:
- (k, v) = item.split('=')
- params[k] = v
- return params
-
- def _get_counter(self):
- cnt = RUNTIME_VARS['counter']
- RUNTIME_VARS['counter'] += 1
- return cnt
-
- def _login(self):
- params = self._get_parameters(self.body)
- if (params['user'] == RUNTIME_VARS['user'] and
- params['password'] == RUNTIME_VARS['password']):
- return RUNTIME_VARS['login'] % RUNTIME_VARS['access_key']
- else:
- return RUNTIME_VARS['bad_login']
-
- def _incorrect_access_key(self, params):
- if params['access_key'] != RUNTIME_VARS['access_key']:
- return True
- else:
- return False
-
- def _create_volume(self):
- params = self._get_parameters(self.body)
- if self._incorrect_access_key(params):
- return RUNTIME_VARS['bad_login']
-
- params['display-name'] = params['name']
- params['cg-name'] = params['name']
- params['snapshots'] = []
- params['attachments'] = []
- vpsa_vol = 'volume-%07d' % self._get_counter()
- RUNTIME_VARS['volumes'].append((vpsa_vol, params))
- return RUNTIME_VARS['good']
-
- def _create_server(self):
- params = self._get_parameters(self.body)
- if self._incorrect_access_key(params):
- return RUNTIME_VARS['bad_login']
-
- params['display-name'] = params['display_name']
- vpsa_srv = 'srv-%07d' % self._get_counter()
- RUNTIME_VARS['servers'].append((vpsa_srv, params))
- return RUNTIME_VARS['server_created'] % vpsa_srv
-
- def _attach(self):
- params = self._get_parameters(self.body)
- if self._incorrect_access_key(params):
- return RUNTIME_VARS['bad_login']
-
- srv = self.url.split('/')[3]
- vol = params['volume_name[]']
-
- for (vol_name, params) in RUNTIME_VARS['volumes']:
- if vol_name == vol:
- attachments = params['attachments']
- if srv in attachments:
- # already attached - ok
- return RUNTIME_VARS['good']
- else:
- attachments.append(srv)
- return RUNTIME_VARS['good']
-
- return RUNTIME_VARS['bad_volume']
-
- def _detach(self):
- params = self._get_parameters(self.body)
- if self._incorrect_access_key(params):
- return RUNTIME_VARS['bad_login']
-
- vol = self.url.split('/')[3]
- srv = params['server_name[]']
-
- for (vol_name, params) in RUNTIME_VARS['volumes']:
- if vol_name == vol:
- attachments = params['attachments']
- if srv not in attachments:
- return RUNTIME_VARS['bad_server']
- else:
- attachments.remove(srv)
- return RUNTIME_VARS['good']
-
- return RUNTIME_VARS['bad_volume']
-
- def _expand(self):
- params = self._get_parameters(self.body)
- if self._incorrect_access_key(params):
- return RUNTIME_VARS['bad_login']
-
- vol = self.url.split('/')[3]
- capacity = params['capacity']
-
- for (vol_name, params) in RUNTIME_VARS['volumes']:
- if vol_name == vol:
- params['capacity'] = capacity
- return RUNTIME_VARS['good']
-
- return RUNTIME_VARS['bad_volume']
-
- def _create_snapshot(self):
- params = self._get_parameters(self.body)
- if self._incorrect_access_key(params):
- return RUNTIME_VARS['bad_login']
-
- cg_name = self.url.split('/')[3]
- snap_name = params['display_name']
-
- for (_vol_name, params) in RUNTIME_VARS['volumes']:
- if params['cg-name'] == cg_name:
- snapshots = params['snapshots']
- if snap_name in snapshots:
- # already attached
- return RUNTIME_VARS['bad_volume']
- else:
- snapshots.append(snap_name)
- return RUNTIME_VARS['good']
-
- return RUNTIME_VARS['bad_volume']
-
- def _delete_snapshot(self):
- snap = self.url.split('/')[3].split('.')[0]
-
- for (_vol_name, params) in RUNTIME_VARS['volumes']:
- if snap in params['snapshots']:
- params['snapshots'].remove(snap)
- return RUNTIME_VARS['good']
-
- return RUNTIME_VARS['bad_volume']
-
- def _create_clone(self):
- params = self._get_parameters(self.body)
- if self._incorrect_access_key(params):
- return RUNTIME_VARS['bad_login']
-
- params['display-name'] = params['name']
- params['cg-name'] = params['name']
- params['capacity'] = 1
- params['snapshots'] = []
- params['attachments'] = []
- vpsa_vol = 'volume-%07d' % self._get_counter()
- RUNTIME_VARS['volumes'].append((vpsa_vol, params))
- return RUNTIME_VARS['good']
-
- def _delete(self):
- vol = self.url.split('/')[3].split('.')[0]
-
- for (vol_name, params) in RUNTIME_VARS['volumes']:
- if vol_name == vol:
- if params['attachments']:
- # there are attachments - should be volume busy error
- return RUNTIME_VARS['bad_volume']
- else:
- RUNTIME_VARS['volumes'].remove((vol_name, params))
- return RUNTIME_VARS['good']
-
- return RUNTIME_VARS['bad_volume']
-
- def _generate_list_resp(self, header, footer, body, lst, vol):
- resp = header
- for (obj, params) in lst:
- if vol:
- resp += body % (obj,
- params['display-name'],
- params['cg-name'],
- params['capacity'])
- else:
- resp += body % (obj, params['display-name'])
- resp += footer
- return resp
-
- def _list_volumes(self):
- header = """<show-volumes-response>
- <status type='integer'>0</status>
- <volumes type='array'>"""
- footer = "</volumes></show-volumes-response>"
- body = """<volume>
- <name>%s</name>
- <display-name>%s</display-name>
- <cg-name>%s</cg-name>
- <status>Available</status>
- <virtual-capacity type='integer'>%s</virtual-capacity>
- <allocated-capacity type='integer'>1</allocated-capacity>
- <raid-group-name>r5</raid-group-name>
- <cache>write-through</cache>
- <created-at type='datetime'>2012-01-28...</created-at>
- <modified-at type='datetime'>2012-01-28...</modified-at>
- </volume>"""
- return self._generate_list_resp(header,
- footer,
- body,
- RUNTIME_VARS['volumes'],
- True)
-
- def _list_controllers(self):
- header = """<show-vcontrollers-response>
- <status type='integer'>0</status>
- <vcontrollers type='array'>"""
- footer = "</vcontrollers></show-vcontrollers-response>"
- body = """<vcontroller>
- <name>%s</name>
- <display-name>%s</display-name>
- <state>active</state>
- <target>iqn.2011-04.com.zadarastorage:vsa-xxx:1</target>
- <iscsi-ip>1.1.1.1</iscsi-ip>
- <mgmt-ip>1.1.1.1</mgmt-ip>
- <software-ver>0.0.09-05.1--77.7</software-ver>
- <heartbeat1>ok</heartbeat1>
- <heartbeat2>ok</heartbeat2>
- <chap-username>test_chap_user</chap-username>
- <chap-target-secret>test_chap_secret</chap-target-secret>
- </vcontroller>"""
- return self._generate_list_resp(header,
- footer,
- body,
- RUNTIME_VARS['controllers'],
- False)
-
- def _list_pools(self):
- header = """<show-pools-response>
- <status type="integer">0</status>
- <pools type="array">
- """
- footer = "</pools></show-pools-response>"
- return header + footer
-
- def _list_servers(self):
- header = """<show-servers-response>
- <status type='integer'>0</status>
- <servers type='array'>"""
- footer = "</servers></show-servers-response>"
- body = """<server>
- <name>%s</name>
- <display-name>%s</display-name>
- <iqn>%s</iqn>
- <status>Active</status>
- <created-at type='datetime'>2012-01-28...</created-at>
- <modified-at type='datetime'>2012-01-28...</modified-at>
- </server>"""
-
- resp = header
- for (obj, params) in RUNTIME_VARS['servers']:
- resp += body % (obj, params['display-name'], params['iqn'])
- resp += footer
- return resp
-
- def _get_server_obj(self, name):
- for (srv_name, params) in RUNTIME_VARS['servers']:
- if srv_name == name:
- return params
-
- def _list_vol_attachments(self):
- vol = self.url.split('/')[3]
-
- header = """<show-servers-response>
- <status type="integer">0</status>
- <servers type="array">"""
- footer = "</servers></show-servers-response>"
- body = """<server>
- <name>%s</name>
- <display-name>%s</display-name>
- <iqn>%s</iqn>
- <target>iqn.2011-04.com.zadarastorage:vsa-xxx:1</target>
- <lun>0</lun>
- </server>"""
-
- for (vol_name, params) in RUNTIME_VARS['volumes']:
- if vol_name == vol:
- attachments = params['attachments']
- resp = header
- for server in attachments:
- srv_params = self._get_server_obj(server)
- resp += body % (server,
- srv_params['display-name'],
- srv_params['iqn'])
- resp += footer
- return resp
-
- return RUNTIME_VARS['bad_volume']
-
- def _list_vol_snapshots(self):
- cg_name = self.url.split('/')[3]
-
- header = """<show-snapshots-on-cg-response>
- <status type="integer">0</status>
- <snapshots type="array">"""
- footer = "</snapshots></show-snapshots-on-cg-response>"
-
- body = """<snapshot>
- <name>%s</name>
- <display-name>%s</display-name>
- <status>normal</status>
- <cg-name>%s</cg-name>
- <pool-name>pool-00000001</pool-name>
- </snapshot>"""
-
- for (_vol_name, params) in RUNTIME_VARS['volumes']:
- if params['cg-name'] == cg_name:
- snapshots = params['snapshots']
- resp = header
- for snap in snapshots:
- resp += body % (snap, snap, cg_name)
- resp += footer
- return resp
-
- return RUNTIME_VARS['bad_volume']
-
-
-class FakeHTTPConnection(object):
- """A fake httplib.HTTPConnection for zadara volume driver tests."""
- def __init__(self, host, port, use_ssl=False):
- LOG.debug('Enter: __init__ FakeHTTPConnection')
- self.host = host
- self.port = port
- self.use_ssl = use_ssl
- self.req = None
-
- def request(self, method, url, body):
- LOG.debug('Enter: request')
- self.req = FakeRequest(method, url, body)
-
- def getresponse(self):
- LOG.debug('Enter: getresponse')
- return self.req
-
- def close(self):
- LOG.debug('Enter: close')
- self.req = None
-
-
-class FakeHTTPSConnection(FakeHTTPConnection):
- def __init__(self, host, port):
- LOG.debug('Enter: __init__ FakeHTTPSConnection')
- super(FakeHTTPSConnection, self).__init__(host, port, use_ssl=True)
-
-
-class ZadaraVPSADriverTestCase(test.TestCase):
- """Test case for Zadara VPSA volume driver."""
-
- def setUp(self):
- LOG.debug('Enter: setUp')
- super(ZadaraVPSADriverTestCase, self).setUp()
-
- global RUNTIME_VARS
- RUNTIME_VARS = copy.deepcopy(DEFAULT_RUNTIME_VARS)
-
- self.configuration = conf.Configuration(None)
- self.configuration.append_config_values(zadara.zadara_opts)
- self.configuration.reserved_percentage = 10
- self.configuration.zadara_user = 'test'
- self.configuration.zadara_password = 'test_password'
- self.configuration.zadara_vpsa_poolname = 'pool-0001'
-
- self.driver = zadara.ZadaraVPSAISCSIDriver(
- configuration=self.configuration)
- self.stubs.Set(httplib, 'HTTPConnection', FakeHTTPConnection)
- self.stubs.Set(httplib, 'HTTPSConnection', FakeHTTPSConnection)
- self.driver.do_setup(None)
-
- def test_create_destroy(self):
- """Create/Delete volume."""
- volume = {'name': 'test_volume_01', 'size': 1}
- self.driver.create_volume(volume)
- self.driver.delete_volume(volume)
-
- def test_create_destroy_multiple(self):
- """Create/Delete multiple volumes."""
- self.flags(zadara_vpsa_allow_nonexistent_delete=False)
- self.driver.create_volume({'name': 'test_volume_01', 'size': 1})
- self.driver.create_volume({'name': 'test_volume_02', 'size': 2})
- self.driver.create_volume({'name': 'test_volume_03', 'size': 3})
- self.driver.delete_volume({'name': 'test_volume_02'})
- self.driver.delete_volume({'name': 'test_volume_03'})
- self.driver.delete_volume({'name': 'test_volume_01'})
-
- self.assertRaises(exception.VolumeNotFound,
- self.driver.delete_volume,
- {'name': 'test_volume_04'})
- self.flags(zadara_vpsa_allow_nonexistent_delete=True)
- self.driver.delete_volume({'name': 'test_volume_04'})
-
- def test_destroy_non_existent(self):
- """Delete non-existent volume."""
- self.flags(zadara_vpsa_allow_nonexistent_delete=False)
- volume = {'name': 'test_volume_02', 'size': 1}
- self.assertRaises(exception.VolumeNotFound,
- self.driver.delete_volume,
- volume)
- self.flags(zadara_vpsa_allow_nonexistent_delete=True)
-
- def test_empty_apis(self):
- """Test empty func (for coverage only)."""
- context = None
- volume = {'name': 'test_volume_01', 'size': 1}
- self.driver.create_export(context, volume)
- self.driver.ensure_export(context, volume)
- self.driver.remove_export(context, volume)
-
- self.assertRaises(NotImplementedError,
- self.driver.local_path,
- None)
-
- self.driver.check_for_setup_error()
-
- def test_volume_attach_detach(self):
- """Test volume attachment and detach."""
- volume = {'name': 'test_volume_01', 'size': 1, 'id': 123}
- connector = dict(initiator='test_iqn.1')
-
- self.driver.create_volume(volume)
-
- props = self.driver.initialize_connection(volume, connector)
- self.assertEqual(props['driver_volume_type'], 'iscsi')
- data = props['data']
- self.assertEqual(data['target_portal'], '1.1.1.1:3260')
- self.assertEqual(data['target_iqn'],
- 'iqn.2011-04.com.zadarastorage:vsa-xxx:1')
- self.assertEqual(data['target_lun'], '0')
- self.assertEqual(data['volume_id'], 123)
- self.assertEqual(data['auth_method'], 'CHAP')
- self.assertEqual(data['auth_username'], 'test_chap_user')
- self.assertEqual(data['auth_password'], 'test_chap_secret')
-
- self.driver.terminate_connection(volume, connector)
- self.driver.delete_volume(volume)
-
- def test_volume_attach_multiple_detach(self):
- """Test multiple volume attachment and detach."""
- volume = {'name': 'test_volume_01', 'size': 1, 'id': 123}
- connector1 = dict(initiator='test_iqn.1')
- connector2 = dict(initiator='test_iqn.2')
- connector3 = dict(initiator='test_iqn.3')
-
- self.driver.create_volume(volume)
- self.driver.initialize_connection(volume, connector1)
- self.driver.initialize_connection(volume, connector2)
- self.driver.initialize_connection(volume, connector3)
-
- self.driver.terminate_connection(volume, connector1)
- self.driver.terminate_connection(volume, connector3)
- self.driver.terminate_connection(volume, connector2)
- self.driver.delete_volume(volume)
-
- def test_wrong_attach_params(self):
- """Test different wrong attach scenarios."""
- volume1 = {'name': 'test_volume_01', 'size': 1, 'id': 101}
- connector1 = dict(initiator='test_iqn.1')
-
- self.assertRaises(exception.VolumeNotFound,
- self.driver.initialize_connection,
- volume1, connector1)
-
- def test_wrong_detach_params(self):
- """Test different wrong detachment scenarios."""
-
- volume1 = {'name': 'test_volume_01', 'size': 1, 'id': 101}
- volume2 = {'name': 'test_volume_02', 'size': 1, 'id': 102}
- volume3 = {'name': 'test_volume_03', 'size': 1, 'id': 103}
- connector1 = dict(initiator='test_iqn.1')
- connector2 = dict(initiator='test_iqn.2')
- connector3 = dict(initiator='test_iqn.3')
-
- self.driver.create_volume(volume1)
- self.driver.create_volume(volume2)
- self.driver.initialize_connection(volume1, connector1)
- self.driver.initialize_connection(volume2, connector2)
-
- self.assertRaises(exception.ZadaraServerNotFound,
- self.driver.terminate_connection,
- volume1, connector3)
- self.assertRaises(exception.VolumeNotFound,
- self.driver.terminate_connection,
- volume3, connector1)
- self.assertRaises(exception.FailedCmdWithDump,
- self.driver.terminate_connection,
- volume1, connector2)
-
- def test_wrong_login_reply(self):
- """Test wrong login reply."""
-
- RUNTIME_VARS['login'] = """<hash>
- <access-key>%s</access-key>
- <status type="integer">0</status>
- </hash>"""
- self.assertRaises(exception.MalformedResponse,
- self.driver.do_setup, None)
-
- RUNTIME_VARS['login'] = """
- <hash>
- <user>
- <updated-at type="datetime">2012-04-30...</updated-at>
- <id type="integer">1</id>
- <created-at type="datetime">2012-02-21...</created-at>
- <email>jsmith@example.com</email>
- <username>jsmith</username>
- </user>
- <access-key>%s</access-key>
- <status type="integer">0</status>
- </hash>"""
- self.assertRaises(exception.MalformedResponse,
- self.driver.do_setup, None)
-
- def test_ssl_use(self):
- """Coverage test for SSL connection."""
- self.flags(zadara_vpsa_use_ssl=True)
- self.driver.do_setup(None)
- self.flags(zadara_vpsa_use_ssl=False)
-
- def test_bad_http_response(self):
- """Coverage test for non-good HTTP response."""
- RUNTIME_VARS['status'] = 400
-
- volume = {'name': 'test_volume_01', 'size': 1}
- self.assertRaises(exception.BadHTTPResponseStatus,
- self.driver.create_volume, volume)
-
- def test_delete_without_detach(self):
- """Test volume deletion without detach."""
-
- volume1 = {'name': 'test_volume_01', 'size': 1, 'id': 101}
- connector1 = dict(initiator='test_iqn.1')
- connector2 = dict(initiator='test_iqn.2')
- connector3 = dict(initiator='test_iqn.3')
-
- self.driver.create_volume(volume1)
- self.driver.initialize_connection(volume1, connector1)
- self.driver.initialize_connection(volume1, connector2)
- self.driver.initialize_connection(volume1, connector3)
-
- self.flags(zadara_vpsa_auto_detach_on_delete=False)
- self.assertRaises(exception.VolumeAttached,
- self.driver.delete_volume, volume1)
-
- self.flags(zadara_vpsa_auto_detach_on_delete=True)
- self.driver.delete_volume(volume1)
-
- def test_no_active_ctrl(self):
-
- RUNTIME_VARS['controllers'] = []
-
- volume = {'name': 'test_volume_01', 'size': 1, 'id': 123}
- connector = dict(initiator='test_iqn.1')
-
- self.driver.create_volume(volume)
- self.assertRaises(exception.ZadaraVPSANoActiveController,
- self.driver.initialize_connection,
- volume, connector)
-
- def test_create_destroy_snapshot(self):
- """Create/Delete snapshot test."""
- volume = {'name': 'test_volume_01', 'size': 1}
- snapshot = {'name': 'snap_01',
- 'volume_name': volume['name']}
-
- self.driver.create_volume(volume)
-
- self.assertRaises(exception.VolumeNotFound,
- self.driver.create_snapshot,
- {'name': snapshot['name'],
- 'volume_name': 'wrong_vol'})
-
- self.driver.create_snapshot(snapshot)
-
- # Deleted should succeed for missing volume
- self.driver.delete_snapshot({'name': snapshot['name'],
- 'volume_name': 'wrong_vol'})
- # Deleted should succeed for missing snap
- self.driver.delete_snapshot({'name': 'wrong_snap',
- 'volume_name': volume['name']})
-
- self.driver.delete_snapshot(snapshot)
- self.driver.delete_volume(volume)
-
- def test_expand_volume(self):
- """Expand volume test."""
- volume = {'name': 'test_volume_01', 'size': 10}
- volume2 = {'name': 'test_volume_02', 'size': 10}
-
- self.driver.create_volume(volume)
-
- self.assertRaises(exception.VolumeNotFound,
- self.driver.extend_volume,
- volume2, 15)
- self.assertRaises(exception.InvalidInput,
- self.driver.extend_volume,
- volume, 5)
-
- self.driver.extend_volume(volume, 15)
- self.driver.delete_volume(volume)
-
- def test_create_destroy_clones(self):
- """Create/Delete clones test."""
- volume1 = {'name': 'test_volume_01', 'size': 1}
- volume2 = {'name': 'test_volume_02', 'size': 1}
- volume3 = {'name': 'test_volume_03', 'size': 1}
- snapshot = {'name': 'snap_01',
- 'volume_name': volume1['name']}
-
- self.driver.create_volume(volume1)
- self.driver.create_snapshot(snapshot)
-
- # Test invalid vol reference
- self.assertRaises(exception.VolumeNotFound,
- self.driver.create_volume_from_snapshot,
- volume2,
- {'name': snapshot['name'],
- 'volume_name': 'wrong_vol'})
- # Test invalid snap reference
- self.assertRaises(exception.VolumeNotFound,
- self.driver.create_volume_from_snapshot,
- volume2,
- {'name': 'wrong_snap',
- 'volume_name': snapshot['volume_name']})
- # Test invalid src_vref for volume clone
- self.assertRaises(exception.VolumeNotFound,
- self.driver.create_cloned_volume,
- volume3, volume2)
-
- self.driver.create_volume_from_snapshot(volume2, snapshot)
- self.driver.create_cloned_volume(volume3, volume1)
-
- self.driver.delete_volume(volume3)
- self.driver.delete_volume(volume2)
- self.driver.delete_snapshot(snapshot)
- self.driver.delete_volume(volume1)
-
- def test_get_volume_stats(self):
- """Get stats test."""
-
- self.mox.StubOutWithMock(self.configuration, 'safe_get')
- self.configuration.safe_get('volume_backend_name'). \
- AndReturn('ZadaraVPSAISCSIDriver')
- self.mox.ReplayAll()
-
- data = self.driver.get_volume_stats(True)
-
- self.assertEqual(data['vendor_name'], 'Zadara Storage')
- self.assertEqual(data['total_capacity_gb'], 'infinite')
- self.assertEqual(data['free_capacity_gb'], 'infinite')
-
- self.assertEqual(data,
- {'total_capacity_gb': 'infinite',
- 'free_capacity_gb': 'infinite',
- 'reserved_percentage':
- self.configuration.reserved_percentage,
- 'QoS_support': False,
- 'vendor_name': 'Zadara Storage',
- 'driver_version': self.driver.VERSION,
- 'storage_protocol': 'iSCSI',
- 'volume_backend_name': 'ZadaraVPSAISCSIDriver',
- })
+++ /dev/null
-# Copyright (c) 2012 Zadara Storage, Inc.
-# Copyright (c) 2012 OpenStack Foundation
-# 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.
-"""
-Volume driver for Zadara Virtual Private Storage Array (VPSA).
-
-This driver requires VPSA with API ver.13.07 or higher.
-"""
-
-
-import httplib
-
-from lxml import etree
-from oslo_config import cfg
-from oslo_log import log as logging
-
-from cinder import exception
-from cinder.i18n import _, _LW
-from cinder.volume import driver
-
-LOG = logging.getLogger(__name__)
-
-zadara_opts = [
- cfg.StrOpt('zadara_vpsa_ip',
- default=None,
- help='Management IP of Zadara VPSA'),
- cfg.StrOpt('zadara_vpsa_port',
- default=None,
- help='Zadara VPSA port number'),
- cfg.BoolOpt('zadara_vpsa_use_ssl',
- default=False,
- help='Use SSL connection'),
- cfg.StrOpt('zadara_user',
- default=None,
- help='User name for the VPSA'),
- cfg.StrOpt('zadara_password',
- default=None,
- help='Password for the VPSA',
- secret=True),
-
- cfg.StrOpt('zadara_vpsa_poolname',
- default=None,
- help='Name of VPSA storage pool for volumes'),
-
- cfg.BoolOpt('zadara_vol_thin',
- default=True,
- help='Default thin provisioning policy for volumes'),
- cfg.BoolOpt('zadara_vol_encrypt',
- default=False,
- help='Default encryption policy for volumes'),
- cfg.StrOpt('zadara_vol_name_template',
- default='OS_%s',
- help='Default template for VPSA volume names'),
- cfg.BoolOpt('zadara_vpsa_auto_detach_on_delete',
- default=True,
- help="Automatically detach from servers on volume delete"),
- cfg.BoolOpt('zadara_vpsa_allow_nonexistent_delete',
- default=True,
- help="Don't halt on deletion of non-existing volumes"), ]
-
-CONF = cfg.CONF
-CONF.register_opts(zadara_opts)
-
-
-class ZadaraVPSAConnection(object):
- """Executes volume driver commands on VPSA."""
-
- def __init__(self, conf):
- self.conf = conf
- self.access_key = None
-
- self.ensure_connection()
-
- def _generate_vpsa_cmd(self, cmd, **kwargs):
- """Generate command to be sent to VPSA."""
-
- def _joined_params(params):
- param_str = []
- for k, v in params.items():
- param_str.append("%s=%s" % (k, v))
- return '&'.join(param_str)
-
- # Dictionary of applicable VPSA commands in the following format:
- # 'command': (method, API_URL, {optional parameters})
- vpsa_commands = {
- 'login': ('POST',
- '/api/users/login.xml',
- {'user': self.conf.zadara_user,
- 'password': self.conf.zadara_password}),
-
- # Volume operations
- 'create_volume': ('POST',
- '/api/volumes.xml',
- {'name': kwargs.get('name'),
- 'capacity': kwargs.get('size'),
- 'pool': self.conf.zadara_vpsa_poolname,
- 'thin': 'YES'
- if self.conf.zadara_vol_thin else 'NO',
- 'crypt': 'YES'
- if self.conf.zadara_vol_encrypt else 'NO'}),
- 'delete_volume': ('DELETE',
- '/api/volumes/%s.xml' % kwargs.get('vpsa_vol'),
- {}),
-
- 'expand_volume': ('POST',
- '/api/volumes/%s/expand.xml'
- % kwargs.get('vpsa_vol'),
- {'capacity': kwargs.get('size')}),
-
- # Snapshot operations
- 'create_snapshot': ('POST',
- '/api/consistency_groups/%s/snapshots.xml'
- % kwargs.get('cg_name'),
- {'display_name': kwargs.get('snap_name')}),
- 'delete_snapshot': ('DELETE',
- '/api/snapshots/%s.xml'
- % kwargs.get('snap_id'),
- {}),
-
- 'create_clone_from_snap': ('POST',
- '/api/consistency_groups/%s/clone.xml'
- % kwargs.get('cg_name'),
- {'name': kwargs.get('name'),
- 'snapshot': kwargs.get('snap_id')}),
-
- 'create_clone': ('POST',
- '/api/consistency_groups/%s/clone.xml'
- % kwargs.get('cg_name'),
- {'name': kwargs.get('name')}),
-
- # Server operations
- 'create_server': ('POST',
- '/api/servers.xml',
- {'display_name': kwargs.get('initiator'),
- 'iqn': kwargs.get('initiator')}),
-
- # Attach/Detach operations
- 'attach_volume': ('POST',
- '/api/servers/%s/volumes.xml'
- % kwargs.get('vpsa_srv'),
- {'volume_name[]': kwargs.get('vpsa_vol'),
- 'force': 'NO'}),
- 'detach_volume': ('POST',
- '/api/volumes/%s/detach.xml'
- % kwargs.get('vpsa_vol'),
- {'server_name[]': kwargs.get('vpsa_srv'),
- 'force': 'NO'}),
-
- # Get operations
- 'list_volumes': ('GET',
- '/api/volumes.xml',
- {}),
- 'list_pools': ('GET',
- '/api/pools.xml',
- {}),
- 'list_controllers': ('GET',
- '/api/vcontrollers.xml',
- {}),
- 'list_servers': ('GET',
- '/api/servers.xml',
- {}),
- 'list_vol_attachments': ('GET',
- '/api/volumes/%s/servers.xml'
- % kwargs.get('vpsa_vol'),
- {}),
- 'list_vol_snapshots': ('GET',
- '/api/consistency_groups/%s/snapshots.xml'
- % kwargs.get('cg_name'),
- {})}
-
- if cmd not in vpsa_commands.keys():
- raise exception.UnknownCmd(cmd=cmd)
- else:
- (method, url, params) = vpsa_commands[cmd]
-
- if method == 'GET':
- # For GET commands add parameters to the URL
- params.update(dict(access_key=self.access_key,
- page=1, start=0, limit=0))
- url += '?' + _joined_params(params)
- body = ''
-
- elif method == 'DELETE':
- # For DELETE commands add parameters to the URL
- params.update(dict(access_key=self.access_key))
- url += '?' + _joined_params(params)
- body = ''
-
- elif method == 'POST':
- if self.access_key:
- params.update(dict(access_key=self.access_key))
- body = _joined_params(params)
-
- else:
- raise exception.UnknownCmd(cmd=method)
-
- return (method, url, body)
-
- def ensure_connection(self, cmd=None):
- """Retrieve access key for VPSA connection."""
-
- if self.access_key or cmd == 'login':
- return
-
- cmd = 'login'
- xml_tree = self.send_cmd(cmd)
- user = xml_tree.find('user')
- if user is None:
- raise exception.MalformedResponse(cmd=cmd,
- reason='no "user" field')
-
- access_key = user.findtext('access-key')
- if access_key is None:
- raise exception.MalformedResponse(cmd=cmd,
- reason='no "access-key" field')
-
- self.access_key = access_key
-
- def send_cmd(self, cmd, **kwargs):
- """Send command to VPSA Controller."""
-
- self.ensure_connection(cmd)
-
- (method, url, body) = self._generate_vpsa_cmd(cmd, **kwargs)
- LOG.debug('Sending %(method)s to %(url)s. Body "%(body)s"',
- {'method': method, 'url': url, 'body': body})
-
- if self.conf.zadara_vpsa_use_ssl:
- connection = httplib.HTTPSConnection(self.conf.zadara_vpsa_ip,
- self.conf.zadara_vpsa_port)
- else:
- connection = httplib.HTTPConnection(self.conf.zadara_vpsa_ip,
- self.conf.zadara_vpsa_port)
- connection.request(method, url, body)
- response = connection.getresponse()
-
- if response.status != 200:
- connection.close()
- raise exception.BadHTTPResponseStatus(status=response.status)
- data = response.read()
- connection.close()
-
- xml_tree = etree.fromstring(data)
- status = xml_tree.findtext('status')
- if status != '0':
- raise exception.FailedCmdWithDump(status=status, data=data)
-
- if method in ['POST', 'DELETE']:
- LOG.debug('Operation completed. %(data)s', {'data': data})
- return xml_tree
-
-
-class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
- """Zadara VPSA iSCSI volume driver."""
-
- VERSION = '13.07'
-
- def __init__(self, *args, **kwargs):
- super(ZadaraVPSAISCSIDriver, self).__init__(*args, **kwargs)
- self.configuration.append_config_values(zadara_opts)
-
- def do_setup(self, context):
- """Any initialization the volume driver does while starting.
-
- Establishes initial connection with VPSA and retrieves access_key.
- """
- self.vpsa = ZadaraVPSAConnection(self.configuration)
-
- def check_for_setup_error(self):
- """Returns an error (exception) if prerequisites aren't met."""
- self.vpsa.ensure_connection()
-
- def local_path(self, volume):
- """Return local path to existing local volume."""
- raise NotImplementedError()
-
- def _xml_parse_helper(self, xml_tree, first_level, search_tuple,
- first=True):
- """Helper for parsing VPSA's XML output.
-
- Returns single item if first==True or list for multiple selection.
- If second argument in search_tuple is None - returns all items with
- appropriate key.
- """
-
- objects = xml_tree.find(first_level)
- if objects is None:
- return None
-
- result_list = []
- (key, value) = search_tuple
- for object in objects.getchildren():
- found_value = object.findtext(key)
- if found_value and (found_value == value or value is None):
- if first:
- return object
- else:
- result_list.append(object)
- return result_list if result_list else None
-
- def _get_vpsa_volume_name_and_size(self, name):
- """Return VPSA's name & size for the volume."""
- xml_tree = self.vpsa.send_cmd('list_volumes')
- volume = self._xml_parse_helper(xml_tree, 'volumes',
- ('display-name', name))
- if volume is not None:
- return (volume.findtext('name'),
- int(volume.findtext('virtual-capacity')))
-
- return (None, None)
-
- def _get_vpsa_volume_name(self, name):
- """Return VPSA's name for the volume."""
- (vol_name, _size) = self._get_vpsa_volume_name_and_size(name)
- return vol_name
-
- def _get_volume_cg_name(self, name):
- """Return name of the consistency group for the volume."""
- xml_tree = self.vpsa.send_cmd('list_volumes')
- volume = self._xml_parse_helper(xml_tree, 'volumes',
- ('display-name', name))
- if volume is not None:
- return volume.findtext('cg-name')
-
- return None
-
- def _get_snap_id(self, cg_name, snap_name):
- """Return snapshot ID for particular volume."""
- xml_tree = self.vpsa.send_cmd('list_vol_snapshots',
- cg_name=cg_name)
- snap = self._xml_parse_helper(xml_tree, 'snapshots',
- ('display-name', snap_name))
- if snap is not None:
- return snap.findtext('name')
-
- return None
-
- def _get_pool_capacity(self, pool_name):
- """Return pool's total and available capacities."""
- xml_tree = self.vpsa.send_cmd('list_pools')
- pool = self._xml_parse_helper(xml_tree, 'pools',
- ('name', pool_name))
- if pool is not None:
- total = int(pool.findtext('capacity'))
- free = int(float(pool.findtext('available-capacity')))
- LOG.debug('Pool %(name)s: %(total)sGB total, %(free)sGB free',
- {'name': pool_name, 'total': total, 'free': free})
- return (total, free)
-
- return ('infinite', 'infinite')
-
- def _get_active_controller_details(self):
- """Return details of VPSA's active controller."""
- xml_tree = self.vpsa.send_cmd('list_controllers')
- ctrl = self._xml_parse_helper(xml_tree, 'vcontrollers',
- ('state', 'active'))
- if ctrl is not None:
- return dict(target=ctrl.findtext('target'),
- ip=ctrl.findtext('iscsi-ip'),
- chap_user=ctrl.findtext('chap-username'),
- chap_passwd=ctrl.findtext('chap-target-secret'))
- return None
-
- def _get_server_name(self, initiator):
- """Return VPSA's name for server object with given IQN."""
- xml_tree = self.vpsa.send_cmd('list_servers')
- server = self._xml_parse_helper(xml_tree, 'servers',
- ('iqn', initiator))
- if server is not None:
- return server.findtext('name')
- return None
-
- def _create_vpsa_server(self, initiator):
- """Create server object within VPSA (if doesn't exist)."""
- vpsa_srv = self._get_server_name(initiator)
- if not vpsa_srv:
- xml_tree = self.vpsa.send_cmd('create_server', initiator=initiator)
- vpsa_srv = xml_tree.findtext('server-name')
- return vpsa_srv
-
- def create_volume(self, volume):
- """Create volume."""
- self.vpsa.send_cmd(
- 'create_volume',
- name=self.configuration.zadara_vol_name_template % volume['name'],
- size=volume['size'])
-
- def delete_volume(self, volume):
- """Delete volume.
-
- Return ok if doesn't exist. Auto detach from all servers.
- """
- # Get volume name
- name = self.configuration.zadara_vol_name_template % volume['name']
- vpsa_vol = self._get_vpsa_volume_name(name)
- if not vpsa_vol:
- msg = _('Volume %(name)s could not be found. '
- 'It might be already deleted') % {'name': name}
- LOG.warning(msg)
- if self.configuration.zadara_vpsa_allow_nonexistent_delete:
- return
- else:
- raise exception.VolumeNotFound(volume_id=name)
-
- # Check attachment info and detach from all
- xml_tree = self.vpsa.send_cmd('list_vol_attachments',
- vpsa_vol=vpsa_vol)
- servers = self._xml_parse_helper(xml_tree, 'servers',
- ('iqn', None), first=False)
- if servers:
- if not self.configuration.zadara_vpsa_auto_detach_on_delete:
- raise exception.VolumeAttached(volume_id=name)
-
- for server in servers:
- vpsa_srv = server.findtext('name')
- if vpsa_srv:
- self.vpsa.send_cmd('detach_volume',
- vpsa_srv=vpsa_srv,
- vpsa_vol=vpsa_vol)
-
- # Delete volume
- self.vpsa.send_cmd('delete_volume', vpsa_vol=vpsa_vol)
-
- def create_snapshot(self, snapshot):
- """Creates a snapshot."""
-
- LOG.debug('Create snapshot: %s', snapshot['name'])
-
- # Retrieve the CG name for the base volume
- volume_name = self.configuration.zadara_vol_name_template\
- % snapshot['volume_name']
- cg_name = self._get_volume_cg_name(volume_name)
- if not cg_name:
- msg = _('Volume %(name)s not found') % {'name': volume_name}
- LOG.error(msg)
- raise exception.VolumeNotFound(volume_id=volume_name)
-
- self.vpsa.send_cmd('create_snapshot',
- cg_name=cg_name,
- snap_name=snapshot['name'])
-
- def delete_snapshot(self, snapshot):
- """Deletes a snapshot."""
-
- LOG.debug('Delete snapshot: %s', snapshot['name'])
-
- # Retrieve the CG name for the base volume
- volume_name = self.configuration.zadara_vol_name_template\
- % snapshot['volume_name']
- cg_name = self._get_volume_cg_name(volume_name)
- if not cg_name:
- # If the volume isn't present, then don't attempt to delete
- LOG.warning(_LW("snapshot: original volume %s not found, "
- "skipping delete operation")
- % snapshot['volume_name'])
- return True
-
- snap_id = self._get_snap_id(cg_name, snapshot['name'])
- if not snap_id:
- # If the snapshot isn't present, then don't attempt to delete
- LOG.warning(_LW("snapshot: snapshot %s not found, "
- "skipping delete operation")
- % snapshot['name'])
- return True
-
- self.vpsa.send_cmd('delete_snapshot',
- snap_id=snap_id)
-
- def create_volume_from_snapshot(self, volume, snapshot):
- """Creates a volume from a snapshot."""
-
- LOG.debug('Creating volume from snapshot: %s' % snapshot['name'])
-
- # Retrieve the CG name for the base volume
- volume_name = self.configuration.zadara_vol_name_template\
- % snapshot['volume_name']
- cg_name = self._get_volume_cg_name(volume_name)
- if not cg_name:
- msg = _('Volume %(name)s not found') % {'name': volume_name}
- LOG.error(msg)
- raise exception.VolumeNotFound(volume_id=volume_name)
-
- snap_id = self._get_snap_id(cg_name, snapshot['name'])
- if not snap_id:
- msg = _('Snapshot %(name)s not found') % {'name': snapshot['name']}
- LOG.error(msg)
- raise exception.VolumeNotFound(volume_id=snapshot['name'])
-
- self.vpsa.send_cmd('create_clone_from_snap',
- cg_name=cg_name,
- name=self.configuration.zadara_vol_name_template
- % volume['name'],
- snap_id=snap_id)
-
- def create_cloned_volume(self, volume, src_vref):
- """Creates a clone of the specified volume."""
-
- LOG.debug('Creating clone of volume: %s' % src_vref['name'])
-
- # Retrieve the CG name for the base volume
- volume_name = self.configuration.zadara_vol_name_template\
- % src_vref['name']
- cg_name = self._get_volume_cg_name(volume_name)
- if not cg_name:
- msg = _('Volume %(name)s not found') % {'name': volume_name}
- LOG.error(msg)
- raise exception.VolumeNotFound(volume_id=volume_name)
-
- self.vpsa.send_cmd('create_clone',
- cg_name=cg_name,
- name=self.configuration.zadara_vol_name_template
- % volume['name'])
-
- def extend_volume(self, volume, new_size):
- """Extend an existing volume."""
- # Get volume name
- name = self.configuration.zadara_vol_name_template % volume['name']
- (vpsa_vol, size) = self._get_vpsa_volume_name_and_size(name)
- if not vpsa_vol:
- msg = _('Volume %(name)s could not be found. '
- 'It might be already deleted') % {'name': name}
- LOG.error(msg)
- raise exception.VolumeNotFound(volume_id=name)
-
- if new_size < size:
- raise exception.InvalidInput(
- reason='%s < current size %s' % (new_size, size))
-
- expand_size = new_size - size
- self.vpsa.send_cmd('expand_volume',
- vpsa_vol=vpsa_vol,
- size=expand_size)
-
- def create_export(self, context, volume):
- """Irrelevant for VPSA volumes. Export created during attachment."""
- pass
-
- def ensure_export(self, context, volume):
- """Irrelevant for VPSA volumes. Export created during attachment."""
- pass
-
- def remove_export(self, context, volume):
- """Irrelevant for VPSA volumes. Export removed during detach."""
- pass
-
- def initialize_connection(self, volume, connector):
- """Attach volume to initiator/host.
-
- During this call VPSA exposes volume to particular Initiator. It also
- creates a 'server' entity for Initiator (if it was not created before)
-
- All necessary connection information is returned, including auth data.
- Connection data (target, LUN) is not stored in the DB.
- """
-
- # Get/Create server name for IQN
- initiator_name = connector['initiator']
- vpsa_srv = self._create_vpsa_server(initiator_name)
- if not vpsa_srv:
- raise exception.ZadaraServerCreateFailure(name=initiator_name)
-
- # Get volume name
- name = self.configuration.zadara_vol_name_template % volume['name']
- vpsa_vol = self._get_vpsa_volume_name(name)
- if not vpsa_vol:
- raise exception.VolumeNotFound(volume_id=name)
-
- # Get Active controller details
- ctrl = self._get_active_controller_details()
- if not ctrl:
- raise exception.ZadaraVPSANoActiveController()
-
- # Attach volume to server
- self.vpsa.send_cmd('attach_volume',
- vpsa_srv=vpsa_srv,
- vpsa_vol=vpsa_vol)
-
- # Get connection info
- xml_tree = self.vpsa.send_cmd('list_vol_attachments',
- vpsa_vol=vpsa_vol)
- server = self._xml_parse_helper(xml_tree, 'servers',
- ('iqn', initiator_name))
- if server is None:
- raise exception.ZadaraAttachmentsNotFound(name=name)
- target = server.findtext('target')
- lun = server.findtext('lun')
- if target is None or lun is None:
- raise exception.ZadaraInvalidAttachmentInfo(
- name=name,
- reason='target=%s, lun=%s' % (target, lun))
-
- properties = {}
- properties['target_discovered'] = False
- properties['target_portal'] = '%s:%s' % (ctrl['ip'], '3260')
- properties['target_iqn'] = target
- properties['target_lun'] = lun
- properties['volume_id'] = volume['id']
-
- properties['auth_method'] = 'CHAP'
- properties['auth_username'] = ctrl['chap_user']
- properties['auth_password'] = ctrl['chap_passwd']
-
- LOG.debug('Attach properties: %(properties)s',
- {'properties': properties})
- return {'driver_volume_type': 'iscsi',
- 'data': properties}
-
- def terminate_connection(self, volume, connector, **kwargs):
- """Detach volume from the initiator."""
- # Get server name for IQN
- initiator_name = connector['initiator']
- vpsa_srv = self._get_server_name(initiator_name)
- if not vpsa_srv:
- raise exception.ZadaraServerNotFound(name=initiator_name)
-
- # Get volume name
- name = self.configuration.zadara_vol_name_template % volume['name']
- vpsa_vol = self._get_vpsa_volume_name(name)
- if not vpsa_vol:
- raise exception.VolumeNotFound(volume_id=name)
-
- # Detach volume from server
- self.vpsa.send_cmd('detach_volume',
- vpsa_srv=vpsa_srv,
- vpsa_vol=vpsa_vol)
-
- def get_volume_stats(self, refresh=False):
- """Get volume stats.
- If 'refresh' is True, run update the stats first.
- """
- if refresh:
- self._update_volume_stats()
-
- return self._stats
-
- def _update_volume_stats(self):
- """Retrieve stats info from volume group."""
-
- LOG.debug("Updating volume stats")
- data = {}
-
- backend_name = self.configuration.safe_get('volume_backend_name')
- data["volume_backend_name"] = backend_name or self.__class__.__name__
- data["vendor_name"] = 'Zadara Storage'
- data["driver_version"] = self.VERSION
- data["storage_protocol"] = 'iSCSI'
- data['reserved_percentage'] = self.configuration.reserved_percentage
- data['QoS_support'] = False
-
- (total, free) = self._get_pool_capacity(self.configuration.
- zadara_vpsa_poolname)
- data['total_capacity_gb'] = total
- data['free_capacity_gb'] = free
-
- self._stats = data