]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
blueprint zadara-volume-driver
authorVladimir Popovski <vladimir@zadarastorage.com>
Fri, 10 Aug 2012 07:14:26 +0000 (00:14 -0700)
committerJohn Griffith <john.griffith@solidfire.com>
Thu, 16 Aug 2012 03:21:21 +0000 (21:21 -0600)
Adds support for Zadara VPSA storage arrays as a BackEnd for Cinder

Change-Id: I8c2a163079853d4003223eb2c156cfd2ccef3129

cinder/exception.py
cinder/tests/test_zadara.py [new file with mode: 0644]
cinder/volume/zadara.py [new file with mode: 0755]

index 80290c5f467d37ab682b35e8e0e54e659833d48d..176291724be019e923554145432a9783cf32aff8 100644 (file)
@@ -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 (file)
index 0000000..521bf89
--- /dev/null
@@ -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': """
+            <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)],
+               '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 = """<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>
+                    <status>Available</status>
+                    <virtual-capacity type='integer'>1</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'])
+
+    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'])
+
+    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']
+
+
+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'] = """<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)
+        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 (executable)
index 0000000..e55d643
--- /dev/null
@@ -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()