From f03e2e3ca3e31c830c5ef9b22f9b1c3aa8b1557a Mon Sep 17 00:00:00 2001 From: Avishay Traeger Date: Sun, 12 Aug 2012 14:55:04 +0300 Subject: [PATCH] Driver for IBM XIV storage. Volume driver for IBM XIV storage systems, along with unit tests. The driver imports the xiv_openstack_box class, which is a closed-source proxy available separately to IBM XIV customers. Change-Id: I3a3ec8b1d3e3adf1895b4a0bbd86993010d2e447 --- cinder/tests/fake_flags.py | 1 + cinder/tests/test_xiv.py | 242 +++++++++++++++++++++++++++++++++++++ cinder/volume/xiv.py | 128 ++++++++++++++++++++ 3 files changed, 371 insertions(+) create mode 100644 cinder/tests/test_xiv.py create mode 100644 cinder/volume/xiv.py diff --git a/cinder/tests/fake_flags.py b/cinder/tests/fake_flags.py index 5d0d1f864..5c247302a 100644 --- a/cinder/tests/fake_flags.py +++ b/cinder/tests/fake_flags.py @@ -35,3 +35,4 @@ def set_defaults(conf): conf.set_default('sql_connection', "sqlite://") conf.set_default('sqlite_synchronous', False) conf.set_default('policy_file', 'cinder/tests/policy.json') + conf.set_default('xiv_proxy', 'cinder.tests.test_xiv.XIVFakeProxyDriver') diff --git a/cinder/tests/test_xiv.py b/cinder/tests/test_xiv.py new file mode 100644 index 000000000..6328fa94e --- /dev/null +++ b/cinder/tests/test_xiv.py @@ -0,0 +1,242 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 IBM, 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. +# +# Authors: +# Erik Zaadi +# Avishay Traeger + +from cinder import exception +from cinder import flags +from cinder import test +from cinder.volume import xiv + + +FLAGS = flags.FLAGS +FAKE = "fake" +VOLUME = { + 'size': 16, + 'name': FAKE, + 'id': 1 + } + +CONNECTOR = { + 'initiator': "iqn.2012-07.org.fake:01:948f189c4695", + } + + +class XIVFakeProxyDriver(object): + """Fake XIV Proxy Driver.""" + + def __init__(self, xiv_info, logger, expt): + """ + Initialize Proxy + """ + + self.xiv_info = xiv_info + self.logger = logger + self.exception = expt + self.xiv_portal = \ + self.xiv_iqn = FAKE + + self.volumes = {} + + def setup(self, context): + if self.xiv_info['xiv_user'] != FLAGS.san_login: + raise self.exception.NotAuthorized() + + if self.xiv_info['xiv_address'] != FLAGS.san_ip: + raise self.exception.HostNotFound() + + def create_volume(self, volume): + if volume['size'] > 100: + raise self.exception.VolumeBackendAPIException() + self.volumes[volume['name']] = volume + + def volume_exists(self, volume): + return self.volumes.get(volume['name'], None) != None + + def delete_volume(self, volume): + if self.volumes.get(volume['name'], None) is not None: + del self.volumes[volume['name']] + + def initialize_connection(self, volume, connector): + if not self.volume_exists(volume): + raise self.exception.VolumeNotFound() + lun_id = volume['id'] + + self.volumes[volume['name']]['attached'] = connector + + return { + 'driver_volume_type': 'iscsi', + 'data': { + 'target_discovered': True, + 'target_portal': self.xiv_portal, + 'target_iqn': self.xiv_iqn, + 'target_lun': lun_id, + 'volume_id': volume['id'], + 'multipath': True, + # part of a patch to nova-compute to enable iscsi multipath + 'provider_location': "%s,1 %s %s" % ( + self.xiv_portal, + self.xiv_iqn, + lun_id), + }, + } + + def terminate_connection(self, volume, connector): + if not self.volume_exists(volume): + raise self.exception.VolumeNotFound() + if not self.is_volume_attached(volume, connector): + raise self.exception.VolumeNotFoundForInstance() + del self.volumes[volume['name']]['attached'] + + def is_volume_attached(self, volume, connector): + if not self.volume_exists(volume): + raise self.exception.VolumeNotFound() + + return self.volumes[volume['name']].get('attached', None) \ + == connector + + +class XIVVolumeDriverTest(test.TestCase): + """Test IBM XIV volume driver.""" + + def setUp(self): + """Initialize IVM XIV Driver.""" + super(XIVVolumeDriverTest, self).setUp() + + self.driver = xiv.XIVDriver() + + def test_initialized_should_set_xiv_info(self): + """Test that the san flags are passed to the XIV proxy.""" + + self.assertEquals( + self.driver.xiv_proxy.xiv_info['xiv_user'], + FLAGS.san_login) + self.assertEquals( + self.driver.xiv_proxy.xiv_info['xiv_pass'], + FLAGS.san_password) + self.assertEquals( + self.driver.xiv_proxy.xiv_info['xiv_address'], + FLAGS.san_ip) + self.assertEquals( + self.driver.xiv_proxy.xiv_info['xiv_vol_pool'], + FLAGS.san_clustername) + + def test_setup_should_fail_if_credentials_are_invalid(self): + """Test that the xiv_proxy validates credentials.""" + + self.driver.xiv_proxy.xiv_info['xiv_user'] = 'invalid' + self.assertRaises(exception.NotAuthorized, self.driver.do_setup, None) + + def test_setup_should_fail_if_connection_is_invalid(self): + """Test that the xiv_proxy validates connection.""" + + self.driver.xiv_proxy.xiv_info['xiv_address'] = 'invalid' + self.assertRaises(exception.HostNotFound, self.driver.do_setup, None) + + def test_create_volume(self): + """Test creating a volume.""" + + self.driver.do_setup(None) + self.driver.create_volume(VOLUME) + has_volume = self.driver.xiv_proxy.volume_exists(VOLUME) + self.assertTrue(has_volume) + self.driver.delete_volume(VOLUME) + + def test_volume_exists(self): + """Test the volume exist method with a volume that doesn't exist.""" + + self.driver.do_setup(None) + self.assertFalse(self.driver.xiv_proxy.volume_exists({'name': FAKE})) + + def test_delete_volume(self): + """Verify that a volume is deleted.""" + + self.driver.do_setup(None) + self.driver.create_volume(VOLUME) + self.driver.delete_volume(VOLUME) + has_volume = self.driver.xiv_proxy.volume_exists(VOLUME) + self.assertFalse(has_volume) + + def test_delete_volume_should_fail_for_not_existing_volume(self): + """Verify that deleting a non-existing volume is OK.""" + + self.driver.do_setup(None) + self.driver.delete_volume(VOLUME) + + def test_create_volume_should_fail_if_no_pool_space_left(self): + """Vertify that the xiv_proxy validates volume pool space.""" + + self.driver.do_setup(None) + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_volume, + {'name': FAKE, 'id': 1, 'size': 12000}) + + def test_initialize_connection(self): + """Test that inititialize connection attaches volume to host.""" + + self.driver.do_setup(None) + self.driver.create_volume(VOLUME) + self.driver.initialize_connection(VOLUME, CONNECTOR) + + self.assertTrue( + self.driver.xiv_proxy.is_volume_attached(VOLUME, CONNECTOR)) + + self.driver.terminate_connection(VOLUME, CONNECTOR) + self.driver.delete_volume(VOLUME) + + def test_initialize_connection_should_fail_for_non_existing_volume(self): + """Verify that initialize won't work for non-existing volume.""" + + self.driver.do_setup(None) + self.assertRaises(exception.VolumeNotFound, + self.driver.initialize_connection, VOLUME, CONNECTOR) + + def test_terminate_connection(self): + """Test terminating a connection.""" + + self.driver.do_setup(None) + self.driver.create_volume(VOLUME) + self.driver.initialize_connection(VOLUME, CONNECTOR) + self.driver.terminate_connection(VOLUME, CONNECTOR) + + self.assertFalse( + self.driver.xiv_proxy.is_volume_attached( + VOLUME, + CONNECTOR)) + + self.driver.delete_volume(VOLUME) + + def test_terminate_connection_should_fail_on_non_existing_volume(self): + """Test that terminate won't work for non-existing volumes.""" + + self.driver.do_setup(None) + self.assertRaises(exception.VolumeNotFound, + self.driver.terminate_connection, VOLUME, CONNECTOR) + + def test_terminate_connection_should_fail_on_non_attached_volume(self): + """Test that terminate won't work for volumes that are not attached.""" + + self.driver.do_setup(None) + self.driver.create_volume(VOLUME) + + self.assertRaises(exception.VolumeNotFoundForInstance, + self.driver.terminate_connection, VOLUME, CONNECTOR) + + self.driver.delete_volume(VOLUME) diff --git a/cinder/volume/xiv.py b/cinder/volume/xiv.py new file mode 100644 index 000000000..e440392a9 --- /dev/null +++ b/cinder/volume/xiv.py @@ -0,0 +1,128 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 IBM, 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. +# +# Authors: +# Erik Zaadi +# Avishay Traeger + +""" +Volume driver for IBM XIV storage systems. +""" + +from cinder import exception +from cinder import flags +from cinder.openstack.common import cfg +from cinder.openstack.common import importutils +from cinder.openstack.common import log as logging +from cinder.volume import san + +ibm_xiv_opts = [ + cfg.StrOpt('xiv_proxy', + default='xiv_openstack.nova_proxy.XIVNovaProxy', + help='Proxy driver'), +] + +FLAGS = flags.FLAGS +FLAGS.register_opts(ibm_xiv_opts) + +LOG = logging.getLogger('nova.volume.xiv') + + +class XIVDriver(san.SanISCSIDriver): + """IBM XIV volume driver.""" + + def __init__(self, *args, **kwargs): + """Initialize the driver.""" + + proxy = importutils.import_class(FLAGS.xiv_proxy) + + self.xiv_proxy = proxy({ + "xiv_user": FLAGS.san_login, + "xiv_pass": FLAGS.san_password, + "xiv_address": FLAGS.san_ip, + "xiv_vol_pool": FLAGS.san_clustername + }, + LOG, + exception) + san.SanISCSIDriver.__init__(self, *args, **kwargs) + + def do_setup(self, context): + """Setup and verify IBM XIV storage connection.""" + + self.xiv_proxy.setup(context) + + def ensure_export(self, context, volume): + """Ensure an export.""" + + return self.xiv_proxy.ensure_export(context, volume) + + def create_export(self, context, volume): + """Create an export.""" + + return self.xiv_proxy.create_export(context, volume) + + def create_volume(self, volume): + """Create a volume on the IBM XIV storage system.""" + + return self.xiv_proxy.create_volume(volume) + + def delete_volume(self, volume): + """Delete a volume on the IBM XIV storage system.""" + + self.xiv_proxy.delete_volume(volume) + + def remove_export(self, context, volume): + """Disconnect a volume from an attached instance.""" + + return self.xiv_proxy.remove_export(context, volume) + + def initialize_connection(self, volume, connector): + """Map the created volume.""" + + return self.xiv_proxy.initialize_connection( + volume, + connector) + + def terminate_connection(self, volume, connector): + """Terminate a connection to a volume.""" + + return self.xiv_proxy.terminate_connection( + volume, + connector) + + def create_volume_from_snapshot(self, volume, snapshot): + """Create a volume from a snapshot.""" + + return self.xiv_proxy.create_volume_from_snapshot( + volume, + snapshot) + + def create_snapshot(self, snapshot): + """Create a snapshot.""" + + return self.xiv_proxy.create_snapshot(snapshot) + + def delete_snapshot(self, snapshot): + """Delete a snapshot.""" + + return self.xiv_proxy.delete_snapshot(snapshot) + + def get_volume_stats(self, refresh=False): + """Get volume stats.""" + + return self.xiv_proxy.get_volume_stats(refresh) -- 2.45.2