]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add features to Zadara Storage Cinder driver
authorVladimir Popovski <vladimir@zadarastorage.com>
Fri, 16 Aug 2013 04:34:54 +0000 (04:34 +0000)
committerVladimir Popovski <vladimir@zadarastorage.com>
Tue, 3 Sep 2013 23:16:35 +0000 (23:16 +0000)
- move to Zadara APIs 13.07
- added support for extend volume
- added support for create/delete snapshot
- added support for create clones from volumes and snaps
- added support for multi-backend
- added volume stats
- added tests

Implements: blueprint zadara-cinder-driver-update

Change-Id: Iad5908a50980c59df2d8d4702743a0b99f82f9b7

cinder/tests/test_zadara.py
cinder/volume/drivers/zadara.py
etc/cinder/cinder.conf.sample

index 88c3bb73101b8d9509d369bec283f27813c39453..5e1c4f687ef50ac83cdb464f8568288876eb2ebc 100644 (file)
@@ -21,13 +21,14 @@ Tests for Zadara VPSA volume driver
 
 import copy
 import httplib
+import mox
 
 from cinder import exception
 from cinder.openstack.common import log as logging
 from cinder import test
-from cinder.volume.drivers import zadara
-
-from lxml import etree
+from cinder.volume import configuration as conf
+from cinder.volume.drivers.zadara import zadara_opts
+from cinder.volume.drivers.zadara import ZadaraVPSAISCSIDriver
 
 LOG = logging.getLogger("cinder.volume.driver")
 
@@ -38,7 +39,7 @@ DEFAULT_RUNTIME_VARS = {
     'access_key': '0123456789ABCDEF',
     'volumes': [],
     'servers': [],
-    'controllers': [('active_ctrl', {'display_name': 'test_ctrl'})],
+    'controllers': [('active_ctrl', {'display-name': 'test_ctrl'})],
     'counter': 1000,
 
     'login': """
@@ -99,11 +100,20 @@ class FakeRequest(object):
                         ('/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)],
+                        ('/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)]
                }
@@ -156,6 +166,9 @@ class FakeRequest(object):
         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))
@@ -166,6 +179,7 @@ class FakeRequest(object):
         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
@@ -209,6 +223,65 @@ class FakeRequest(object):
 
         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]
 
@@ -223,10 +296,16 @@ class FakeRequest(object):
 
         return RUNTIME_VARS['bad_volume']
 
-    def _generate_list_resp(self, header, footer, body, lst):
+    def _generate_list_resp(self, header, footer, body, lst, vol):
         resp = header
         for (obj, params) in lst:
-            resp += body % (obj, params['display_name'])
+            if vol:
+                resp += body % (obj,
+                                params['display-name'],
+                                params['cg-name'],
+                                params['capacity'])
+            else:
+                resp += body % (obj, params['display-name'])
         resp += footer
         return resp
 
@@ -238,8 +317,9 @@ class FakeRequest(object):
         body = """<volume>
                     <name>%s</name>
                     <display-name>%s</display-name>
+                    <cg-name>%s</cg-name>
                     <status>Available</status>
-                    <virtual-capacity type='integer'>1</virtual-capacity>
+                    <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>
@@ -249,7 +329,8 @@ class FakeRequest(object):
         return self._generate_list_resp(header,
                                         footer,
                                         body,
-                                        RUNTIME_VARS['volumes'])
+                                        RUNTIME_VARS['volumes'],
+                                        True)
 
     def _list_controllers(self):
         header = """<show-vcontrollers-response>
@@ -272,7 +353,16 @@ class FakeRequest(object):
         return self._generate_list_resp(header,
                                         footer,
                                         body,
-                                        RUNTIME_VARS['controllers'])
+                                        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>
@@ -290,7 +380,7 @@ class FakeRequest(object):
 
         resp = header
         for (obj, params) in RUNTIME_VARS['servers']:
-            resp += body % (obj, params['display_name'], params['iqn'])
+            resp += body % (obj, params['display-name'], params['iqn'])
         resp += footer
         return resp
 
@@ -321,13 +411,40 @@ class FakeRequest(object):
                 for server in attachments:
                     srv_params = self._get_server_obj(server)
                     resp += body % (server,
-                                    srv_params['display_name'],
+                                    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."""
@@ -363,14 +480,18 @@ class ZadaraVPSADriverTestCase(test.TestCase):
     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.configuration = conf.Configuration(None)
+        self.configuration.append_config_values(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 = ZadaraVPSAISCSIDriver(configuration=self.configuration)
         self.stubs.Set(httplib, 'HTTPConnection', FakeHTTPConnection)
         self.stubs.Set(httplib, 'HTTPSConnection', FakeHTTPSConnection)
         self.driver.do_setup(None)
@@ -417,15 +538,6 @@ class ZadaraVPSADriverTestCase(test.TestCase):
         self.driver.ensure_export(context, volume)
         self.driver.remove_export(context, volume)
 
-        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)
@@ -579,3 +691,107 @@ class ZadaraVPSADriverTestCase(test.TestCase):
         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.assertEquals(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',
+                           })
index 354a439bd4e403882d66a066f3c1e6b806afa8af..a102e2dec149dd00d39f38f93d7c704e4a3b4939 100644 (file)
@@ -18,7 +18,7 @@
 """
 Volume driver for Zadara Virtual Private Storage Array (VPSA).
 
-This driver requires VPSA with API ver.12.06 or higher.
+This driver requires VPSA with API ver.13.07 or higher.
 """
 
 
@@ -31,8 +31,7 @@ from cinder import exception
 from cinder.openstack.common import log as logging
 from cinder.volume import driver
 
-
-LOG = logging.getLogger("cinder.volume.driver")
+LOG = logging.getLogger(__name__)
 
 zadara_opts = [
     cfg.StrOpt('zadara_vpsa_ip',
@@ -56,12 +55,12 @@ zadara_opts = [
                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.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_default_striping_mode',
                default='simple',
                help='Default striping mode for volumes'),
@@ -85,12 +84,8 @@ CONF.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
+    def __init__(self, conf):
+        self.conf = conf
         self.access_key = None
 
         self.ensure_connection()
@@ -109,25 +104,49 @@ class ZadaraVPSAConnection(object):
         vpsa_commands = {
             'login': ('POST',
                       '/api/users/login.xml',
-                      {'user': self.user,
-                       'password': self.password}),
+                      {'user': self.conf.zadara_user,
+                       'password': self.conf.zadara_password}),
 
             # Volume operations
             'create_volume': ('POST',
                               '/api/volumes.xml',
-                              {'display_name': kwargs.get('name'),
-                               'virtual_capacity': kwargs.get('size'),
-                               'raid_group_name[]': CONF.zadara_vpsa_poolname,
-                               'quantity': 1,
-                               'cache': CONF.zadara_default_cache_policy,
-                               'crypt': CONF.zadara_default_encryption,
-                               'mode': CONF.zadara_default_striping_mode,
-                               'stripesize': CONF.zadara_default_stripesize,
-                               'force': 'NO'}),
+                              {'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',
@@ -150,6 +169,9 @@ class ZadaraVPSAConnection(object):
             'list_volumes': ('GET',
                              '/api/volumes.xml',
                              {}),
+            'list_pools': ('GET',
+                           '/api/pools.xml',
+                           {}),
             'list_controllers': ('GET',
                                  '/api/vcontrollers.xml',
                                  {}),
@@ -159,7 +181,11 @@ class ZadaraVPSAConnection(object):
             '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)
@@ -218,10 +244,12 @@ class ZadaraVPSAConnection(object):
         LOG.debug(_('Sending %(method)s to %(url)s. Body "%(body)s"'),
                   {'method': method, 'url': url, 'body': body})
 
-        if self.use_ssl:
-            connection = httplib.HTTPSConnection(self.host, self.port)
+        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.host, self.port)
+            connection = httplib.HTTPConnection(self.conf.zadara_vpsa_ip,
+                                                self.conf.zadara_vpsa_port)
         connection.request(method, url, body)
         response = connection.getresponse()
 
@@ -244,19 +272,18 @@ class ZadaraVPSAConnection(object):
 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(CONF.zadara_vpsa_ip,
-                                         CONF.zadara_vpsa_port,
-                                         CONF.zadara_vpsa_use_ssl,
-                                         CONF.zadara_user,
-                                         CONF.zadara_password)
+        self.vpsa = ZadaraVPSAConnection(self.configuration)
 
     def check_for_setup_error(self):
         """Returns an error (exception) if prerequisites aren't met."""
@@ -291,16 +318,57 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
                     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('name')
+            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')
@@ -334,7 +402,7 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
         """Create volume."""
         self.vpsa.send_cmd(
             'create_volume',
-            name=CONF.zadara_vol_name_template % volume['name'],
+            name=self.configuration.zadara_vol_name_template % volume['name'],
             size=volume['size'])
 
     def delete_volume(self, volume):
@@ -344,13 +412,13 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
         Return ok if doesn't exist. Auto detach from all servers.
         """
         # Get volume name
-        name = CONF.zadara_vol_name_template % 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 CONF.zadara_vpsa_allow_nonexistent_delete:
+            if self.configuration.zadara_vpsa_allow_nonexistent_delete:
                 return
             else:
                 raise exception.VolumeNotFound(volume_id=name)
@@ -361,7 +429,7 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
         servers = self._xml_parse_helper(xml_tree, 'servers',
                                          ('iqn', None), first=False)
         if servers:
-            if not CONF.zadara_vpsa_auto_detach_on_delete:
+            if not self.configuration.zadara_vpsa_auto_detach_on_delete:
                 raise exception.VolumeAttached(volume_id=name)
 
             for server in servers:
@@ -374,6 +442,116 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
         # 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(_("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(_("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
@@ -404,7 +582,7 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
             raise exception.ZadaraServerCreateFailure(name=initiator_name)
 
         # Get volume name
-        name = CONF.zadara_vol_name_template % 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)
@@ -460,7 +638,7 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
             raise exception.ZadaraServerNotFound(name=initiator_name)
 
         # Get volume name
-        name = CONF.zadara_vol_name_template % 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)
@@ -469,3 +647,33 @@ class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
         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
index 3d71f0e2d08847716124cbb816230e7d6aa0e5c6..2f10697145854a0e9ef0b2bb2a2cc7602c99cd04 100644 (file)
 # Name of VPSA storage pool for volumes (string value)
 #zadara_vpsa_poolname=<None>
 
-# Default cache policy for volumes (string value)
-#zadara_default_cache_policy=write-through
+# Default thin provisioning policy for volumes (boolean value)
+#zadara_vol_thin=true
 
-# Default encryption policy for volumes (string value)
-#zadara_default_encryption=NO
+# Default encryption policy for volumes (boolean value)
+#zadara_vol_encrypt=false
 
 # Default striping mode for volumes (string value)
 #zadara_default_striping_mode=simple