From d14ac4bf38f23e429572f210f7b0560493968b15 Mon Sep 17 00:00:00 2001 From: Nirmal Ranganathan Date: Tue, 1 May 2012 14:02:16 -0500 Subject: Adding 'host' info to volume-compute connection information. Added a new key 'host' for the initialize/terminate volume connection information. Updated the HpSanISCSIDriver to use teh initialize and terminate methods. Added missing unit tests for the HpSanISCSIDriver. Fixes bug 992729. Change-Id: I09fad6b5328cbfb860ab2c7ae5d206010f3dd45d --- nova/tests/test_libvirt.py | 8 +- nova/tests/test_virt_drivers.py | 1 + nova/tests/volume/__init__.py | 13 ++ nova/tests/volume/test_HpSanISCSIDriver.py | 212 +++++++++++++++++++++++++++++ nova/virt/driver.py | 5 +- nova/virt/fake.py | 2 +- nova/virt/libvirt/connection.py | 1 + nova/virt/vmwareapi_conn.py | 5 +- nova/virt/xenapi/connection.py | 11 +- nova/volume/san.py | 89 ++++++------ 10 files changed, 286 insertions(+), 61 deletions(-) create mode 100644 nova/tests/volume/__init__.py create mode 100644 nova/tests/volume/test_HpSanISCSIDriver.py (limited to 'nova') diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py index 9a459545c..ee17abc4f 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -142,7 +142,8 @@ class LibvirtVolumeTestCase(test.TestCase): self.fake_conn = FakeLibvirtConnection() self.connr = { 'ip': '127.0.0.1', - 'initiator': 'fake_initiator' + 'initiator': 'fake_initiator', + 'host': 'fake_host' } def test_libvirt_iscsi_driver(self): @@ -478,12 +479,15 @@ class LibvirtConnTestCase(test.TestCase): def test_get_connector(self): initiator = 'fake.initiator.iqn' ip = 'fakeip' + host = 'fakehost' self.flags(my_ip=ip) + self.flags(host=host) conn = connection.LibvirtConnection(True) expected = { 'ip': ip, - 'initiator': initiator + 'initiator': initiator, + 'host': host } volume = { 'id': 'fake' diff --git a/nova/tests/test_virt_drivers.py b/nova/tests/test_virt_drivers.py index 592926597..f8ed0f2ea 100644 --- a/nova/tests/test_virt_drivers.py +++ b/nova/tests/test_virt_drivers.py @@ -231,6 +231,7 @@ class _VirtDriverTestCase(test.TestCase): result = self.connection.get_volume_connector({'id': 'fake'}) self.assertTrue('ip' in result) self.assertTrue('initiator' in result) + self.assertTrue('host' in result) @catch_notimplementederror def test_attach_detach_volume(self): diff --git a/nova/tests/volume/__init__.py b/nova/tests/volume/__init__.py new file mode 100644 index 000000000..4549abf92 --- /dev/null +++ b/nova/tests/volume/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2012 OpenStack LLC +# +# 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. diff --git a/nova/tests/volume/test_HpSanISCSIDriver.py b/nova/tests/volume/test_HpSanISCSIDriver.py new file mode 100644 index 000000000..467c5d728 --- /dev/null +++ b/nova/tests/volume/test_HpSanISCSIDriver.py @@ -0,0 +1,212 @@ +# Copyright 2012 OpenStack LLC +# +# 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. +from nova import exception +from nova import log as logging +from nova.volume import san +from nova import test + +LOG = logging.getLogger(__name__) + + +class HpSanISCSITestCase(test.TestCase): + + def setUp(self): + super(HpSanISCSITestCase, self).setUp() + self.stubs.Set(san.HpSanISCSIDriver, "_cliq_run", + self._fake_cliq_run) + self.stubs.Set(san.HpSanISCSIDriver, "_get_iscsi_properties", + self._fake_get_iscsi_properties) + self.driver = san.HpSanISCSIDriver() + self.volume_name = "fakevolume" + self.connector = {'ip': '10.0.0.2', + 'initiator': 'iqn.1993-08.org.debian:01:222', + 'host': 'fakehost'} + self.properties = {'target_discoverd': True, + 'target_portal': '10.0.1.6:3260', + 'target_iqn': + 'iqn.2003-10.com.lefthandnetworks:group01:25366:fakev', + 'volume_id': 1} + + def tearDown(self): + super(HpSanISCSITestCase, self).tearDown() + + def _fake_get_iscsi_properties(self, volume): + return self.properties + + def _fake_cliq_run(self, verb, cliq_args): + """Return fake results for the various methods.""" + + def create_volume(cliq_args): + """ + input = "createVolume description="fake description" + clusterName=Cluster01 volumeName=fakevolume + thinProvision=0 output=XML size=1GB" + """ + output = """ + + """ + self.assertEqual(cliq_args['volumeName'], self.volume_name) + self.assertEqual(cliq_args['thinProvision'], '1') + self.assertEqual(cliq_args['size'], '1GB') + return output, None + + def delete_volume(cliq_args): + """ + input = "deleteVolume volumeName=fakevolume prompt=false + output=XML" + """ + output = """ + + """ + self.assertEqual(cliq_args['volumeName'], self.volume_name) + self.assertEqual(cliq_args['prompt'], 'false') + return output, None + + def assign_volume(cliq_args): + """ + input = "assignVolumeToServer volumeName=fakevolume + serverName=fakehost + output=XML" + """ + output = """ + + """ + self.assertEqual(cliq_args['volumeName'], self.volume_name) + self.assertEqual(cliq_args['serverName'], self.connector['host']) + return output, None + + def unassign_volume(cliq_args): + """ + input = "unassignVolumeToServer volumeName=fakevolume + serverName=fakehost output=XML + """ + output = """ + + """ + self.assertEqual(cliq_args['volumeName'], self.volume_name) + self.assertEqual(cliq_args['serverName'], self.connector['host']) + return output, None + + def get_cluster_info(cliq_args): + """ + input = "getClusterInfo clusterName=Cluster01 searchDepth=1 + verbose=0 output=XML" + """ + output = """ + + + + + + """ + return output, None + + def get_volume_info(cliq_args): + """ + input = "getVolumeInfo volumeName=fakevolume output=XML" + """ + output = """ + + + + + """ + return output, None + + def test_error(cliq_args): + output = """ + + """ + return output, None + + self.assertEqual(cliq_args['output'], 'XML') + try: + verbs = {'createVolume': create_volume, + 'deleteVolume': delete_volume, + 'assignVolumeToServer': assign_volume, + 'unassignVolumeToServer': unassign_volume, + 'getClusterInfo': get_cluster_info, + 'getVolumeInfo': get_volume_info, + 'testError': test_error} + except KeyError: + raise NotImplementedError() + + return verbs[verb](cliq_args) + + def test_create_volume(self): + volume = {'name': self.volume_name, 'size': 1} + model_update = self.driver.create_volume(volume) + expected_iqn = "iqn.2003-10.com.lefthandnetworks:group01:25366:fakev" + expected_location = "10.0.1.6:3260,1 %s" % expected_iqn + self.assertEqual(model_update['provider_location'], expected_location) + + def test_delete_volume(self): + volume = {'name': self.volume_name} + self.driver.delete_volume(volume) + + def test_initialize_connection(self): + volume = {'name': self.volume_name} + result = self.driver.initialize_connection(volume, self.connector) + self.assertEqual(result['driver_volume_type'], 'iscsi') + self.assertDictMatch(result['data'], self.properties) + + def test_terminate_connection(self): + volume = {'name': self.volume_name} + self.driver.terminate_connection(volume, self.connector) + + def test_create_snapshot(self): + try: + self.driver.create_snapshot("") + except NotImplementedError: + pass + + def test_create_volume_from_snapshot(self): + try: + self.driver.create_volume_from_snapshot("", "") + except NotImplementedError: + pass + + def test_cliq_error(self): + try: + self.driver._cliq_run_xml("testError", {}) + except exception.Error: + pass diff --git a/nova/virt/driver.py b/nova/virt/driver.py index db004de80..68cbea340 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -676,12 +676,13 @@ class ComputeDriver(object): """Get connector information for the instance for attaching to volumes. Connector information is a dictionary representing the ip of the - machine that will be making the connection and and the name of the - iscsi initiator as follows:: + machine that will be making the connection, the name of the iscsi + initiator and the hostname of the machine as follows:: { 'ip': ip, 'initiator': initiator, + 'host': hostname } """ raise NotImplementedError() diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 3b0dbfd4c..b08c7661d 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -335,4 +335,4 @@ class FakeConnection(driver.ComputeDriver): pass def get_volume_connector(self, instance): - return {'ip': '127.0.0.1', 'initiator': 'fake'} + return {'ip': '127.0.0.1', 'initiator': 'fake', 'host': 'fakehost'} diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 42b6cbdbd..44e0bd09b 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -488,6 +488,7 @@ class LibvirtConnection(driver.ComputeDriver): return { 'ip': FLAGS.my_ip, 'initiator': self._initiator, + 'host': FLAGS.host } def _cleanup_resize(self, instance): diff --git a/nova/virt/vmwareapi_conn.py b/nova/virt/vmwareapi_conn.py index 444fc785a..a5f771eee 100644 --- a/nova/virt/vmwareapi_conn.py +++ b/nova/virt/vmwareapi_conn.py @@ -177,10 +177,11 @@ class VMWareESXConnection(driver.ComputeDriver): def get_volume_connector(self, _instance): """Return volume connector information""" # TODO(vish): When volume attaching is supported, return the - # proper initiator iqn. + # proper initiator iqn and host. return { 'ip': FLAGS.vmwareapi_host_ip, - 'initiator': None + 'initiator': None, + 'host': None } def attach_volume(self, connection_info, instance_name, mountpoint): diff --git a/nova/virt/xenapi/connection.py b/nova/virt/xenapi/connection.py index 2fa5516d3..c02f3581d 100644 --- a/nova/virt/xenapi/connection.py +++ b/nova/virt/xenapi/connection.py @@ -156,6 +156,7 @@ class XenAPIConnection(driver.ComputeDriver): self._host = host.Host(self._session) self._vmops = vmops.VMOps(self._session) self._initiator = None + self._hypervisor_hostname = None self._pool = pool.ResourcePool(self._session) @property @@ -336,17 +337,19 @@ class XenAPIConnection(driver.ComputeDriver): def get_volume_connector(self, instance): """Return volume connector information""" - if not self._initiator: + if not self._initiator or not self._hypervisor_hostname: stats = self.get_host_stats(refresh=True) try: self._initiator = stats['host_other-config']['iscsi_iqn'] - except (TypeError, KeyError): - LOG.warn(_('Could not determine iscsi initiator name'), + self._hypervisor_hostname = stats['host_hostname'] + except (TypeError, KeyError) as err: + LOG.warn(_('Could not determine key: %s') % err, instance=instance) self._initiator = None return { 'ip': self.get_host_ip_addr(), - 'initiator': self._initiator + 'initiator': self._initiator, + 'host': self._hypervisor_hostname } @staticmethod diff --git a/nova/volume/san.py b/nova/volume/san.py index aecb9a0b5..9d83864d0 100644 --- a/nova/volume/san.py +++ b/nova/volume/san.py @@ -582,6 +582,14 @@ class HpSanISCSIDriver(SanISCSIDriver): return model_update + def create_volume_from_snapshot(self, volume, snapshot): + """Creates a volume from a snapshot.""" + raise NotImplementedError() + + def create_snapshot(self, snapshot): + """Creates a snapshot.""" + raise NotImplementedError() + def delete_volume(self, volume): """Deletes a volume.""" cliq_args = {} @@ -594,64 +602,45 @@ class HpSanISCSIDriver(SanISCSIDriver): # TODO(justinsb): Is this needed here? raise exception.Error(_("local_path not supported")) - def ensure_export(self, context, volume): - """Synchronously recreates an export for a logical volume.""" - return self._do_export(context, volume, force_create=False) - - def create_export(self, context, volume): - return self._do_export(context, volume, force_create=True) + def initialize_connection(self, volume, connector): + """Assigns the volume to a server. - def _do_export(self, context, volume, force_create): - """Supports ensure_export and create_export""" - volume_info = self._cliq_get_volume_info(volume['name']) + Assign any created volume to a compute node/host so that it can be + used from that host. HP VSA requires a volume to be assigned + to a server. - is_shared = 'permission.authGroup' in volume_info + This driver returns a driver_volume_type of 'iscsi'. + The format of the driver data is defined in _get_iscsi_properties. + Example return value:: - model_update = {} + { + 'driver_volume_type': 'iscsi' + 'data': { + 'target_discovered': True, + 'target_iqn': 'iqn.2010-10.org.openstack:volume-00000001', + 'target_portal': '127.0.0.0.1:3260', + 'volume_id': 1, + } + } - should_export = False - - if force_create or not is_shared: - should_export = True - # Check that we have a project_id - project_id = volume['project_id'] - if not project_id: - project_id = context.project_id - - if project_id: - #TODO(justinsb): Use a real per-project password here - chap_username = 'proj_' + project_id - # HP/Lefthand requires that the password be >= 12 characters - chap_password = 'project_secret_' + project_id - else: - msg = (_("Could not determine project for volume %s, " - "can't export") % - (volume['name'])) - if force_create: - raise exception.Error(msg) - else: - LOG.warn(msg) - should_export = False - - if should_export: - cliq_args = {} - cliq_args['volumeName'] = volume['name'] - cliq_args['chapName'] = chap_username - cliq_args['targetSecret'] = chap_password - - self._cliq_run_xml("assignVolumeChap", cliq_args) - - model_update['provider_auth'] = ("CHAP %s %s" % - (chap_username, chap_password)) + """ + cliq_args = {} + cliq_args['volumeName'] = volume['name'] + cliq_args['serverName'] = connector['host'] + self._cliq_run_xml("assignVolumeToServer", cliq_args) - return model_update + iscsi_properties = self._get_iscsi_properties(volume) + return { + 'driver_volume_type': 'iscsi', + 'data': iscsi_properties + } - def remove_export(self, context, volume): - """Removes an export for a logical volume.""" + def terminate_connection(self, volume, connector): + """Unassign the volume from the host.""" cliq_args = {} cliq_args['volumeName'] = volume['name'] - - self._cliq_run_xml("unassignVolume", cliq_args) + cliq_args['serverName'] = connector['host'] + self._cliq_run_xml("unassignVolumeToServer", cliq_args) class SolidFireSanISCSIDriver(SanISCSIDriver): -- cgit