From 7ea56d215453a01ac33934a71f801f2e4a99f4ce Mon Sep 17 00:00:00 2001 From: Vladimir Popovski Date: Fri, 10 Aug 2012 00:14:26 -0700 Subject: [PATCH] blueprint zadara-volume-driver Adds support for Zadara VPSA storage arrays as a BackEnd for Cinder Change-Id: I8c2a163079853d4003223eb2c156cfd2ccef3129 --- cinder/exception.py | 36 +++ cinder/tests/test_zadara.py | 577 ++++++++++++++++++++++++++++++++++++ cinder/volume/zadara.py | 483 ++++++++++++++++++++++++++++++ 3 files changed, 1096 insertions(+) create mode 100644 cinder/tests/test_zadara.py create mode 100755 cinder/volume/zadara.py diff --git a/cinder/exception.py b/cinder/exception.py index 80290c5f4..176291724 100644 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -894,6 +894,42 @@ class DuplicateVlan(Duplicate): message = _("Detected existing vlan with id %(vlan)d") +class UnknownCmd(Invalid): + message = _("Unknown or unsupported command %(cmd)s") + + +class MalformedResponse(Invalid): + message = _("Malformed response to command %(cmd)s: %(reason)s") + + +class BadHTTPResponseStatus(CinderException): + message = _("Bad HTTP response status %(status)s") + + +class FailedCmdWithDump(CinderException): + message = _("Operation failed with status=%(status)s. Full dump: %(data)s") + + +class ZadaraServerCreateFailure(CinderException): + message = _("Unable to create server object for initiator %(name)s") + + +class ZadaraServerNotFound(NotFound): + message = _("Unable to find server object for initiator %(name)s") + + +class ZadaraVPSANoActiveController(CinderException): + message = _("Unable to find any active VPSA controller") + + +class ZadaraAttachmentsNotFound(NotFound): + message = _("Failed to retrieve attachments for volume %(name)s") + + +class ZadaraInvalidAttachmentInfo(Invalid): + message = _("Invalid attachment info for volume %(name)s: %(reason)s") + + class InstanceNotFound(NotFound): message = _("Instance %(instance_id)s could not be found.") diff --git a/cinder/tests/test_zadara.py b/cinder/tests/test_zadara.py new file mode 100644 index 000000000..521bf8997 --- /dev/null +++ b/cinder/tests/test_zadara.py @@ -0,0 +1,577 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 Zadara Storage, Inc. +# Copyright (c) 2012 OpenStack LLC. +# 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 cinder import exception +from cinder import test +from cinder.openstack.common import log as logging +from cinder.volume import zadara + +from lxml import etree + +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': """ + + + 2012-04-30... + %s + 1 + 2012-02-21... + jsmith@example.com + jsmith + + 0 + """, + + 'good': """ + + 0 + """, + + 'bad_login': """ + + 5 + Some message... + """, + + 'bad_volume': """ + + 10081 + Virtual volume xxx not found + """, + + 'bad_server': """ + + 10086 + Server xxx not found + """, + + 'server_created': """ + + %s + 0 + """, +} + +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)], + 'DELETE': [('/api/volumes/*', self._delete)], + 'GET': [('/api/volumes.xml', self._list_volumes), + ('/api/vcontrollers.xml', self._list_controllers), + ('/api/servers.xml', self._list_servers), + ('/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['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'] + + 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 _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): + resp = header + for (obj, params) in lst: + resp += body % (obj, params['display_name']) + resp += footer + return resp + + def _list_volumes(self): + header = """ + 0 + """ + footer = "" + body = """ + %s + %s + Available + 1 + 1 + r5 + write-through + 2012-01-28... + 2012-01-28... + """ + return self._generate_list_resp(header, footer, body, + RUNTIME_VARS['volumes']) + + def _list_controllers(self): + header = """ + 0 + """ + footer = "" + body = """ + %s + %s + active + iqn.2011-04.com.zadarastorage:vsa-xxx:1 + 1.1.1.1 + 1.1.1.1 + 0.0.09-05.1--77.7 + ok + ok + test_chap_user + test_chap_secret + """ + return self._generate_list_resp(header, footer, body, + RUNTIME_VARS['controllers']) + + def _list_servers(self): + header = """ + 0 + """ + footer = "" + body = """ + %s + %s + %s + Active + 2012-01-28... + 2012-01-28... + """ + + 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 = """ + 0 + """ + footer = "" + body = """ + %s + %s + %s + iqn.2011-04.com.zadarastorage:vsa-xxx:1 + 0 + """ + + 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'] + + +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() + self.flags( + zadara_user='test', + zadara_password='test_password', + ) + global RUNTIME_VARS + RUNTIME_VARS = copy.deepcopy(DEFAULT_RUNTIME_VARS) + + self.driver = zadara.ZadaraVPSAISCSIDriver() + self.stubs.Set(httplib, 'HTTPConnection', FakeHTTPConnection) + self.stubs.Set(httplib, 'HTTPSConnection', FakeHTTPSConnection) + self.driver.do_setup(None) + + def tearDown(self): + super(ZadaraVPSADriverTestCase, self).tearDown() + + 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.driver.check_for_export(context, 0) + + self.assertRaises(NotImplementedError, + self.driver.create_volume_from_snapshot, + volume, None) + self.assertRaises(NotImplementedError, + self.driver.create_snapshot, + None) + self.assertRaises(NotImplementedError, + self.driver.delete_snapshot, + None) + 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) + props1 = self.driver.initialize_connection(volume, connector1) + props2 = self.driver.initialize_connection(volume, connector2) + props3 = 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} + 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.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) + props1 = self.driver.initialize_connection(volume1, connector1) + props2 = 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'] = """ + %s + 0 + """ + self.assertRaises(exception.MalformedResponse, + self.driver.do_setup, None) + + RUNTIME_VARS['login'] = """ + + + 2012-04-30... + 1 + 2012-02-21... + jsmith@example.com + jsmith + + %s + 0 + """ + 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) + props1 = self.driver.initialize_connection(volume1, connector1) + props2 = self.driver.initialize_connection(volume1, connector2) + props3 = 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) diff --git a/cinder/volume/zadara.py b/cinder/volume/zadara.py new file mode 100755 index 000000000..e55d6432d --- /dev/null +++ b/cinder/volume/zadara.py @@ -0,0 +1,483 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 Zadara Storage, Inc. +# Copyright (c) 2012 OpenStack LLC. +# 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.12.06 or higher. +""" + +import httplib + +from cinder import exception +from cinder import flags +from cinder.openstack.common import log as logging +from cinder.openstack.common import cfg +from cinder import utils +from cinder.volume import driver +from cinder.volume import iscsi + +from lxml import etree + + +LOG = logging.getLogger("cinder.volume.driver") + +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'), + + cfg.StrOpt('zadara_vpsa_poolname', + default=None, + help='Name of VPSA storage pool for volumes'), + + cfg.StrOpt('zadara_default_cache_policy', + default='write-through', + help='Default cache policy for volumes'), + cfg.StrOpt('zadara_default_encryption', + default='NO', + help='Default encryption policy for volumes'), + cfg.StrOpt('zadara_default_striping_mode', + default='simple', + help='Default striping mode for volumes'), + cfg.StrOpt('zadara_default_stripesize', + default='64', + help='Default stripe size 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"), + ] + +FLAGS = flags.FLAGS +FLAGS.register_opts(zadara_opts) + + +class ZadaraVPSAConnection(object): + """Executes volume driver commands on VPSA.""" + + def __init__(self, host, port, ssl, user, password): + self.host = host + self.port = port + self.use_ssl = ssl + self.user = user + self.password = password + 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.user, + 'password': self.password}), + + # Volume operations + 'create_volume': ('POST', + '/api/volumes.xml', + {'display_name': kwargs.get('name'), + 'virtual_capacity': kwargs.get('size'), + 'raid_group_name[]': FLAGS.zadara_vpsa_poolname, + 'quantity': 1, + 'cache': FLAGS.zadara_default_cache_policy, + 'crypt': FLAGS.zadara_default_encryption, + 'mode': FLAGS.zadara_default_striping_mode, + 'stripesize': FLAGS.zadara_default_stripesize, + 'force': 'NO'}), + 'delete_volume': ('DELETE', + '/api/volumes/%s.xml' % kwargs.get('vpsa_vol'), + {}), + + # 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_controllers': ('GET', + '/api/vcontrollers.xml', + {}), + 'list_servers': ('GET', + '/api/servers.xml', + {}), + 'list_vol_attachments': ('GET', + '/api/volumes/%s/servers.xml' + % kwargs.get('vpsa_vol'), + {}), + } + + 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"') + % locals()) + + if self.use_ssl: + connection = httplib.HTTPSConnection(self.host, self.port) + else: + connection = httplib.HTTPConnection(self.host, self.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') % locals()) + return xml_tree + + +class ZadaraVPSAISCSIDriver(driver.ISCSIDriver): + """Zadara VPSA iSCSI volume driver.""" + + def __init__(self, *args, **kwargs): + super(ZadaraVPSAISCSIDriver, self).__init__(*args, **kwargs) + + 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(FLAGS.zadara_vpsa_ip, + FLAGS.zadara_vpsa_port, + FLAGS.zadara_vpsa_use_ssl, + FLAGS.zadara_user, + FLAGS.zadara_password) + + 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.""" + LOG.error(_("Call to local_path should not happen." + " Verify that use_local_volumes flag is turned off.")) + 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(self, name): + """Return VPSA's name 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') + + return None + + 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=FLAGS.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 = FLAGS.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') % locals() + LOG.warning(msg) + if FLAGS.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 FLAGS.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_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 check_for_export(self, context, volume_id): + """Irrelevant for VPSA volumes. Export created during attachment.""" + 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 = FLAGS.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') % locals()) + return {'driver_volume_type': 'iscsi', + 'data': properties} + + def terminate_connection(self, volume, connector): + """ + 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 = FLAGS.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 create_volume_from_snapshot(self, volume, snapshot): + raise NotImplementedError() + + def create_snapshot(self, snapshot): + raise NotImplementedError() + + def delete_snapshot(self, snapshot): + raise NotImplementedError() -- 2.45.2