]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Volume driver for HP XP storage
authorKazumasa Nomura <kazumasa.nomura.rx@hitachi.com>
Tue, 16 Jun 2015 11:24:39 +0000 (20:24 +0900)
committerKazumasa Nomura <kazumasa.nomura.rx@hitachi.com>
Tue, 16 Jun 2015 11:30:07 +0000 (20:30 +0900)
Volume driver for HP XP storage, along with unit tests. The driver
imports the hp_xp_horcm_fc class, which is available separately to
HP XP customers.

Implements: blueprint hpxp-storage-driver

Change-Id: Ic28c7f3fc5d61e9c2304dca60e416b5e94b990db
Signed-off-by: Kazumasa Nomura <kazumasa.nomura.rx@hitachi.com>
cinder/tests/unit/test_hp_xp_fc.py [new file with mode: 0644]
cinder/volume/drivers/san/hp/hp_xp_fc.py [new file with mode: 0644]
cinder/volume/drivers/san/hp/hp_xp_opts.py [new file with mode: 0644]

diff --git a/cinder/tests/unit/test_hp_xp_fc.py b/cinder/tests/unit/test_hp_xp_fc.py
new file mode 100644 (file)
index 0000000..cc0e82b
--- /dev/null
@@ -0,0 +1,833 @@
+# Copyright (C) 2015, Hitachi, Ltd.
+#
+#    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.
+#
+
+import mock
+
+from cinder import exception
+from cinder import test
+from cinder.tests.unit import fake_snapshot
+from cinder.tests.unit import fake_volume
+from cinder.volume import configuration as conf
+from cinder.volume import driver
+from cinder.volume.drivers.san.hp import hp_xp_fc
+from cinder.volume.drivers.san.hp import hp_xp_opts
+
+from oslo_config import cfg
+from oslo_utils import importutils
+CONF = cfg.CONF
+
+NORMAL_LDEV_TYPE = 'Normal'
+POOL_INFO = {'30': {'total_gb': 'infinite', 'free_gb': 'infinite'}}
+EXISTING_POOL_REF = {
+    '101': {'size': 128}
+}
+
+
+class HPXPFakeCommon(object):
+    """Fake HPXP Common."""
+
+    def __init__(self, conf, storage_protocol, **kwargs):
+        self.conf = conf
+        self.volumes = {}
+        self.snapshots = {}
+        self._stats = {}
+        self.POOL_SIZE = 1000
+        self.LDEV_MAX = 1024
+
+        self.driver_info = {
+            'hba_id': 'wwpns',
+            'hba_id_type': 'World Wide Name',
+            'msg_id': {'target': 308},
+            'volume_backend_name': 'HPXPFC',
+            'volume_opts': hp_xp_opts.FC_VOLUME_OPTS,
+            'volume_type': 'fibre_channel',
+        }
+
+        self.storage_info = {
+            'protocol': storage_protocol,
+            'pool_id': None,
+            'ldev_range': None,
+            'ports': [],
+            'compute_ports': [],
+            'wwns': {},
+            'output_first': True
+        }
+
+    def create_volume(self, volume):
+        if volume['size'] > self.POOL_SIZE:
+            raise exception.VolumeBackendAPIException(
+                data='The volume size (%s) exceeds the pool size (%s).' %
+                (volume['size'], self.POOL_SIZE))
+
+        newldev = self._available_ldev()
+        self.volumes[newldev] = volume
+
+        return {
+            'provider_location': newldev,
+            'metadata': {
+                'ldev': newldev,
+                'type': NORMAL_LDEV_TYPE
+            }
+        }
+
+    def _available_ldev(self):
+        for i in range(1, self.LDEV_MAX):
+            if self.volume_exists({'provider_location': str(i)}) is False:
+                return str(i)
+
+        raise exception.VolumeBackendAPIException(
+            data='Failed to get an available logical device.')
+
+    def volume_exists(self, volume):
+        return self.volumes.get(volume['provider_location'], None) is not None
+
+    def delete_volume(self, volume):
+        vol = self.volumes.get(volume['provider_location'], None)
+        if vol is not None:
+            if vol.get('is_busy') is True:
+                raise exception.VolumeIsBusy(volume_name=volume['name'])
+            del self.volumes[volume['provider_location']]
+
+    def create_snapshot(self, snapshot):
+        src_vref = self.volumes.get(snapshot["volume_id"])
+        if not src_vref:
+            raise exception.VolumeBackendAPIException(
+                data='The %(type)s %(id)s source to be replicated was not '
+                'found.' % {'type': 'snapshot', 'id': (snapshot.get('id'))})
+
+        newldev = self._available_ldev()
+        self.volumes[newldev] = snapshot
+
+        return {'provider_location': newldev}
+
+    def delete_snapshot(self, snapshot):
+        snap = self.volumes.get(snapshot['provider_location'], None)
+        if snap is not None:
+            if snap.get('is_busy') is True:
+                raise exception.SnapshotIsBusy(snapshot_name=snapshot['name'])
+            del self.volumes[snapshot['provider_location']]
+
+    def get_volume_stats(self, refresh=False):
+        if refresh:
+            d = {}
+            d['volume_backend_name'] = self.driver_info['volume_backend_name']
+            d['vendor_name'] = 'Hewlett-Packard'
+            d['driver_version'] = '1.3.0-0_2015.1'
+            d['storage_protocol'] = self.storage_info['protocol']
+            pool_info = POOL_INFO.get(self.conf.hpxp_pool)
+            if pool_info is None:
+                return self._stats
+            d['total_capacity_gb'] = pool_info['total_gb']
+            d['free_capacity_gb'] = pool_info['free_gb']
+            d['allocated_capacity_gb'] = 0
+            d['reserved_percentage'] = 0
+            d['QoS_support'] = False
+            self._stats = d
+        return self._stats
+
+    def create_volume_from_snapshot(self, volume, snapshot):
+        ldev = snapshot.get('provider_location')
+        if self.volumes.get(ldev) is None:
+            raise exception.VolumeBackendAPIException(
+                data='The %(type)s %(id)s source to be replicated '
+                'was not found.' % {'type': 'snapshot', 'id': snapshot['id']})
+        if volume['size'] != snapshot['volume_size']:
+            raise exception.VolumeBackendAPIException(
+                data='The specified operation is not supported. '
+                'The volume size must be the same as the source %(type)s. '
+                '(volume: %(volume_id)s)'
+                % {'type': 'snapshot', 'volume_id': volume['id']})
+
+        newldev = self._available_ldev()
+        self.volumes[newldev] = volume
+
+        return {
+            'provider_location': newldev,
+            'metadata': {
+                'ldev': newldev,
+                'type': NORMAL_LDEV_TYPE,
+                'snapshot': snapshot['id']
+            }
+        }
+
+    def create_cloned_volume(self, volume, src_vref):
+        ldev = src_vref.get('provider_location')
+        if self.volumes.get(ldev) is None:
+            raise exception.VolumeBackendAPIException(
+                data='The %(type)s %(id)s source to be replicated was not '
+                'found.' % {'type': 'volume', 'id': src_vref.get('id')})
+        if volume['size'] != src_vref['size']:
+            raise exception.VolumeBackendAPIException(
+                data='The specified operation is not supported. '
+                'The volume size must be the same as the source %(type)s. '
+                '(volume: %(volume_id)s)' %
+                {'type': 'volume', 'volume_id': volume['id']})
+
+        newldev = self._available_ldev()
+        self.volumes[newldev] = volume
+
+        return {
+            'provider_location': newldev,
+            'metadata': {
+                'ldev': newldev,
+                'type': NORMAL_LDEV_TYPE,
+                'volume': src_vref['id']
+            }
+        }
+
+    def extend_volume(self, volume, new_size):
+        ldev = volume.get('provider_location')
+        if not self.volumes.get(ldev):
+            raise exception.VolumeBackendAPIException(
+                data='The volume %(volume_id)s to be extended was not found.' %
+                {'volume_id': volume['id']})
+        if new_size > self.POOL_SIZE:
+            raise exception.VolumeBackendAPIException(
+                data='The volume size (%s) exceeds the pool size (%s).' %
+                (new_size, self.POOL_SIZE))
+
+        self.volumes[ldev]['size'] = new_size
+
+    def manage_existing(self, volume, existing_ref):
+        ldev = existing_ref.get('source-id')
+
+        return {
+            'provider_location': ldev,
+            'metadata': {
+                'ldev': ldev,
+                'type': NORMAL_LDEV_TYPE
+            }
+        }
+
+    def manage_existing_get_size(self, dummy_volume, existing_ref):
+        ldev = existing_ref.get('source-id')
+        if not EXISTING_POOL_REF.get(ldev):
+            raise exception.ManageExistingInvalidReference(
+                existing_ref=existing_ref, reason='No valid value is '
+                'specified for "source-id". A valid value '
+                'must be specified for "source-id" to manage the volume.')
+
+        size = EXISTING_POOL_REF[ldev]['size']
+        return size
+
+    def unmanage(self, volume):
+        vol = self.volumes.get(volume['provider_location'], None)
+        if vol is not None:
+            if vol.get('is_busy') is True:
+                raise exception.VolumeIsBusy(
+                    volume_name=volume['provider_location'])
+            del self.volumes[volume['provider_location']]
+
+    def get_pool_id(self):
+        pool = self.conf.hpxp_pool
+        if pool.isdigit():
+            return int(pool)
+        return None
+
+    def do_setup(self, context):
+        self.ctxt = context
+        self.storage_info['pool_id'] = self.get_pool_id()
+        if self.storage_info['pool_id'] is None:
+            raise exception.VolumeBackendAPIException(
+                data='A pool could not be found. (pool: %(pool)s)' %
+                {'pool': self.conf.hpxp_pool})
+
+    def initialize_connection(self, volume, connector):
+        ldev = volume.get('provider_location')
+        if not self.volumes.get(ldev):
+            raise exception.VolumeBackendAPIException(
+                data='The volume %(volume_id)s to be mapped was not found.' %
+                {'volume_id': volume['id']})
+
+        self.volumes[ldev]['attached'] = connector
+
+        return {
+            'driver_volume_type': self.driver_info['volume_type'],
+            'data': {
+                'target_discovered': True,
+                'target_lun': volume['id'],
+                'access_mode': 'rw',
+                'multipath': True,
+                'target_wwn': ['50060E801053C2E0'],
+                'initiator_target_map': {
+                    u'2388000087e1a2e0': ['50060E801053C2E0']},
+            }
+        }
+
+    def terminate_connection(self, volume, connector):
+        ldev = volume.get('provider_location')
+        if not self.volumes.get(ldev):
+            return
+        if not self.is_volume_attached(volume, connector):
+            raise exception.VolumeBackendAPIException(
+                data='Volume not found for %s' % ldev)
+
+        del self.volumes[volume['provider_location']]['attached']
+
+        for vol in self.volumes:
+            if 'attached' in self.volumes[vol]:
+                return
+
+        return {
+            'driver_volume_type': self.driver_info['volume_type'],
+            'data': {
+                'target_lun': volume['id'],
+                'target_wwn': ['50060E801053C2E0'],
+                'initiator_target_map': {
+                    u'2388000087e1a2e0': ['50060E801053C2E0']},
+            }
+        }
+
+    def is_volume_attached(self, volume, connector):
+        if not self.volume_exists(volume):
+            return False
+        return (self.volumes[volume['provider_location']].get('attached', None)
+                == connector)
+
+    def copy_volume_data(self, context, src_vol, dest_vol, remote=None):
+        pass
+
+    def copy_image_to_volume(self, context, volume, image_service, image_id):
+        pass
+
+    def restore_backup(self, context, backup, volume, backup_service):
+        pass
+
+
+class HPXPFCDriverTest(test.TestCase):
+    """Test HPXPFCDriver."""
+
+    _VOLUME = {'size': 128,
+               'name': 'test1',
+               'id': 'id1',
+               'status': 'available'}
+
+    _VOLUME2 = {'size': 128,
+                'name': 'test2',
+                'id': 'id2',
+                'status': 'available'}
+
+    _VOLUME3 = {'size': 256,
+                'name': 'test2',
+                'id': 'id3',
+                'status': 'available'}
+
+    _VOLUME_BACKUP = {'size': 128,
+                      'name': 'backup-test',
+                      'id': 'id-backup',
+                      'provider_location': '0',
+                      'status': 'available'}
+
+    _TEST_SNAPSHOT = {'volume_name': 'test',
+                      'size': 128,
+                      'volume_size': 128,
+                      'name': 'test-snap',
+                      'volume_id': '1',
+                      'id': 'test-snap-0',
+                      'status': 'available'}
+
+    _TOO_BIG_VOLUME_SIZE = 100000
+
+    def __init__(self, *args, **kwargs):
+        super(HPXPFCDriverTest, self).__init__(*args, **kwargs)
+
+    def setUp(self):
+        self._setup_config()
+        self._setup_driver()
+        super(HPXPFCDriverTest, self).setUp()
+
+    def _setup_config(self):
+        self.configuration = mock.Mock(conf.Configuration)
+        self.configuration.hpxp_storage_id = "00000"
+        self.configuration.hpxp_pool = "30"
+
+    @mock.patch.object(importutils, 'import_object', return_value=None)
+    def _setup_driver(self, arg1):
+        self.driver = hp_xp_fc.HPXPFCDriver(configuration=self.configuration)
+        self.driver.common = HPXPFakeCommon(self.configuration, 'FC')
+        self.driver.do_setup(None)
+
+    # API test cases
+    def test_create_volume(self):
+        """Test create_volume."""
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        rc = self.driver.create_volume(volume)
+        volume['provider_location'] = rc['provider_location']
+
+        has_volume = self.driver.common.volume_exists(volume)
+        self.assertTrue(has_volume)
+
+    def test_create_volume_error_on_no_pool_space(self):
+        """Test create_volume is error on no pool space."""
+        update = {
+            'size': self._TOO_BIG_VOLUME_SIZE,
+            'name': 'test',
+            'id': 'id1',
+            'status': 'available'
+        }
+        volume = fake_volume.fake_db_volume(**update)
+
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.create_volume, volume)
+
+    def test_create_volume_error_on_no_available_ldev(self):
+        """Test create_volume is error on no available ldev."""
+        for i in range(1, 1024):
+            volume = fake_volume.fake_db_volume(**self._VOLUME)
+            rc = self.driver.create_volume(volume)
+            self.assertEqual(str(i), rc['provider_location'])
+
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.create_volume, volume)
+
+    def test_delete_volume(self):
+        """Test delete_volume."""
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        rc = self.driver.create_volume(volume)
+        volume['provider_location'] = rc['provider_location']
+
+        self.driver.delete_volume(volume)
+
+        has_volume = self.driver.common.volume_exists(volume)
+        self.assertFalse(has_volume)
+
+    def test_delete_volume_on_non_existing_volume(self):
+        """Test delete_volume on non existing volume."""
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        volume['provider_location'] = '1'
+        has_volume = self.driver.common.volume_exists(volume)
+        self.assertFalse(has_volume)
+
+        self.driver.delete_volume(volume)
+
+    def test_delete_volume_error_on_busy_volume(self):
+        """Test delete_volume is error on busy volume."""
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        rc = self.driver.create_volume(volume)
+        self.driver.common.volumes[rc['provider_location']]['is_busy'] = True
+
+        volume['provider_location'] = rc['provider_location']
+
+        self.assertRaises(exception.VolumeIsBusy,
+                          self.driver.delete_volume, volume)
+
+    def test_create_snapshot(self):
+        """Test create_snapshot."""
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        self.driver.create_volume(volume)
+
+        snapshot = fake_snapshot.fake_db_snapshot(**self._TEST_SNAPSHOT)
+        rc = self.driver.create_snapshot(snapshot)
+        snapshot['provider_location'] = rc['provider_location']
+
+        has_volume = self.driver.common.volume_exists(snapshot)
+        self.assertTrue(has_volume)
+
+    def test_create_snapshot_error_on_non_src_ref(self):
+        """Test create_snapshot is error on non source reference."""
+        snapshot = fake_snapshot.fake_db_snapshot(**self._TEST_SNAPSHOT)
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.create_snapshot,
+                          snapshot)
+
+    def test_delete_snapshot(self):
+        """Test delete_snapshot."""
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        self.driver.create_volume(volume)
+
+        snapshot = fake_snapshot.fake_db_snapshot(**self._TEST_SNAPSHOT)
+        rc = self.driver.create_snapshot(snapshot)
+        snapshot['provider_location'] = rc['provider_location']
+
+        rc = self.driver.delete_snapshot(snapshot)
+
+        has_volume = self.driver.common.volume_exists(snapshot)
+        self.assertFalse(has_volume)
+
+    def test_delete_snapshot_error_on_busy_snapshot(self):
+        """Test delete_snapshot is error on busy snapshot."""
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        self.driver.create_volume(volume)
+
+        snapshot = fake_snapshot.fake_db_snapshot(**self._TEST_SNAPSHOT)
+        rc = self.driver.create_snapshot(snapshot)
+        self.driver.common.volumes[rc['provider_location']]['is_busy'] = True
+        snapshot['provider_location'] = rc['provider_location']
+
+        self.assertRaises(exception.SnapshotIsBusy,
+                          self.driver.delete_snapshot,
+                          snapshot)
+
+    def test_delete_snapshot_on_non_existing_snapshot(self):
+        """Test delete_snapshot on non existing snapshot."""
+        snapshot = fake_snapshot.fake_db_snapshot(**self._TEST_SNAPSHOT)
+        snapshot['provider_location'] = '1'
+
+        self.driver.delete_snapshot(snapshot)
+
+    def test_create_volume_from_snapshot(self):
+        """Test create_volume_from_snapshot."""
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        self.driver.create_volume(volume)
+
+        snapshot = fake_snapshot.fake_db_snapshot(**self._TEST_SNAPSHOT)
+        rc_snap = self.driver.create_snapshot(snapshot)
+        snapshot['provider_location'] = rc_snap['provider_location']
+
+        volume2 = fake_volume.fake_db_volume(**self._VOLUME2)
+        rc = self.driver.create_volume_from_snapshot(volume2, snapshot)
+        volume2['provider_location'] = rc['provider_location']
+
+        has_volume = self.driver.common.volume_exists(volume2)
+        self.assertTrue(has_volume)
+
+    def test_create_volume_from_snapshot_error_on_non_existing_snapshot(self):
+        """Test create_volume_from_snapshot is error on non existing snapshot.
+        """
+        volume2 = fake_volume.fake_db_volume(**self._VOLUME2)
+        snapshot = fake_snapshot.fake_db_snapshot(**self._TEST_SNAPSHOT)
+        snapshot['provider_location'] = '1'
+
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.create_volume_from_snapshot,
+                          volume2, snapshot)
+
+    def test_create_volume_from_snapshot_error_on_diff_size(self):
+        """Test create_volume_from_snapshot is error on different size."""
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        self.driver.create_volume(volume)
+
+        snapshot = fake_snapshot.fake_db_snapshot(**self._TEST_SNAPSHOT)
+        rc_snap = self.driver.create_snapshot(snapshot)
+        snapshot['provider_location'] = rc_snap['provider_location']
+
+        volume3 = fake_volume.fake_db_volume(**self._VOLUME3)
+
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.create_volume_from_snapshot,
+                          volume3, snapshot)
+
+    def test_create_cloned_volume(self):
+        """Test create_cloned_volume."""
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        rc_vol = self.driver.create_volume(volume)
+        volume['provider_location'] = rc_vol['provider_location']
+
+        volume2 = fake_volume.fake_db_volume(**self._VOLUME2)
+        rc = self.driver.create_cloned_volume(volume2, volume)
+
+        volume2['provider_location'] = rc['provider_location']
+
+        has_volume = self.driver.common.volume_exists(volume2)
+        self.assertTrue(has_volume)
+
+    def test_create_cloned_volume_error_on_non_existing_volume(self):
+        """Test create_cloned_volume is error on non existing volume."""
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        volume['provider_location'] = '1'
+        volume2 = fake_volume.fake_db_volume(**self._VOLUME2)
+
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.create_cloned_volume,
+                          volume2, volume)
+
+    def test_create_cloned_volume_error_on_diff_size(self):
+        """Test create_cloned_volume is error on different size."""
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        rc_vol = self.driver.create_volume(volume)
+        volume['provider_location'] = rc_vol['provider_location']
+
+        volume3 = fake_volume.fake_db_volume(**self._VOLUME3)
+
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.create_cloned_volume,
+                          volume3, volume)
+
+    def test_get_volume_stats(self):
+        """Test get_volume_stats."""
+        rc = self.driver.get_volume_stats(True)
+        self.assertEqual("Hewlett-Packard", rc['vendor_name'])
+
+    def test_get_volume_stats_error_on_non_existing_pool_id(self):
+        """Test get_volume_stats is error on non existing pool id."""
+        self.configuration.hpxp_pool = 29
+        rc = self.driver.get_volume_stats(True)
+        self.assertEqual({}, rc)
+
+    @mock.patch.object(driver.FibreChannelDriver, 'copy_volume_data')
+    def test_copy_volume_data(self, arg1):
+        """Test copy_volume_data."""
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        rc_vol = self.driver.create_volume(volume)
+        volume['provider_location'] = rc_vol['provider_location']
+
+        volume2 = fake_volume.fake_db_volume(**self._VOLUME2)
+        rc_vol2 = self.driver.create_volume(volume2)
+        volume2['provider_location'] = rc_vol2['provider_location']
+
+        self.driver.copy_volume_data(None, volume, volume2, None)
+
+        arg1.assert_called_with(None, volume, volume2, None)
+
+    @mock.patch.object(driver.FibreChannelDriver, 'copy_volume_data',
+                       side_effect=exception.CinderException)
+    def test_copy_volume_data_error(self, arg1):
+        """Test copy_volume_data is error."""
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        rc_vol = self.driver.create_volume(volume)
+        volume['provider_location'] = rc_vol['provider_location']
+
+        volume2 = fake_volume.fake_db_volume(**self._VOLUME2)
+        volume2['provider_location'] = '2'
+
+        self.assertRaises(exception.CinderException,
+                          self.driver.copy_volume_data,
+                          None, volume, volume2, None)
+
+        arg1.assert_called_with(None, volume, volume2, None)
+
+    @mock.patch.object(driver.FibreChannelDriver, 'copy_image_to_volume')
+    def test_copy_image_to_volume(self, arg1):
+        """Test copy_image_to_volume."""
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        rc_vol = self.driver.create_volume(volume)
+        volume['provider_location'] = rc_vol['provider_location']
+
+        self.driver.copy_image_to_volume(None, volume, None, None)
+
+        arg1.assert_called_with(None, volume, None, None)
+
+    @mock.patch.object(driver.FibreChannelDriver, 'copy_image_to_volume',
+                       side_effect=exception.CinderException)
+    def test_copy_image_to_volume_error(self, arg1):
+        """Test copy_image_to_volume is error."""
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        volume['provider_location'] = '1'
+
+        self.assertRaises(exception.CinderException,
+                          self.driver.copy_image_to_volume,
+                          None, volume, None, None)
+        arg1.assert_called_with(None, volume, None, None)
+
+    @mock.patch.object(driver.FibreChannelDriver, 'restore_backup')
+    def test_restore_backup(self, arg1):
+        """Test restore_backup."""
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        rc_vol = self.driver.create_volume(volume)
+        volume['provider_location'] = rc_vol['provider_location']
+
+        volume_backup = fake_volume.fake_db_volume(**self._VOLUME_BACKUP)
+        self.driver.restore_backup(None, volume_backup, volume, None)
+        arg1.assert_called_with(None, volume_backup, volume, None)
+
+    @mock.patch.object(driver.FibreChannelDriver, 'restore_backup',
+                       side_effect=exception.CinderException)
+    def test_restore_backup_error(self, arg1):
+        """Test restore_backup is error."""
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        volume['provider_location'] = '1'
+
+        volume_backup = fake_volume.fake_db_volume(**self._VOLUME_BACKUP)
+
+        self.assertRaises(exception.CinderException,
+                          self.driver.restore_backup,
+                          None, volume_backup, volume, None)
+        arg1.assert_called_with(None, volume_backup, volume, None)
+
+    def test_extend_volume(self):
+        """Test extend_volume."""
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        rc = self.driver.create_volume(volume)
+        volume['provider_location'] = rc['provider_location']
+
+        new_size = 256
+        self.driver.extend_volume(volume, new_size)
+
+        actual = self.driver.common.volumes[rc['provider_location']]['size']
+        self.assertEqual(new_size, actual)
+
+    def test_extend_volume_error_on_non_existing_volume(self):
+        """Test extend_volume is error on non existing volume."""
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        volume['provider_location'] = '1'
+
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.extend_volume, volume, 256)
+
+    def test_extend_volume_error_on_no_pool_space(self):
+        """Test extend_volume is error on no pool space."""
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        rc = self.driver.create_volume(volume)
+        volume['provider_location'] = rc['provider_location']
+
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.extend_volume,
+                          volume, self._TOO_BIG_VOLUME_SIZE)
+
+    def test_manage_existing(self):
+        """Test manage_existing."""
+        existing_ref = {'source-id': '101'}
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        rc = self.driver.manage_existing(volume, existing_ref)
+
+        self.assertEqual('101', rc['provider_location'])
+
+    def test_manage_existing_with_none_sourceid(self):
+        """Test manage_existing is error with no source-id."""
+        existing_ref = {'source-id': None}
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        rc = self.driver.manage_existing(volume, existing_ref)
+
+        self.assertEqual(None, rc['provider_location'])
+
+    def test_manage_existing_get_size(self):
+        """Test manage_existing_get_size."""
+        existing_ref = {'source-id': '101'}
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        return_size = self.driver.manage_existing_get_size(
+            volume, existing_ref)
+        self.assertEqual(EXISTING_POOL_REF['101']['size'], return_size)
+
+    def test_manage_existing_get_size_with_none_sourceid(self):
+        """Test manage_existing_get_size is error with no source-id."""
+        existing_ref = {'source-id': None}
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        self.assertRaises(exception.ManageExistingInvalidReference,
+                          self.driver.manage_existing_get_size,
+                          volume, existing_ref)
+
+    def test_unmanage(self):
+        """Test unmanage."""
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        rc = self.driver.create_volume(volume)
+        volume['provider_location'] = rc['provider_location']
+        self.assertTrue(self.driver.common.volume_exists(volume))
+
+        self.driver.unmanage(volume)
+        self.assertFalse(self.driver.common.volume_exists(volume))
+
+    def test_unmanage_error_on_busy_volume(self):
+        """Test unmanage is error on busy volume."""
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        rc = self.driver.create_volume(volume)
+        ldev = rc['provider_location']
+        self.driver.common.volumes[ldev]['is_busy'] = True
+
+        self.assertRaises(exception.VolumeIsBusy,
+                          self.driver.unmanage,
+                          {'provider_location': ldev})
+
+    def test_initialize_connection(self):
+        """Test initialize_connection."""
+        connector = {'wwpns': ['12345678912345aa', '12345678912345bb'],
+                     'ip': '127.0.0.1'}
+
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        rc_vol = self.driver.create_volume(volume)
+        volume['provider_location'] = rc_vol['provider_location']
+        conn_info = self.driver.initialize_connection(volume, connector)
+        self.assertIn('data', conn_info)
+        self.assertIn('initiator_target_map', conn_info['data'])
+
+        is_attached = self.driver.common.is_volume_attached(volume, connector)
+        self.assertTrue(is_attached)
+
+        self.driver.terminate_connection(volume, connector)
+        self.driver.delete_volume(volume)
+
+    def test_initialize_connection_error_on_non_exisiting_volume(self):
+        """Test initialize_connection is error on non existing volume."""
+        connector = {'wwpns': ['12345678912345aa', '12345678912345bb'],
+                     'ip': '127.0.0.1'}
+
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        volume['provider_location'] = '1'
+
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.initialize_connection,
+                          volume, connector)
+
+    def test_terminate_connection_on_non_last_volume(self):
+        """Test terminate_connection on non last volume."""
+        connector = {'wwpns': ['12345678912345aa', '12345678912345bb'],
+                     'ip': '127.0.0.1'}
+        last_volume = fake_volume.fake_db_volume(**self._VOLUME)
+        last_rc_vol = self.driver.create_volume(last_volume)
+        last_volume['provider_location'] = last_rc_vol['provider_location']
+        self.driver.initialize_connection(last_volume, connector)
+
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        rc_vol = self.driver.create_volume(volume)
+        volume['provider_location'] = rc_vol['provider_location']
+
+        self.driver.initialize_connection(volume, connector)
+        conn_info = self.driver.terminate_connection(volume, connector)
+        self.assertNotIn('data', conn_info)
+
+        is_attached = self.driver.common.is_volume_attached(volume, connector)
+        self.assertFalse(is_attached)
+
+        self.driver.delete_volume(volume)
+
+        self.driver.terminate_connection(last_volume, connector)
+        self.driver.delete_volume(last_volume)
+
+    def test_terminate_connection_on_non_existing_volume(self):
+        """Test terminate_connection on non existing volume."""
+        connector = {'wwpns': ['12345678912345aa', '12345678912345bb'],
+                     'ip': '127.0.0.1'}
+
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        volume['provider_location'] = '1'
+
+        self.driver.terminate_connection(volume, connector)
+
+    def test_terminate_connection_error_on_non_initialized_volume(self):
+        """Test terminate_connection is error on non initialized volume."""
+        connector = {'wwpns': ['12345678912345aa', '12345678912345bb'],
+                     'ip': '127.0.0.1'}
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        rc_vol = self.driver.create_volume(volume)
+        volume['provider_location'] = rc_vol['provider_location']
+
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.terminate_connection,
+                          volume, connector)
+
+    def test_terminate_connection_last_volume(self):
+        """Test terminate_connection on last volume on a host."""
+        connector = {'wwpns': ['12345678912345aa', '12345678912345bb'],
+                     'ip': '127.0.0.1'}
+        volume = fake_volume.fake_db_volume(**self._VOLUME)
+        rc_vol = self.driver.create_volume(volume)
+        volume['provider_location'] = rc_vol['provider_location']
+
+        self.driver.initialize_connection(volume, connector)
+        conn_info = self.driver.terminate_connection(volume, connector)
+        self.assertIn('data', conn_info)
+        self.assertIn('initiator_target_map', conn_info['data'])
+
+        is_attached = self.driver.common.is_volume_attached(volume, connector)
+        self.assertFalse(is_attached)
+
+        self.driver.delete_volume(volume)
+
+    def test_do_setup_error_on_invalid_pool_id(self):
+        """Test do_setup is error on invalid pool id."""
+        self.configuration.hpxp_pool = 'invalid'
+
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.do_setup, None)
diff --git a/cinder/volume/drivers/san/hp/hp_xp_fc.py b/cinder/volume/drivers/san/hp/hp_xp_fc.py
new file mode 100644 (file)
index 0000000..2e84087
--- /dev/null
@@ -0,0 +1,155 @@
+# Copyright (C) 2014, 2015, Hitachi, Ltd.
+#
+#    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.
+"""
+Fibre channel Cinder volume driver for Hewlett-Packard storage.
+
+"""
+
+from oslo_config import cfg
+from oslo_utils import importutils
+
+from cinder.volume import driver
+from cinder.volume.drivers.san.hp import hp_xp_opts as opts
+from cinder.zonemanager import utils as fczm_utils
+
+_DRIVER_DIR = 'cinder.volume.drivers.san.hp'
+_DRIVER_CLASS = 'hp_xp_horcm_fc.HPXPHORCMFC'
+
+CONF = cfg.CONF
+CONF.register_opts(opts.FC_VOLUME_OPTS)
+
+
+class HPXPFCDriver(driver.FibreChannelDriver):
+    """OpenStack Fibre Channel driver to enable HP XP storage."""
+
+    def __init__(self, *args, **kwargs):
+        """Initialize the driver."""
+        super(HPXPFCDriver, self).__init__(*args, **kwargs)
+
+        self.configuration.append_config_values(opts.FC_VOLUME_OPTS)
+        self.configuration.append_config_values(opts.COMMON_VOLUME_OPTS)
+        self.common = importutils.import_object(
+            '.'.join([_DRIVER_DIR, _DRIVER_CLASS]),
+            self.configuration, 'FC', **kwargs)
+
+    def check_for_setup_error(self):
+        """Setup errors are already checked for in do_setup so return pass."""
+        pass
+
+    def create_volume(self, volume):
+        """Create a volume."""
+        return self.common.create_volume(volume)
+
+    def create_volume_from_snapshot(self, volume, snapshot):
+        """Create a volume from a snapshot."""
+        return self.common.create_volume_from_snapshot(volume, snapshot)
+
+    def create_cloned_volume(self, volume, src_vref):
+        """Create a clone of the specified volume."""
+        return self.common.create_cloned_volume(volume, src_vref)
+
+    def delete_volume(self, volume):
+        """Delete a volume."""
+        self.common.delete_volume(volume)
+
+    def create_snapshot(self, snapshot):
+        """Create a snapshot."""
+        return self.common.create_snapshot(snapshot)
+
+    def delete_snapshot(self, snapshot):
+        """Delete a snapshot."""
+        self.common.delete_snapshot(snapshot)
+
+    def local_path(self, volume):
+        pass
+
+    def get_volume_stats(self, refresh=False):
+        """Get volume stats."""
+        return self.common.get_volume_stats(refresh)
+
+    def copy_volume_data(self, context, src_vol, dest_vol, remote=None):
+        """Copy data from src_vol to dest_vol.
+
+        Call copy_volume_data() of super class and
+        carry out original postprocessing.
+        """
+        super(HPXPFCDriver, self).copy_volume_data(
+            context, src_vol, dest_vol, remote)
+        self.common.copy_volume_data(context, src_vol, dest_vol, remote)
+
+    def copy_image_to_volume(self, context, volume, image_service, image_id):
+        """Fetch the image from image_service and write it to the volume.
+
+        Call copy_image_to_volume() of super class and
+        carry out original postprocessing.
+        """
+        super(HPXPFCDriver, self).copy_image_to_volume(
+            context, volume, image_service, image_id)
+        self.common.copy_image_to_volume(
+            context, volume, image_service, image_id)
+
+    def restore_backup(self, context, backup, volume, backup_service):
+        """Restore an existing backup to a new or existing volume.
+
+        Call restore_backup() of super class and
+        carry out original postprocessing.
+        """
+        super(HPXPFCDriver, self).restore_backup(
+            context, backup, volume, backup_service)
+        self.common.restore_backup(context, backup, volume, backup_service)
+
+    def extend_volume(self, volume, new_size):
+        """Extend a volume."""
+        self.common.extend_volume(volume, new_size)
+
+    def manage_existing(self, volume, existing_ref):
+        """Manage an existing HP XP storage volume.
+
+        existing_ref is a dictionary of the form:
+
+        {'ldev': <logical device number on storage>,
+         'storage_id': <product number of storage system>}
+        """
+        return self.common.manage_existing(volume, existing_ref)
+
+    def manage_existing_get_size(self, volume, existing_ref):
+        """Return size of volume for manage_existing."""
+        return self.common.manage_existing_get_size(volume, existing_ref)
+
+    def unmanage(self, volume):
+        """Remove the specified volume from Cinder management."""
+        self.common.unmanage(volume)
+
+    def do_setup(self, context):
+        """Setup and verify HP XP storage connection."""
+        self.common.do_setup(context)
+
+    def ensure_export(self, context, volume):
+        pass
+
+    def create_export(self, context, volume):
+        pass
+
+    def remove_export(self, context, volume):
+        pass
+
+    @fczm_utils.AddFCZone
+    def initialize_connection(self, volume, connector):
+        """Attach the volume to an instance."""
+        return self.common.initialize_connection(volume, connector)
+
+    @fczm_utils.RemoveFCZone
+    def terminate_connection(self, volume, connector, **kwargs):
+        """Detach a volume from an instance."""
+        return self.common.terminate_connection(volume, connector, **kwargs)
diff --git a/cinder/volume/drivers/san/hp/hp_xp_opts.py b/cinder/volume/drivers/san/hp/hp_xp_opts.py
new file mode 100644 (file)
index 0000000..3dcb68d
--- /dev/null
@@ -0,0 +1,111 @@
+# Copyright (C) 2015, Hitachi, Ltd.
+#
+#    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.
+"""HP XP driver options."""
+
+from oslo_config import cfg
+from oslo_config import types
+
+FC_VOLUME_OPTS = [
+    cfg.BoolOpt(
+        'hpxp_zoning_request',
+        default=False,
+        help='Request for FC Zone creating host group'),
+]
+
+COMMON_VOLUME_OPTS = [
+    cfg.StrOpt(
+        'hpxp_storage_cli',
+        default=None,
+        required=True,
+        help='Type of storage command line interface'),
+    cfg.StrOpt(
+        'hpxp_storage_id',
+        default=None,
+        required=True,
+        help='ID of storage system'),
+    cfg.StrOpt(
+        'hpxp_pool',
+        default=None,
+        required=True,
+        help='Pool of storage system'),
+    cfg.StrOpt(
+        'hpxp_thin_pool',
+        default=None,
+        help='Thin pool of storage system'),
+    cfg.StrOpt(
+        'hpxp_ldev_range',
+        default=None,
+        help='Logical device range of storage system'),
+    cfg.StrOpt(
+        'hpxp_default_copy_method',
+        default='FULL',
+        help='Default copy method of storage system. '
+             'There are two valid values: "FULL" specifies that a full copy; '
+             '"THIN" specifies that a thin copy. Default value is "FULL"'),
+    cfg.Opt(
+        'hpxp_copy_speed',
+        type=types.Integer(min=1, max=15),
+        default=3,
+        help='Copy speed of storage system'),
+    cfg.Opt(
+        'hpxp_copy_check_interval',
+        type=types.Integer(min=1, max=600),
+        default=3,
+        help='Interval to check copy'),
+    cfg.Opt(
+        'hpxp_async_copy_check_interval',
+        type=types.Integer(min=1, max=600),
+        default=10,
+        help='Interval to check copy asynchronously'),
+    cfg.ListOpt(
+        'hpxp_target_ports',
+        default=None,
+        required=True,
+        help='Target port names for host group or iSCSI target'),
+    cfg.ListOpt(
+        'hpxp_compute_target_ports',
+        default=None,
+        help=(
+            'Target port names of compute node '
+            'for host group or iSCSI target')),
+    cfg.BoolOpt(
+        'hpxp_group_request',
+        default=False,
+        help='Request for creating host group or iSCSI target'),
+]
+
+HORCM_VOLUME_OPTS = [
+    cfg.Opt(
+        'hpxp_horcm_numbers',
+        type=types.List(item_type=types.Integer(min=0, max=2047)),
+        default=[200, 201],
+        help='Instance numbers for HORCM'),
+    cfg.StrOpt(
+        'hpxp_horcm_user',
+        default=None,
+        required=True,
+        help='Username of storage system for HORCM'),
+    cfg.BoolOpt(
+        'hpxp_horcm_add_conf',
+        default=True,
+        help='Add to HORCM configuration'),
+    cfg.StrOpt(
+        'hpxp_horcm_resource_name',
+        default='meta_resource',
+        help='Resource group name of storage system for HORCM'),
+    cfg.BoolOpt(
+        'hpxp_horcm_name_only_discovery',
+        default=False,
+        help='Only discover a specific name of host group or iSCSI target'),
+]