From 83e2053567eeb5649b0af8826b71b50517e17f47 Mon Sep 17 00:00:00 2001 From: Kurt Martin Date: Wed, 13 Feb 2013 13:55:43 -0800 Subject: [PATCH] HP 3PAR Fibre Channel Driver and iSCSI Updates This is the initial FC driver for the HP 3PAR array, along with some 3PAR iSCSI driver cleanup and changes in the HP 3PAR common class. Summary of changes: * Initial FC Driver for the Grizzly release (depends on the FC base class and nova FC changes https://review.openstack.org/#/c/19992/ & https://review.openstack.org/#/c/20003/) * Updates to iSCSI Driver include volume type and create clone volume support as well as general cleanup(comments, copyright, minor bug fixes). * The tests have been updated to add FC test and the file was renamed to reflect both FC and iSCSI 3PAR tests. * Volume types added for the Grizzly release include cpg, snap_cpg, persona and provisioning(thin vs. full) Partially Implements: blueprint fibre-channel-block-storage Change-Id: Ie1c2e755aa1bcd5994ea4b02674718721b958e07 --- .../{test_hp3par_iscsi.py => test_hp3par.py} | 409 ++++++++++++------ .../volume/drivers/san/hp/hp_3par_common.py | 190 +++++++- cinder/volume/drivers/san/hp/hp_3par_fc.py | 241 +++++++++++ cinder/volume/drivers/san/hp/hp_3par_iscsi.py | 40 +- 4 files changed, 721 insertions(+), 159 deletions(-) rename cinder/tests/{test_hp3par_iscsi.py => test_hp3par.py} (62%) create mode 100644 cinder/volume/drivers/san/hp/hp_3par_fc.py diff --git a/cinder/tests/test_hp3par_iscsi.py b/cinder/tests/test_hp3par.py similarity index 62% rename from cinder/tests/test_hp3par_iscsi.py rename to cinder/tests/test_hp3par.py index d1c0ae03b..1e51da049 100644 --- a/cinder/tests/test_hp3par_iscsi.py +++ b/cinder/tests/test_hp3par.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # vim: tabstop=4 shiftwidth=4 softtabstop=4 # -# Copyright (c) 2012 Hewlett-Packard, Inc. +# (c) Copyright 2013 Hewlett-Packard Development Company, L.P. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -16,16 +16,18 @@ # License for the specific language governing permissions and limitations # under the License. """ -Unit tests for OpenStack Cinder volume driver +Unit tests for OpenStack Cinder volume drivers """ import shutil import tempfile from hp3parclient import exceptions as hpexceptions +from cinder import exception import cinder.flags from cinder.openstack.common import log as logging from cinder import test +from cinder.volume.drivers.san.hp import hp_3par_fc as hpfcdriver from cinder.volume.drivers.san.hp import hp_3par_iscsi as hpdriver FLAGS = cinder.flags.FLAGS @@ -54,8 +56,7 @@ class FakeHP3ParClient(object): 'usedMiB': 256}, 'SDGrowth': {'LDLayout': {'RAIDType': 4, 'diskPatterns': [{'diskType': 2}]}, - 'incrementMiB': 32768, - 'limitMiB': 1024000}, + 'incrementMiB': 32768}, 'SDUsage': {'rawTotalMiB': 49152, 'rawUsedMiB': 1023, 'totalMiB': 36864, @@ -249,22 +250,258 @@ class FakeHP3ParClient(object): raise hpexceptions.HTTPNotFound(msg) -class TestHP3PARDriver(test.TestCase): +class HP3PARBaseDriver(): - TARGET_IQN = "iqn.2000-05.com.3pardata:21810002ac00383d" VOLUME_ID = "d03338a9-9115-48a3-8dfc-35cdfcdc15a7" + CLONE_ID = "d03338a9-9115-48a3-8dfc-000000000000" VOLUME_NAME = "volume-d03338a9-9115-48a3-8dfc-35cdfcdc15a7" SNAPSHOT_ID = "2f823bdc-e36e-4dc8-bd15-de1c7a28ff31" SNAPSHOT_NAME = "snapshot-2f823bdc-e36e-4dc8-bd15-de1c7a28ff31" VOLUME_3PAR_NAME = "osv-0DM4qZEVSKON-DXN-NwVpw" SNAPSHOT_3PAR_NAME = "oss-L4I73ONuTci9Fd4ceij-MQ" FAKE_HOST = "fakehost" + USER_ID = '2689d9a913974c008b1d859013f23607' + PROJECT_ID = 'fac88235b9d64685a3530f73e490348f' + VOLUME_ID_SNAP = '761fc5e5-5191-4ec7-aeba-33e36de44156' + FAKE_DESC = 'test description name' + FAKE_FC_PORTS = ['0987654321234', '123456789000987'] + FAKE_ISCSI_PORTS = ['10.10.10.10', '10.10.10.11'] + + volume = {'name': VOLUME_NAME, + 'id': VOLUME_ID, + 'display_name': 'Foo Volume', + 'size': 2, + 'host': FAKE_HOST, + 'volume_type': None, + 'volume_type_id': None} + + snapshot = {'name': SNAPSHOT_NAME, + 'id': SNAPSHOT_ID, + 'user_id': USER_ID, + 'project_id': PROJECT_ID, + 'volume_id': VOLUME_ID_SNAP, + 'volume_name': VOLUME_NAME, + 'status': 'creating', + 'progress': '0%', + 'volume_size': 2, + 'display_name': 'fakesnap', + 'display_description': FAKE_DESC} + + connector = {'ip': '10.0.0.2', + 'initiator': 'iqn.1993-08.org.debian:01:222', + 'wwpns': ["123456789012345", "123456789054321"], + 'wwnns': ["223456789012345", "223456789054321"], + 'host': 'fakehost'} + + def fake_create_client(self): + return FakeHP3ParClient(FLAGS.hp3par_api_url) + + def fake_get_3par_host(self, hostname): + if hostname not in self._hosts: + msg = {'code': 'NON_EXISTENT_HOST', + 'desc': "HOST '%s' was not found" % hostname} + raise hpexceptions.HTTPNotFound(msg) + else: + return self._hosts[hostname] + + def fake_delete_3par_host(self, hostname): + if hostname not in self._hosts: + msg = {'code': 'NON_EXISTENT_HOST', + 'desc': "HOST '%s' was not found" % hostname} + raise hpexceptions.HTTPNotFound(msg) + else: + self._hosts[hostname] = None + + def fake_create_3par_vlun(self, volume, hostname): + self.driver.client.createVLUN(volume, 19, hostname) + + def fake_get_ports(self): + return {'FC': self.FAKE_FC_PORTS, 'iSCSI': self.FAKE_ISCSI_PORTS} + + def fake_copy_volume(self, src_name, dest_name): + pass + + def fake_get_volume_state(self, vol_name): + return "normal" + + def test_delete_volume(self): + self.flags(lock_path=self.tempdir) + self.driver.delete_volume(self.volume) + self.assertRaises(hpexceptions.HTTPNotFound, + self.driver.client.getVolume, + self.VOLUME_ID) + + def test_create_snapshot(self): + self.flags(lock_path=self.tempdir) + self.driver.create_snapshot(self.snapshot) + + # check to see if the snapshot was created + snap_vol = self.driver.client.getVolume(self.SNAPSHOT_3PAR_NAME) + self.assertEqual(snap_vol['name'], self.SNAPSHOT_3PAR_NAME) + + def test_delete_snapshot(self): + self.flags(lock_path=self.tempdir) + self.driver.delete_snapshot(self.snapshot) + + # the snapshot should be deleted now + self.assertRaises(hpexceptions.HTTPNotFound, + self.driver.client.getVolume, + self.SNAPSHOT_3PAR_NAME) + + def test_create_volume_from_snapshot(self): + self.flags(lock_path=self.tempdir) + self.driver.create_volume_from_snapshot(self.volume, self.snapshot) + + snap_vol = self.driver.client.getVolume(self.SNAPSHOT_3PAR_NAME) + self.assertEqual(snap_vol['name'], self.SNAPSHOT_3PAR_NAME) + + volume = self.volume.copy() + volume['size'] = 1 + self.assertRaises(exception.InvalidInput, + self.driver.create_volume_from_snapshot, + volume, self.snapshot) + + def test_terminate_connection(self): + self.flags(lock_path=self.tempdir) + self.driver.terminate_connection(self.volume, self.connector, True) + # vlun should be gone. + self.assertRaises(hpexceptions.HTTPNotFound, + self.driver.client.getVLUN, + self.VOLUME_3PAR_NAME) + + +class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase): + + _hosts = {} + + def setUp(self): + self.tempdir = tempfile.mkdtemp() + super(TestHP3PARFCDriver, self).setUp() + self.flags( + hp3par_username='testUser', + hp3par_password='testPassword', + hp3par_api_url='https://1.1.1.1/api/v1', + hp3par_domain=HP3PAR_DOMAIN, + hp3par_cpg=HP3PAR_CPG, + hp3par_cpg_snap=HP3PAR_CPG_SNAP, + iscsi_ip_address='1.1.1.2', + iscsi_port='1234', + san_ip='2.2.2.2', + san_login='test', + san_password='test' + ) + self.stubs.Set(hpfcdriver.HP3PARFCDriver, "_create_client", + self.fake_create_client) + self.stubs.Set(hpfcdriver.HP3PARFCDriver, + "_create_3par_fibrechan_host", + self.fake_create_3par_fibrechan_host) + + self.stubs.Set(hpfcdriver.HP3PARCommon, "_get_3par_host", + self.fake_get_3par_host) + self.stubs.Set(hpfcdriver.HP3PARCommon, "_delete_3par_host", + self.fake_delete_3par_host) + self.stubs.Set(hpdriver.HP3PARCommon, "_create_3par_vlun", + self.fake_create_3par_vlun) + self.stubs.Set(hpdriver.HP3PARCommon, "get_ports", + self.fake_get_ports) + + self.driver = hpfcdriver.HP3PARFCDriver() + self.driver.do_setup(None) + + def tearDown(self): + shutil.rmtree(self.tempdir) + super(TestHP3PARFCDriver, self).tearDown() + + def fake_create_3par_fibrechan_host(self, hostname, wwn, + domain, persona_id): + host = {'FCPaths': [{'driverVersion': None, + 'firmwareVersion': None, + 'hostSpeed': 0, + 'model': None, + 'portPos': {'cardPort': 1, 'node': 1, + 'slot': 2}, + 'vendor': None, + 'wwn': wwn[0]}, + {'driverVersion': None, + 'firmwareVersion': None, + 'hostSpeed': 0, + 'model': None, + 'portPos': {'cardPort': 1, 'node': 0, + 'slot': 2}, + 'vendor': None, + 'wwn': wwn[1]}], + 'descriptors': None, + 'domain': domain, + 'iSCSIPaths': [], + 'id': 11, + 'name': hostname} + self._hosts[hostname] = host + + self.properties = {'data': + {'target_discovered': True, + 'target_lun': 186, + 'target_portal': '1.1.1.2:1234'}, + 'driver_volume_type': 'fibre_channel'} + + def test_create_volume(self): + self.flags(lock_path=self.tempdir) + model_update = self.driver.create_volume(self.volume) + metadata = model_update['metadata'] + self.assertFalse(metadata['3ParName'] is None) + self.assertEqual(metadata['CPG'], HP3PAR_CPG) + self.assertEqual(metadata['snapCPG'], HP3PAR_CPG_SNAP) + + def test_initialize_connection(self): + self.flags(lock_path=self.tempdir) + result = self.driver.initialize_connection(self.volume, self.connector) + self.assertEqual(result['driver_volume_type'], 'fibre_channel') + + # we should have a host and a vlun now. + host = self.fake_get_3par_host(self.FAKE_HOST) + self.assertEquals(self.FAKE_HOST, host['name']) + self.assertEquals(HP3PAR_DOMAIN, host['domain']) + vlun = self.driver.client.getVLUN(self.VOLUME_3PAR_NAME) + + self.assertEquals(self.VOLUME_3PAR_NAME, vlun['volumeName']) + self.assertEquals(self.FAKE_HOST, vlun['hostname']) + + def test_create_cloned_volume(self): + self.flags(lock_path=self.tempdir) + self.stubs.Set(hpdriver.HP3PARCommon, "_get_volume_state", + self.fake_get_volume_state) + self.stubs.Set(hpdriver.HP3PARCommon, "_copy_volume", + self.fake_copy_volume) + self.state_tries = 0 + volume = {'name': HP3PARBaseDriver.VOLUME_NAME, + 'id': HP3PARBaseDriver.CLONE_ID, + 'display_name': 'Foo Volume', + 'size': 2, + 'host': HP3PARBaseDriver.FAKE_HOST, + 'source_volid': HP3PARBaseDriver.VOLUME_ID} + src_vref = {} + model_update = self.driver.create_cloned_volume(volume, src_vref) + self.assertTrue(model_update is not None) + metadata = model_update['metadata'] + self.assertFalse(metadata['3ParName'] is None) + self.assertEqual(metadata['CPG'], HP3PAR_CPG) + self.assertEqual(metadata['snapCPG'], HP3PAR_CPG_SNAP) + + def test_get_volume_stats(self): + self.flags(lock_path=self.tempdir) + stats = self.driver.get_volume_stats(True) + self.assertEquals(stats['storage_protocol'], 'FC') + self.assertEquals(stats['volume_backend_name'], 'HP3PARFCDriver') + + +class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase): + + TARGET_IQN = "iqn.2000-05.com.3pardata:21810002ac00383d" _hosts = {} def setUp(self): self.tempdir = tempfile.mkdtemp() - super(TestHP3PARDriver, self).setUp() + super(TestHP3PARISCSIDriver, self).setUp() self.flags( hp3par_username='testUser', hp3par_password='testPassword', @@ -295,65 +532,27 @@ class TestHP3PARDriver(test.TestCase): self.fake_delete_3par_host) self.stubs.Set(hpdriver.HP3PARCommon, "_create_3par_vlun", self.fake_create_3par_vlun) - self.stubs.Set(hpdriver.HP3PARCommon, "get_ports", - self.fake_get_ports) self.driver = hpdriver.HP3PARISCSIDriver() self.driver.do_setup(None) - self.volume = {'name': self.VOLUME_NAME, - 'id': self.VOLUME_ID, - 'display_name': 'Foo Volume', - 'size': 2, - 'host': self.FAKE_HOST} - - user_id = '2689d9a913974c008b1d859013f23607' - project_id = 'fac88235b9d64685a3530f73e490348f' - volume_id = '761fc5e5-5191-4ec7-aeba-33e36de44156' - fake_desc = 'test description name' - fake_fc_ports = ['0987654321234', '123456789000987'] - fake_iscsi_ports = ['10.10.10.10', '10.10.10.11'] - self.snapshot = {'name': self.SNAPSHOT_NAME, - 'id': self.SNAPSHOT_ID, - 'user_id': user_id, - 'project_id': project_id, - 'volume_id': volume_id, - 'volume_name': self.VOLUME_NAME, - 'status': 'creating', - 'progress': '0%', - 'volume_size': 2, - 'display_name': 'fakesnap', - 'display_description': fake_desc} - self.connector = {'ip': '10.0.0.2', - 'initiator': 'iqn.1993-08.org.debian:01:222', - 'host': 'fakehost'} - target_iqn = 'iqn.2000-05.com.3pardata:21810002ac00383d' self.properties = {'data': - {'target_discovered': True, - 'target_iqn': target_iqn, - 'target_lun': 186, - 'target_portal': '1.1.1.2:1234'}, + {'target_discovered': True, + 'target_iqn': target_iqn, + 'target_lun': 186, + 'target_portal': '1.1.1.2:1234'}, 'driver_volume_type': 'iscsi'} - self.stats = {'driver_version': '1.0', - 'free_capacity_gb': 968, - 'reserved_percentage': 0, - 'storage_protocol': 'iSCSI', - 'total_capacity_gb': 1000, - 'vendor_name': 'Hewlett-Packard', - 'volume_backend_name': 'HP3PARISCSIDriver'} def tearDown(self): shutil.rmtree(self.tempdir) - super(TestHP3PARDriver, self).tearDown() - - def fake_create_client(self): - return FakeHP3ParClient(FLAGS.hp3par_api_url) + super(TestHP3PARISCSIDriver, self).tearDown() def fake_iscsi_discover_target_iqn(self, ip_address): return self.TARGET_IQN - def fake_create_3par_iscsi_host(self, hostname, iscsi_iqn, domain): + def fake_create_3par_iscsi_host(self, hostname, iscsi_iqn, + domain, persona_id): host = {'FCPaths': [], 'descriptors': None, 'domain': domain, @@ -370,82 +569,20 @@ class TestHP3PARDriver(test.TestCase): 'name': hostname} self._hosts[hostname] = host - def fake_get_3par_host(self, hostname): - if hostname not in self._hosts: - msg = {'code': 'NON_EXISTENT_HOST', - 'desc': "HOST '%s' was not found" % hostname} - raise hpexceptions.HTTPNotFound(msg) - else: - return self._hosts[hostname] - - def fake_delete_3par_host(self, hostname): - if hostname not in self._hosts: - msg = {'code': 'NON_EXISTENT_HOST', - 'desc': "HOST '%s' was not found" % hostname} - raise hpexceptions.HTTPNotFound(msg) - else: - self._hosts[hostname] = None - - def fake_create_3par_vlun(self, volume, hostname): - self.driver.client.createVLUN(volume, 19, hostname) - - def fake_get_ports(self): - return {'FC': self.fake_fc_ports, 'iSCSI': self.fake_iscsi_ports} + def fake_iscsi_discover_target_iqn(self, remote_ip): + return 'iqn.2000-05.com.3pardata:21810002ac00383d' def test_create_volume(self): self.flags(lock_path=self.tempdir) model_update = self.driver.create_volume(self.volume) + metadata = model_update['metadata'] + self.assertFalse(metadata['3ParName'] is None) + self.assertEqual(metadata['CPG'], HP3PAR_CPG) + self.assertEqual(metadata['snapCPG'], HP3PAR_CPG_SNAP) expected_location = "%s:%s" % (FLAGS.iscsi_ip_address, FLAGS.iscsi_port) self.assertEqual(model_update['provider_location'], expected_location) - def test_delete_volume(self): - self.flags(lock_path=self.tempdir) - self.driver.delete_volume(self.volume) - self.assertRaises(hpexceptions.HTTPNotFound, - self.driver.client.getVolume, - self.VOLUME_NAME) - - def test_get_volume_stats(self): - self.flags(lock_path=self.tempdir) - vol_stats = self.driver.get_volume_stats(True) - self.assertEqual(vol_stats['driver_version'], - self.stats['driver_version']) - self.assertEqual(vol_stats['free_capacity_gb'], - self.stats['free_capacity_gb']) - self.assertEqual(vol_stats['reserved_percentage'], - self.stats['reserved_percentage']) - self.assertEqual(vol_stats['storage_protocol'], - self.stats['storage_protocol']) - self.assertEqual(vol_stats['vendor_name'], - self.stats['vendor_name']) - self.assertEqual(vol_stats['volume_backend_name'], - self.stats['volume_backend_name']) - - def test_create_snapshot(self): - self.flags(lock_path=self.tempdir) - self.driver.create_snapshot(self.snapshot) - - # check to see if the snapshot was created - snap_vol = self.driver.client.getVolume(self.SNAPSHOT_3PAR_NAME) - self.assertEqual(snap_vol['name'], self.SNAPSHOT_3PAR_NAME) - - def test_delete_snapshot(self): - self.flags(lock_path=self.tempdir) - self.driver.delete_snapshot(self.snapshot) - - # the snapshot should be deleted now - self.assertRaises(hpexceptions.HTTPNotFound, - self.driver.client.getVolume, - self.SNAPSHOT_NAME) - - def test_create_volume_from_snapshot(self): - self.flags(lock_path=self.tempdir) - self.driver.create_volume_from_snapshot(self.volume, self.snapshot) - - snap_vol = self.driver.client.getVolume(self.SNAPSHOT_3PAR_NAME) - self.assertEqual(snap_vol['name'], self.SNAPSHOT_3PAR_NAME) - def test_initialize_connection(self): self.flags(lock_path=self.tempdir) result = self.driver.initialize_connection(self.volume, self.connector) @@ -466,11 +603,29 @@ class TestHP3PARDriver(test.TestCase): self.assertEquals(self.VOLUME_3PAR_NAME, vlun['volumeName']) self.assertEquals(self.FAKE_HOST, vlun['hostname']) - def test_terminate_connection(self): + def test_create_cloned_volume(self): self.flags(lock_path=self.tempdir) - self.driver.terminate_connection(self.volume, - self.connector, True) - # vlun should be gone. - self.assertRaises(hpexceptions.HTTPNotFound, - self.driver.client.getVLUN, - self.VOLUME_3PAR_NAME) + self.stubs.Set(hpdriver.HP3PARCommon, "_get_volume_state", + self.fake_get_volume_state) + self.stubs.Set(hpdriver.HP3PARCommon, "_copy_volume", + self.fake_copy_volume) + self.state_tries = 0 + volume = {'name': HP3PARBaseDriver.VOLUME_NAME, + 'id': HP3PARBaseDriver.CLONE_ID, + 'display_name': 'Foo Volume', + 'size': 2, + 'host': HP3PARBaseDriver.FAKE_HOST, + 'source_volid': HP3PARBaseDriver.VOLUME_ID} + src_vref = {} + model_update = self.driver.create_cloned_volume(volume, src_vref) + self.assertTrue(model_update is not None) + metadata = model_update['metadata'] + self.assertFalse(metadata['3ParName'] is None) + self.assertEqual(metadata['CPG'], HP3PAR_CPG) + self.assertEqual(metadata['snapCPG'], HP3PAR_CPG_SNAP) + + def test_get_volume_stats(self): + self.flags(lock_path=self.tempdir) + stats = self.driver.get_volume_stats(True) + self.assertEquals(stats['storage_protocol'], 'iSCSI') + self.assertEquals(stats['volume_backend_name'], 'HP3PARISCSIDriver') diff --git a/cinder/volume/drivers/san/hp/hp_3par_common.py b/cinder/volume/drivers/san/hp/hp_3par_common.py index e981f7558..7203f299a 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_common.py +++ b/cinder/volume/drivers/san/hp/hp_3par_common.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # -# Copyright (c) 2012 Hewlett-Packard, Inc. +# (c) Copyright 2012-2013 Hewlett-Packard Development Company, L.P. # All Rights Reserved. # # Copyright 2012 OpenStack LLC @@ -19,14 +19,14 @@ # """ Volume driver common utilities for HP 3PAR Storage array -This driver requires 3.1.2 firmware on the 3PAR array. +The 3PAR drivers requires 3.1.2 firmware on the 3PAR array. -The driver uses both the REST service and the SSH +The drivers uses both the REST service and the SSH command line to correctly operate. Since the ssh credentials and the REST credentials can be different we need to have settings for both. -This driver requires the use of the san_ip, san_login, +The drivers requires the use of the san_ip, san_login, san_password settings for ssh connections into the 3PAR array. It also requires the setting of hp3par_api_url, hp3par_username, hp3par_password @@ -38,17 +38,20 @@ import json import paramiko import pprint from random import randint +import time import uuid from eventlet import greenthread from hp3parclient import exceptions as hpexceptions +from cinder import context from cinder import exception from cinder import flags from cinder.openstack.common import cfg from cinder.openstack.common import lockutils from cinder.openstack.common import log as logging from cinder import utils +from cinder.volume import volume_types LOG = logging.getLogger(__name__) @@ -102,6 +105,18 @@ class HP3PARCommon(): 'vendor_name': 'Hewlett-Packard', 'volume_backend_name': None} + # Valid values for volume type extra specs + # The first value in the list is the default value + valid_prov_values = ['thin', 'full'] + valid_persona_values = ['1 - Generic', + '2 - Generic-ALUA', + '6 - Generic-legacy', + '7 - HPUX-legacy', + '8 - AIX-legacy', + '9 - EGENERA', + '10 - ONTAP-legacy', + '11 - VMWare'] + def __init__(self): self.sshpool = None @@ -159,7 +174,7 @@ class HP3PARCommon(): return capacity def _cli_run(self, verb, cli_args): - """Runs a CLI command over SSH, without doing any result parsing""" + """ Runs a CLI command over SSH, without doing any result parsing. """ cli_arg_strings = [] if cli_args: for k, v in cli_args.items(): @@ -185,7 +200,7 @@ class HP3PARCommon(): from the CLI command. We first have to issue a command to tell the CLI that we want the output to be formatted in CSV, then we issue the real - command + command. """ LOG.debug(_('Running cmd (SSH): %s'), cmd) @@ -260,7 +275,7 @@ exit def _safe_hostname(self, hostname): """ We have to use a safe hostname length - for 3PAR host names + for 3PAR host names. """ try: index = hostname.index('.') @@ -376,8 +391,7 @@ exit if tmp: if tmp[1] == 'target' and tmp[2] == 'ready': if tmp[6] == 'FC': - port = {'wwn': tmp[4], 'nsp': tmp[0]} - ports['FC'].append(port) + ports['FC'].append(tmp[4]) # now get the active iSCSI ports out = self._cli_run('showport -iscsi', None) @@ -391,8 +405,7 @@ exit if tmp: if tmp[1] == 'ready': - port = {'ip': tmp[2], 'nsp': tmp[0]} - ports['iSCSI'].append(port) + ports['iSCSI'].append(tmp[2]) LOG.debug("PORTS = %s" % pprint.pformat(ports)) return ports @@ -438,9 +451,42 @@ exit client.deleteVLUN(volume_name, vlun['lun'], hostname) self._delete_3par_host(hostname) + def _get_volume_type(self, type_id): + ctxt = context.get_admin_context() + return volume_types.get_volume_type(ctxt, type_id) + + def _get_volume_type_value(self, volume_type, key, default=None): + if volume_type is not None: + specs = volume_type.get('extra_specs') + if key in specs: + return specs[key] + else: + return default + else: + return default + + def get_persona_type(self, volume): + default_persona = self.valid_persona_values[0] + type_id = volume.get('volume_type_id', None) + volume_type = None + if type_id is not None: + volume_type = self._get_volume_type(type_id) + persona_value = self._get_volume_type_value(volume_type, 'persona', + default_persona) + if persona_value not in self.valid_persona_values: + err = _("Must specify a valid persona %(valid)s, " + "value '%(persona)s' is invalid.") % \ + ({'valid': self.valid_persona_values, + 'persona': persona_value}) + raise exception.InvalidInput(reason=err) + # persona is set by the id so remove the text and return the id + # i.e for persona '1 - Generic' returns 1 + persona_id = persona_value.split(' ') + return persona_id[0] + @lockutils.synchronized('3par', 'cinder-', True) def create_volume(self, volume, client, FLAGS): - """ Create a new volume """ + """ Create a new volume. """ LOG.debug("CREATE VOLUME (%s : %s %s)" % (volume['display_name'], volume['name'], self._get_3par_vol_name(volume['id']))) @@ -453,16 +499,57 @@ exit if name: comments['display_name'] = name - extras = {'comment': json.dumps(comments), - 'snapCPG': FLAGS.hp3par_cpg_snap} + # get the options supported by volume types + volume_type = None + type_id = volume.get('volume_type_id', None) + if type_id is not None: + volume_type = self._get_volume_type(type_id) + + cpg = self._get_volume_type_value(volume_type, 'cpg', + FLAGS.hp3par_cpg) + + # if provisioning is not set use thin + default_prov = self.valid_prov_values[0] + prov_value = self._get_volume_type_value(volume_type, + 'provisioning', + default_prov) + # check for valid provisioning type + if prov_value not in self.valid_prov_values: + err = _("Must specify a valid provisioning type %(valid)s, " + "value '%(prov)s' is invalid.") % \ + ({'valid': self.valid_prov_values, + 'prov': prov_value}) + raise exception.InvalidInput(reason=err) + + ttpv = True + if prov_value == "full": + ttpv = False + + # default to hp3par_cpg if hp3par_cpg_snap is not set. + if FLAGS.hp3par_cpg_snap == "": + snap_default = FLAGS.hp3par_cpg + else: + snap_default = FLAGS.hp3par_cpg_snap + snap_cpg = self._get_volume_type_value(volume_type, + 'snap_cpg', + snap_default) - if not FLAGS.hp3par_cpg_snap: - extras['snapCPG'] = FLAGS.hp3par_cpg + # check for valid persona even if we don't use it until + # attach time, this will given end user notice that the + # persona type is invalid at volume creation time + self.get_persona_type(volume) + + if type_id is not None: + comments['volume_type_name'] = volume_type.get('name') + comments['volume_type_id'] = type_id + + extras = {'comment': json.dumps(comments), + 'snapCPG': snap_cpg, + 'tpvv': ttpv} capacity = self._capacity_from_size(volume['size']) volume_name = self._get_3par_vol_name(volume['id']) - client.createVolume(volume_name, FLAGS.hp3par_cpg, - capacity, extras) + client.createVolume(volume_name, cpg, capacity, extras) except hpexceptions.HTTPConflict: raise exception.Duplicate(_("Volume (%s) already exists on array") @@ -470,6 +557,9 @@ exit except hpexceptions.HTTPBadRequest as ex: LOG.error(str(ex)) raise exception.Invalid(ex.get_description()) + except exception.InvalidInput as ex: + LOG.error(str(ex)) + raise ex except Exception as ex: LOG.error(str(ex)) raise exception.CinderException(ex.get_description()) @@ -478,9 +568,67 @@ exit 'snapCPG': extras['snapCPG']} return metadata + @lockutils.synchronized('3parcopy', 'cinder-', True) + def _copy_volume(self, src_name, dest_name): + self._cli_run('createvvcopy -p %s %s' % (src_name, dest_name), None) + + @lockutils.synchronized('3parstate', 'cinder-', True) + def _get_volume_state(self, vol_name): + out = self._cli_run('showvv -state %s' % vol_name, None) + status = None + if out: + # out[0] is the header + info = out[1].split(',') + status = info[5] + + return status + + @lockutils.synchronized('3parclone', 'cinder-', True) + def create_cloned_volume(self, volume, src_vref, client, FLAGS): + + try: + orig_name = self._get_3par_vol_name(volume['source_volid']) + vol_name = self._get_3par_vol_name(volume['id']) + # We need to create a new volume first. Otherwise you + # can't delete the original + new_vol = self.create_volume(volume, client, FLAGS) + + # make the 3PAR copy the contents. + # can't delete the original until the copy is done. + self._copy_volume(orig_name, vol_name) + + # this can take a long time to complete + done = False + while not done: + status = self._get_volume_state(vol_name) + if status == 'normal': + done = True + elif status == 'copy_target': + LOG.debug("3Par still copying %s => %s" + % (orig_name, vol_name)) + else: + msg = _("Unexpected state while cloning %s") % status + LOG.warn(msg) + raise exception.CinderException(msg) + + if not done: + # wait 5 seconds between tests + time.sleep(5) + + return new_vol + except hpexceptions.HTTPForbidden: + raise exception.NotAuthorized() + except hpexceptions.HTTPNotFound: + raise exception.NotFound() + except Exception as ex: + LOG.error(str(ex)) + raise exception.CinderException(ex) + + return None + @lockutils.synchronized('3par', 'cinder-', True) def delete_volume(self, volume, client): - """ Delete a volume """ + """ Delete a volume. """ try: volume_name = self._get_3par_vol_name(volume['id']) client.deleteVolume(volume_name) @@ -537,7 +685,7 @@ be the same as it's Snapshot." @lockutils.synchronized('3par', 'cinder-', True) def create_snapshot(self, snapshot, client, FLAGS): - """Creates a snapshot.""" + """ Creates a snapshot. """ LOG.debug("Create Snapshot\n%s" % pprint.pformat(snapshot)) try: @@ -575,7 +723,7 @@ be the same as it's Snapshot." @lockutils.synchronized('3par', 'cinder-', True) def delete_snapshot(self, snapshot, client): - """Driver entry point for deleting a snapshot.""" + """ Driver entry point for deleting a snapshot. """ LOG.debug("Delete Snapshot\n%s" % pprint.pformat(snapshot)) try: diff --git a/cinder/volume/drivers/san/hp/hp_3par_fc.py b/cinder/volume/drivers/san/hp/hp_3par_fc.py new file mode 100644 index 000000000..d9d0334c4 --- /dev/null +++ b/cinder/volume/drivers/san/hp/hp_3par_fc.py @@ -0,0 +1,241 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# (c) Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# 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. +# +""" +Volume driver for HP 3PAR Storage array. This driver requires 3.1.2 firmware +on the 3PAR array. Set the following in the cinder.conf file to enable the +3PAR Fibre Channel Driver along with the required flags: + +volume_driver=cinder.volume.drivers.san.hp.hp_3par_fc.HP3PARFCDriver +""" + +from hp3parclient import client +from hp3parclient import exceptions as hpexceptions + +from cinder import exception +from cinder import flags +from cinder.openstack.common import cfg +from cinder.openstack.common import lockutils +from cinder.openstack.common import log as logging +import cinder.volume.driver +from cinder.volume.drivers.san.hp.hp_3par_common import HP3PARCommon + +VERSION = 1.0 +LOG = logging.getLogger(__name__) + +FLAGS = flags.FLAGS + + +class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver): + """OpenStack Fibre Channel driver to enable 3PAR storage array. + + Version history: + 1.0 - Initial driver + + """ + + def __init__(self, *args, **kwargs): + super(HP3PARFCDriver, self).__init__(*args, **kwargs) + self.client = None + self.common = None + + def _init_common(self): + return HP3PARCommon() + + def _check_flags(self): + """Sanity check to ensure we have required options set.""" + required_flags = ['hp3par_api_url', 'hp3par_username', + 'hp3par_password', + 'san_ip', 'san_login', 'san_password'] + self.common.check_flags(FLAGS, required_flags) + + def _create_client(self): + return client.HP3ParClient(FLAGS.hp3par_api_url) + + def get_volume_stats(self, refresh): + stats = self.common.get_volume_stats(refresh, self.client) + stats['storage_protocol'] = 'FC' + stats['volume_backend_name'] = 'HP3PARFCDriver' + return stats + + def do_setup(self, context): + self.common = self._init_common() + self._check_flags() + self.client = self._create_client() + if FLAGS.hp3par_debug: + self.client.debug_rest(True) + + try: + LOG.debug("Connecting to 3PAR") + self.client.login(FLAGS.hp3par_username, FLAGS.hp3par_password) + except hpexceptions.HTTPUnauthorized as ex: + LOG.warning("Failed to connect to 3PAR (%s) because %s" % + (FLAGS.hp3par_api_url, str(ex))) + msg = _("Login to 3PAR array invalid") + raise exception.InvalidInput(reason=msg) + + # make sure the CPG exists + try: + cpg = self.client.getCPG(FLAGS.hp3par_cpg) + except hpexceptions.HTTPNotFound as ex: + err = _("CPG (%s) doesn't exist on array") % FLAGS.hp3par_cpg + LOG.error(err) + raise exception.InvalidInput(reason=err) + + if 'domain' not in cpg and cpg['domain'] != FLAGS.hp3par_domain: + err = "CPG's domain '%s' and config option hp3par_domain '%s' \ +must be the same" % (cpg['domain'], FLAGS.hp3par_domain) + LOG.error(err) + raise exception.InvalidInput(reason=err) + + def check_for_setup_error(self): + """Returns an error if prerequisites aren't met.""" + self._check_flags() + + def create_volume(self, volume): + """ Create a new volume. """ + metadata = self.common.create_volume(volume, self.client, FLAGS) + return {'metadata': metadata} + + def create_cloned_volume(self, volume, src_vref): + """ Clone an existing volume. """ + new_vol = self.common.create_cloned_volume(volume, src_vref, + self.client, FLAGS) + return {'metadata': new_vol} + + def delete_volume(self, volume): + """ Delete a volume. """ + self.common.delete_volume(volume, self.client) + + def create_volume_from_snapshot(self, volume, snapshot): + """ + Creates a volume from a snapshot. + + TODO: support using the size from the user. + """ + self.common.create_volume_from_snapshot(volume, snapshot, self.client) + + def create_snapshot(self, snapshot): + """Creates a snapshot.""" + self.common.create_snapshot(snapshot, self.client, FLAGS) + + def delete_snapshot(self, snapshot): + """Driver entry point for deleting a snapshot.""" + self.common.delete_snapshot(snapshot, self.client) + + def initialize_connection(self, volume, connector): + """Assigns the volume to a server. + + Assign any created volume to a compute node/host so that it can be + used from that host. + + The driver returns a driver_volume_type of 'fibre_channel'. + The target_wwn can be a single entry or a list of wwns that + correspond to the list of remote wwn(s) that will export the volume. + Example return values: + + { + 'driver_volume_type': 'fibre_channel' + 'data': { + 'target_discovered': True, + 'target_lun': 1, + 'target_wwn': '1234567890123', + } + } + + or + + { + 'driver_volume_type': 'fibre_channel' + 'data': { + 'target_discovered': True, + 'target_lun': 1, + 'target_wwn': ['1234567890123', '0987654321321'], + } + } + + + Steps to export a volume on 3PAR + * Create a host on the 3par with the target wwn + * Create a VLUN for that HOST with the volume we want to export. + + """ + # we have to make sure we have a host + host = self._create_host(volume, connector) + + # now that we have a host, create the VLUN + vlun = self.common.create_vlun(volume, host, self.client) + + ports = self.common.get_ports() + + info = {'driver_volume_type': 'fibre_channel', + 'data': {'target_lun': vlun['lun'], + 'target_discovered': True, + 'target_wwn': ports['FC']}} + return info + + def terminate_connection(self, volume, connector, force): + """ + Driver entry point to unattach a volume from an instance. + """ + self.common.delete_vlun(volume, connector, self.client) + pass + + def _create_3par_fibrechan_host(self, hostname, wwn, domain, persona_id): + out = self.common._cli_run('createhost -persona %s -domain %s %s %s' + % (persona_id, domain, + hostname, " ".join(wwn)), None) + + def _modify_3par_fibrechan_host(self, hostname, wwn): + # when using -add, you can not send the persona or domain options + out = self.common._cli_run('createhost -add %s %s' + % (hostname, " ".join(wwn)), None) + + def _create_host(self, volume, connector): + """ + This is a 3PAR host entry for exporting volumes + via active VLUNs. + """ + host = None + hostname = self.common._safe_hostname(connector['host']) + try: + host = self.common._get_3par_host(hostname) + if not host['FCPaths']: + self._modify_3par_fibrechan_host(hostname, connector['wwpns']) + host = self.common._get_3par_host(hostname) + except hpexceptions.HTTPNotFound as ex: + # get persona from the volume type extra specs + persona_id = self.common.get_persona_type(volume) + # host doesn't exist, we have to create it + self._create_3par_fibrechan_host(hostname, connector['wwpns'], + FLAGS.hp3par_domain, persona_id) + host = self.common._get_3par_host(hostname) + + return host + + def create_export(self, context, volume): + pass + + def ensure_export(self, context, volume): + """Exports the volume.""" + pass + + def remove_export(self, context, volume): + """Removes an export for a logical volume.""" + pass diff --git a/cinder/volume/drivers/san/hp/hp_3par_iscsi.py b/cinder/volume/drivers/san/hp/hp_3par_iscsi.py index 9548b4f4e..5b7690074 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_iscsi.py +++ b/cinder/volume/drivers/san/hp/hp_3par_iscsi.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # -# Copyright (c) 2012 Hewlett-Packard, Inc. +# (c) Copyright 2012-2013 Hewlett-Packard Development Company, L.P. # All Rights Reserved. # # Copyright 2012 OpenStack LLC @@ -18,9 +18,11 @@ # under the License. # """ -Volume driver for HP 3PAR Storage array -This driver requires 3.1.2 firmware on -the 3Par array +Volume driver for HP 3PAR Storage array. This driver requires 3.1.2 firmware +on the 3PAR array. Set the following in the cinder.conf file to enable the +3PAR iSCSI Driver along with the required flags: + +volume_driver=cinder.volume.drivers.san.hp.hp_3par_iscsi.HP3PARISCSIDriver """ from hp3parclient import client @@ -33,13 +35,19 @@ from cinder.openstack.common import log as logging import cinder.volume.driver from cinder.volume.drivers.san.hp.hp_3par_common import HP3PARCommon +VERSION = 1.0 LOG = logging.getLogger(__name__) FLAGS = flags.FLAGS class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver): + """OpenStack iSCSI driver to enable 3PAR storage array. + + Version history: + 1.0 - Initial driver + """ def __init__(self, *args, **kwargs): super(HP3PARISCSIDriver, self).__init__(*args, **kwargs) self.client = None @@ -104,16 +112,24 @@ must be the same" % (cpg['domain'], FLAGS.hp3par_domain) @lockutils.synchronized('3par-vol', 'cinder-', True) def create_volume(self, volume): - """ Create a new volume """ + """ Create a new volume. """ metadata = self.common.create_volume(volume, self.client, FLAGS) return {'provider_location': "%s:%s" % (FLAGS.iscsi_ip_address, FLAGS.iscsi_port), 'metadata': metadata} + def create_cloned_volume(self, volume, src_vref): + """ Clone an existing volume. """ + new_vol = self.common.create_cloned_volume(volume, src_vref, + self.client, FLAGS) + return {'provider_location': "%s:%s" % + (FLAGS.iscsi_ip_address, FLAGS.iscsi_port), + 'metadata': new_vol} + @lockutils.synchronized('3par-vol', 'cinder-', True) def delete_volume(self, volume): - """ Delete a volume """ + """ Delete a volume. """ self.common.delete_volume(volume, self.client) @lockutils.synchronized('3par-vol', 'cinder-', True) @@ -202,9 +218,9 @@ must be the same" % (cpg['domain'], FLAGS.hp3par_domain) return iqn - def _create_3par_iscsi_host(self, hostname, iscsi_iqn, domain): - cmd = 'createhost -iscsi -persona 1 -domain %s %s %s' % \ - (domain, hostname, iscsi_iqn) + def _create_3par_iscsi_host(self, hostname, iscsi_iqn, domain, persona_id): + cmd = 'createhost -iscsi -persona %s -domain %s %s %s' % \ + (persona_id, domain, hostname, iscsi_iqn) self.common._cli_run(cmd, None) def _modify_3par_iscsi_host(self, hostname, iscsi_iqn): @@ -215,7 +231,7 @@ must be the same" % (cpg['domain'], FLAGS.hp3par_domain) def _create_host(self, volume, connector): """ This is a 3PAR host entry for exporting volumes - via active VLUNs + via active VLUNs. """ # make sure we don't have the host already host = None @@ -226,9 +242,11 @@ must be the same" % (cpg['domain'], FLAGS.hp3par_domain) self._modify_3par_iscsi_host(hostname, connector['initiator']) host = self.common._get_3par_host(hostname) except hpexceptions.HTTPNotFound: + # get persona from the volume type extra specs + persona_id = self.common.get_persona_type(volume) # host doesn't exist, we have to create it self._create_3par_iscsi_host(hostname, connector['initiator'], - FLAGS.hp3par_domain) + FLAGS.hp3par_domain, persona_id) host = self.common._get_3par_host(hostname) return host -- 2.45.2