--- /dev/null
+#!/usr/bin/env python
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright (c) 2012 Hewlett-Packard, Inc.
+# 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.
+"""
+Unit tests for OpenStack Cinder volume driver
+"""
+import shutil
+import tempfile
+
+from hp3parclient import exceptions as hpexceptions
+
+import cinder.flags
+from cinder.openstack.common import log as logging
+from cinder import test
+from cinder.volume.drivers.san.hp import hp_3par_iscsi as hpdriver
+
+FLAGS = cinder.flags.FLAGS
+
+LOG = logging.getLogger(__name__)
+
+HP3PAR_DOMAIN = 'OpenStack',
+HP3PAR_CPG = 'OpenStackCPG',
+HP3PAR_CPG_SNAP = 'OpenStackCPGSnap'
+
+
+class FakeHP3ParClient(object):
+
+ api_url = None
+ debug = False
+
+ volumes = []
+ hosts = []
+ vluns = []
+ cpgs = [
+ {'SAGrowth': {'LDLayout': {'diskPatterns': [{'diskType': 2}]},
+ 'incrementMiB': 8192},
+ 'SAUsage': {'rawTotalMiB': 24576,
+ 'rawUsedMiB': 768,
+ 'totalMiB': 8192,
+ 'usedMiB': 256},
+ 'SDGrowth': {'LDLayout': {'RAIDType': 4,
+ 'diskPatterns': [{'diskType': 2}]},
+ 'incrementMiB': 32768},
+ 'SDUsage': {'rawTotalMiB': 49152,
+ 'rawUsedMiB': 1023,
+ 'totalMiB': 36864,
+ 'usedMiB': 768},
+ 'UsrUsage': {'rawTotalMiB': 57344,
+ 'rawUsedMiB': 43349,
+ 'totalMiB': 43008,
+ 'usedMiB': 32512},
+ 'additionalStates': [],
+ 'degradedStates': [],
+ 'domain': HP3PAR_DOMAIN,
+ 'failedStates': [],
+ 'id': 5,
+ 'name': HP3PAR_CPG,
+ 'numFPVVs': 2,
+ 'numTPVVs': 0,
+ 'state': 1,
+ 'uuid': '29c214aa-62b9-41c8-b198-543f6cf24edf'}]
+
+ def __init__(self, api_url):
+ self.api_url = api_url
+
+ def debug_rest(self, flag):
+ self.debug = flag
+
+ def login(self, username, password, optional=None):
+ return None
+
+ def logout(self):
+ return None
+
+ def getVolumes(self):
+ return self.volumes
+
+ def getVolume(self, name):
+ if self.volumes:
+ for volume in self.volumes:
+ if volume['name'] == name:
+ return volume
+
+ msg = {'code': 'NON_EXISTENT_HOST',
+ 'desc': "VOLUME '%s' was not found" % name}
+ raise hpexceptions.HTTPNotFound(msg)
+
+ def createVolume(self, name, cpgName, sizeMiB, optional=None):
+ new_vol = {'additionalStates': [],
+ 'adminSpace': {'freeMiB': 0,
+ 'rawReservedMiB': 384,
+ 'reservedMiB': 128,
+ 'usedMiB': 128},
+ 'baseId': 115,
+ 'comment': optional['comment'],
+ 'copyType': 1,
+ 'creationTime8601': '2012-10-22T16:37:57-07:00',
+ 'creationTimeSec': 1350949077,
+ 'degradedStates': [],
+ 'domain': HP3PAR_DOMAIN,
+ 'failedStates': [],
+ 'id': 115,
+ 'name': name,
+ 'policies': {'caching': True,
+ 'oneHost': False,
+ 'staleSS': True,
+ 'system': False,
+ 'zeroDetect': False},
+ 'provisioningType': 1,
+ 'readOnly': False,
+ 'sizeMiB': sizeMiB,
+ 'snapCPG': optional['snapCPG'],
+ 'snapshotSpace': {'freeMiB': 0,
+ 'rawReservedMiB': 683,
+ 'reservedMiB': 512,
+ 'usedMiB': 512},
+ 'ssSpcAllocLimitPct': 0,
+ 'ssSpcAllocWarningPct': 0,
+ 'state': 1,
+ 'userCPG': cpgName,
+ 'userSpace': {'freeMiB': 0,
+ 'rawReservedMiB': 41984,
+ 'reservedMiB': 31488,
+ 'usedMiB': 31488},
+ 'usrSpcAllocLimitPct': 0,
+ 'usrSpcAllocWarningPct': 0,
+ 'uuid': '1e7daee4-49f4-4d07-9ab8-2b6a4319e243',
+ 'wwn': '50002AC00073383D'}
+ self.volumes.append(new_vol)
+ return None
+
+ def deleteVolume(self, name):
+ volume = self.getVolume(name)
+ self.volumes.remove(volume)
+
+ def createSnapshot(self, name, copyOfName, optional=None):
+ new_snap = {'additionalStates': [],
+ 'adminSpace': {'freeMiB': 0,
+ 'rawReservedMiB': 0,
+ 'reservedMiB': 0,
+ 'usedMiB': 0},
+ 'baseId': 342,
+ 'comment': optional['comment'],
+ 'copyOf': copyOfName,
+ 'copyType': 3,
+ 'creationTime8601': '2012-11-09T15:13:28-08:00',
+ 'creationTimeSec': 1352502808,
+ 'degradedStates': [],
+ 'domain': HP3PAR_DOMAIN,
+ 'expirationTime8601': '2012-11-09T17:13:28-08:00',
+ 'expirationTimeSec': 1352510008,
+ 'failedStates': [],
+ 'id': 343,
+ 'name': name,
+ 'parentId': 342,
+ 'policies': {'caching': True,
+ 'oneHost': False,
+ 'staleSS': True,
+ 'system': False,
+ 'zeroDetect': False},
+ 'provisioningType': 3,
+ 'readOnly': True,
+ 'retentionTime8601': '2012-11-09T16:13:27-08:00',
+ 'retentionTimeSec': 1352506407,
+ 'sizeMiB': 256,
+ 'snapCPG': HP3PAR_CPG_SNAP,
+ 'snapshotSpace': {'freeMiB': 0,
+ 'rawReservedMiB': 0,
+ 'reservedMiB': 0,
+ 'usedMiB': 0},
+ 'ssSpcAllocLimitPct': 0,
+ 'ssSpcAllocWarningPct': 0,
+ 'state': 1,
+ 'userCPG': HP3PAR_CPG,
+ 'userSpace': {'freeMiB': 0,
+ 'rawReservedMiB': 0,
+ 'reservedMiB': 0,
+ 'usedMiB': 0},
+ 'usrSpcAllocLimitPct': 0,
+ 'usrSpcAllocWarningPct': 0,
+ 'uuid': 'd7a40b8f-2511-46a8-9e75-06383c826d19',
+ 'wwn': '50002AC00157383D'}
+ self.volumes.append(new_snap)
+ return None
+
+ def deleteSnapshot(self, name):
+ volume = self.getVolume(name)
+ self.volumes.remove(volume)
+
+ def getCPGs(self):
+ return self.cpgs
+
+ def getCPG(self, name):
+ if self.cpgs:
+ for cpg in self.cpgs:
+ if cpg['name'] == name:
+ return cpg
+
+ msg = {'code': 'NON_EXISTENT_HOST',
+ 'desc': "CPG '%s' was not found" % name}
+ raise hpexceptions.HTTPNotFound(msg)
+
+ def createVLUN(self, volumeName, lun, hostname=None,
+ portPos=None, noVcn=None,
+ overrideLowerPriority=None):
+
+ vlun = {'active': False,
+ 'failedPathInterval': 0,
+ 'failedPathPol': 1,
+ 'hostname': hostname,
+ 'lun': lun,
+ 'multipathing': 1,
+ 'portPos': portPos,
+ 'type': 4,
+ 'volumeName': volumeName,
+ 'volumeWWN': '50002AC00077383D'}
+ self.vluns.append(vlun)
+ return None
+
+ def deleteVLUN(self, name, lunID, hostname=None, port=None):
+ vlun = self.getVLUN(name)
+ self.vluns.remove(vlun)
+
+ def getVLUNs(self):
+ return self.vluns
+
+ def getVLUN(self, volumeName):
+ for vlun in self.vluns:
+ if vlun['volumeName'] == volumeName:
+ return vlun
+
+ msg = {'code': 'NON_EXISTENT_HOST',
+ 'desc': "VLUN '%s' was not found" % volumeName}
+ raise hpexceptions.HTTPNotFound(msg)
+
+
+class TestHP3PARDriver(test.TestCase):
+
+ TARGET_IQN = "iqn.2000-05.com.3pardata:21810002ac00383d"
+ VOLUME_NAME = "volume-d03338a9-9115-48a3-8dfc-35cdfcdc15a7"
+ SNAPSHOT_NAME = "snapshot-2f823bdc-e36e-4dc8-bd15-de1c7a28ff31"
+ VOLUME_3PAR_NAME = "osv-0DM4qZEVSKON-DXN-NwVpw"
+ SNAPSHOT_VOL_NAME = "oss-L4I73ONuTci9Fd4ceij-MQ"
+ FAKE_HOST = "fakehost"
+
+ _hosts = {}
+
+ def setUp(self):
+ self.tempdir = tempfile.mkdtemp()
+ super(TestHP3PARDriver, 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(hpdriver.HP3PARISCSIDriver, "_create_client",
+ self.fake_create_client)
+ self.stubs.Set(hpdriver.HP3PARISCSIDriver,
+ "_iscsi_discover_target_iqn",
+ self.fake_iscsi_discover_target_iqn)
+ self.stubs.Set(hpdriver.HP3PARISCSIDriver, "_create_3par_iscsi_host",
+ self.fake_create_3par_iscsi_host)
+ self.stubs.Set(hpdriver.HP3PARISCSIDriver,
+ "_iscsi_discover_target_iqn",
+ self.fake_iscsi_discover_target_iqn)
+
+ self.stubs.Set(hpdriver.HP3PARCommon, "_get_3par_host",
+ self.fake_get_3par_host)
+ self.stubs.Set(hpdriver.HP3PARCommon, "_delete_3par_host",
+ self.fake_delete_3par_host)
+ self.stubs.Set(hpdriver.HP3PARCommon, "_create_3par_vlun",
+ self.fake_create_3par_vlun)
+
+ self.driver = hpdriver.HP3PARISCSIDriver()
+ self.driver.do_setup(None)
+
+ self.volume = {'name': self.VOLUME_NAME,
+ 'display_name': 'Foo Volume',
+ 'size': 1,
+ 'host': self.FAKE_HOST}
+
+ user_id = '2689d9a913974c008b1d859013f23607'
+ project_id = 'fac88235b9d64685a3530f73e490348f'
+ volume_id = '761fc5e5-5191-4ec7-aeba-33e36de44156'
+ fake_desc = 'test description name'
+ self.snapshot = type('snapshot',
+ (object,),
+ {'name': self.SNAPSHOT_NAME,
+ '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'},
+ 'driver_volume_type': 'iscsi'}
+
+ def tearDown(self):
+ shutil.rmtree(self.tempdir)
+ super(TestHP3PARDriver, self).tearDown()
+
+ def fake_create_client(self):
+ return FakeHP3ParClient(FLAGS.hp3par_api_url)
+
+ def fake_iscsi_discover_target_iqn(self, ip_address):
+ return self.TARGET_IQN
+
+ def fake_create_3par_iscsi_host(self, hostname, iscsi_iqn, domain):
+ host = {'FCPaths': [],
+ 'descriptors': None,
+ 'domain': domain,
+ 'iSCSIPaths': [{'driverVersion': None,
+ 'firmwareVersion': None,
+ 'hostSpeed': 0,
+ 'ipAddr': '10.10.221.59',
+ 'model': None,
+ 'name': iscsi_iqn,
+ 'portPos': {'cardPort': 1, 'node': 1,
+ 'slot': 8},
+ 'vendor': None}],
+ 'id': 11,
+ '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 test_create_volume(self):
+ self.flags(lock_path=self.tempdir)
+ model_update = self.driver.create_volume(self.volume)
+ 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_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_VOL_NAME)
+ self.assertEqual(snap_vol['name'], self.SNAPSHOT_VOL_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_VOL_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_VOL_NAME)
+ self.assertEqual(snap_vol['name'], self.SNAPSHOT_VOL_NAME)
+
+ 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'], 'iscsi')
+ self.assertEqual(result['data']['target_iqn'],
+ self.properties['data']['target_iqn'])
+ self.assertEqual(result['data']['target_portal'],
+ self.properties['data']['target_portal'])
+ self.assertEqual(result['data']['target_discovered'],
+ self.properties['data']['target_discovered'])
+
+ # 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_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)
--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright (c) 2012 Hewlett-Packard, Inc.
+# 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 common utilities for HP 3PAR Storage array
+This driver requires 3.1.2 firmware on the 3PAR array.
+
+The driver 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,
+san_password settings for ssh connections into the 3PAR
+array. It also requires the setting of
+hp3par_api_url, hp3par_username, hp3par_password
+for credentials to talk to the REST service on the 3PAR
+array.
+"""
+import base64
+import json
+import paramiko
+import pprint
+from random import randint
+import uuid
+
+from eventlet import greenthread
+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
+from cinder import utils
+
+
+LOG = logging.getLogger(__name__)
+
+hp3par_opts = [
+ cfg.StrOpt('hp3par_api_url',
+ default='',
+ help="3PAR WSAPI Server Url like "
+ "https://<3par ip>:8080/api/v1"),
+ cfg.StrOpt('hp3par_username',
+ default='',
+ help="3PAR Super user username"),
+ cfg.StrOpt('hp3par_password',
+ default='',
+ help="3PAR Super user password"),
+ cfg.StrOpt('hp3par_domain',
+ default="OpenStack",
+ help="The 3par domain name to use"),
+ cfg.StrOpt('hp3par_cpg',
+ default="OpenStack",
+ help="The CPG to use for volume creation"),
+ cfg.StrOpt('hp3par_cpg_snap',
+ default="",
+ help="The CPG to use for Snapshots for volumes. "
+ "If empty hp3par_cpg will be used"),
+ cfg.StrOpt('hp3par_snapshot_retention',
+ default="",
+ help="The time in hours to retain a snapshot. "
+ "You can't delete it before this expires."),
+ cfg.StrOpt('hp3par_snapshot_expiration',
+ default="",
+ help="The time in hours when a snapshot expires "
+ " and is deleted. This must be larger than expiration"),
+ cfg.BoolOpt('hp3par_debug',
+ default=False,
+ help="Enable HTTP debugging to 3PAR")
+]
+
+FLAGS = flags.FLAGS
+FLAGS.register_opts(hp3par_opts)
+
+
+class HP3PARCommon():
+
+ def __init__(self):
+ self.sshpool = None
+
+ def check_flags(self, FLAGS, required_flags):
+ for flag in required_flags:
+ if not getattr(FLAGS, flag, None):
+ raise exception.InvalidInput(reason=_('%s is not set') % flag)
+
+ def _get_3par_vol_name(self, name):
+ """
+ Converts the openstack volume name from
+ volume-ecffc30f-98cb-4cf5-85ee-d7309cc17cd2
+ to
+ osv-7P.DD5jLTPWF7tcwnMF80g
+
+ We convert the 128 bits of the uuid into a 24character long
+ base64 encoded string to ensure we don't exceed the maximum
+ allowed 31 character name limit on 3Par
+
+ We strip the padding '=' and replace + with .
+ and / with -
+ """
+ name = name.replace("volume-", "")
+ volume_name = self._encode_name(name)
+ return "osv-%s" % volume_name
+
+ def _get_3par_snap_name(self, name):
+ name = name.replace("snapshot-", "")
+ snapshot_name = self._encode_name(name)
+ return "oss-%s" % snapshot_name
+
+ def _encode_name(self, name):
+ uuid_str = name.replace("-", "")
+ vol_uuid = uuid.UUID('urn:uuid:%s' % uuid_str)
+ vol_encoded = base64.b64encode(vol_uuid.bytes)
+
+ # 3par doesn't allow +, nor /
+ vol_encoded = vol_encoded.replace('+', '.')
+ vol_encoded = vol_encoded.replace('/', '-')
+ #strip off the == as 3par doesn't like those.
+ vol_encoded = vol_encoded.replace('=', '')
+ return vol_encoded
+
+ def _capacity_from_size(self, vol_size):
+
+ # because 3PAR volume sizes are in
+ # Mebibytes, Gigibytes, not Megabytes.
+ MB = 1000L
+ MiB = 1.048576
+
+ if int(vol_size) == 0:
+ capacity = MB # default: 1GB
+ else:
+ capacity = vol_size * MB
+
+ capacity = int(round(capacity / MiB))
+ return capacity
+
+ def _cli_run(self, verb, cli_args):
+ """Runs a CLI command over SSH, without doing any result parsing"""
+ cli_arg_strings = []
+ if cli_args:
+ for k, v in cli_args.items():
+ if k == '':
+ cli_arg_strings.append(" %s" % k)
+ else:
+ cli_arg_strings.append(" %s=%s" % (k, v))
+
+ cmd = verb + ''.join(cli_arg_strings)
+ LOG.debug("SSH CMD = %s " % cmd)
+
+ (stdout, stderr) = self._run_ssh(cmd, False)
+
+ # we have to strip out the input and exit lines
+ tmp = stdout.split("\r\n")
+ out = tmp[5:len(tmp) - 2]
+ return out
+
+ def _ssh_execute(self, ssh, cmd,
+ check_exit_code=True):
+ """
+ We have to do this in order to get CSV output
+ 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
+ """
+ LOG.debug(_('Running cmd (SSH): %s'), cmd)
+
+ channel = ssh.invoke_shell()
+ stdin_stream = channel.makefile('wb')
+ stdout_stream = channel.makefile('rb')
+ stderr_stream = channel.makefile('rb')
+
+ stdin_stream.write('''setclienv csvtable 1
+%s
+exit
+''' % cmd)
+
+ #stdin.write('process_input would go here')
+ #stdin.flush()
+
+ # NOTE(justinsb): This seems suspicious...
+ # ...other SSH clients have buffering issues with this approach
+ stdout = stdout_stream.read()
+ stderr = stderr_stream.read()
+ stdin_stream.close()
+ stdout_stream.close()
+ stderr_stream.close()
+
+ exit_status = channel.recv_exit_status()
+
+ # exit_status == -1 if no exit code was returned
+ if exit_status != -1:
+ LOG.debug(_('Result was %s') % exit_status)
+ if check_exit_code and exit_status != 0:
+ raise exception.ProcessExecutionError(exit_code=exit_status,
+ stdout=stdout,
+ stderr=stderr,
+ cmd=cmd)
+ channel.close()
+ return (stdout, stderr)
+
+ def _run_ssh(self, command, check_exit=True, attempts=1):
+ if not self.sshpool:
+ self.sshpool = utils.SSHPool(FLAGS.san_ip,
+ FLAGS.san_ssh_port,
+ FLAGS.ssh_conn_timeout,
+ FLAGS.san_login,
+ password=FLAGS.san_password,
+ privatekey=FLAGS.san_private_key,
+ min_size=FLAGS.ssh_min_pool_conn,
+ max_size=FLAGS.ssh_max_pool_conn)
+ try:
+ total_attempts = attempts
+ with self.sshpool.item() as ssh:
+ while attempts > 0:
+ attempts -= 1
+ try:
+ return self._ssh_execute(ssh, command,
+ check_exit_code=check_exit)
+ except Exception as e:
+ LOG.error(e)
+ greenthread.sleep(randint(20, 500) / 100.0)
+ raise paramiko.SSHException(_("SSH Command failed after "
+ "'%(total_attempts)r' attempts"
+ ": '%(command)s'"), locals())
+ except Exception as e:
+ LOG.error(_("Error running ssh command: %s") % command)
+ raise e
+
+ def _delete_3par_host(self, hostname):
+ self._cli_run('removehost %s' % hostname, None)
+
+ def _create_3par_vlun(self, volume, hostname):
+ self._cli_run('createvlun %s auto %s' % (volume, hostname), None)
+
+ def _safe_hostname(self, hostname):
+ """
+ We have to use a safe hostname length
+ for 3PAR host names
+ """
+ try:
+ index = hostname.index('.')
+ except ValueError:
+ # couldn't find it
+ index = len(hostname)
+
+ #we'll just chop this off for now.
+ if index > 23:
+ index = 23
+
+ return hostname[:index]
+
+ def _get_3par_host(self, hostname):
+ out = self._cli_run('showhost -verbose %s' % (hostname), None)
+ LOG.debug("OUTPUT = \n%s" % (pprint.pformat(out)))
+ host = {'id': None, 'name': None,
+ 'domain': None,
+ 'descriptors': {},
+ 'iSCSIPaths': [],
+ 'FCPaths': []}
+
+ if out:
+ err = out[0]
+ if err == 'no hosts listed':
+ msg = {'code': 'NON_EXISTENT_HOST',
+ 'desc': "HOST '%s' was not found" % hostname}
+ raise hpexceptions.HTTPNotFound(msg)
+
+ # start parsing the lines after the header line
+ for line in out[1:]:
+ if line == '':
+ break
+ tmp = line.split(',')
+ paths = {}
+
+ LOG.debug("line = %s" % (pprint.pformat(tmp)))
+ host['id'] = tmp[0]
+ host['name'] = tmp[1]
+
+ portPos = tmp[4]
+ LOG.debug("portPos = %s" % (pprint.pformat(portPos)))
+ if portPos == '---':
+ portPos = None
+ else:
+ port = portPos.split(':')
+ portPos = {'node': int(port[0]), 'slot': int(port[1]),
+ 'cardPort': int(port[2])}
+
+ paths['portPos'] = portPos
+
+ # If FC entry
+ if tmp[5] == 'n/a':
+ paths['wwn'] = tmp[3]
+ host['FCPaths'].append(paths)
+ # else iSCSI entry
+ else:
+ paths['name'] = tmp[3]
+ paths['ipAddr'] = tmp[5]
+ host['iSCSIPaths'].append(paths)
+
+ # find the offset to the description stuff
+ offset = 0
+ for line in out:
+ if line[:15] == '---------- Host':
+ break
+ else:
+ offset += 1
+
+ info = out[offset + 2]
+ tmp = info.split(':')
+ host['domain'] = tmp[1]
+
+ info = out[offset + 4]
+ tmp = info.split(':')
+ host['descriptors']['location'] = tmp[1]
+
+ info = out[offset + 5]
+ tmp = info.split(':')
+ host['descriptors']['ipAddr'] = tmp[1]
+
+ info = out[offset + 6]
+ tmp = info.split(':')
+ host['descriptors']['os'] = tmp[1]
+
+ info = out[offset + 7]
+ tmp = info.split(':')
+ host['descriptors']['model'] = tmp[1]
+
+ info = out[offset + 8]
+ tmp = info.split(':')
+ host['descriptors']['contact'] = tmp[1]
+
+ info = out[offset + 9]
+ tmp = info.split(':')
+ host['descriptors']['comment'] = tmp[1]
+
+ return host
+
+ def create_vlun(self, volume, host, client):
+ """
+ In order to export a volume on a 3PAR box, we have to
+ create a VLUN.
+ """
+ volume_name = self._get_3par_vol_name(volume['name'])
+ self._create_3par_vlun(volume_name, host['name'])
+ return client.getVLUN(volume_name)
+
+ def delete_vlun(self, volume, connector, client):
+ hostname = self._safe_hostname(connector['host'])
+
+ volume_name = self._get_3par_vol_name(volume['name'])
+ vlun = client.getVLUN(volume_name)
+ client.deleteVLUN(volume_name, vlun['lun'], hostname)
+ self._delete_3par_host(hostname)
+
+ @lockutils.synchronized('3par', 'cinder-', True)
+ def create_volume(self, volume, client, FLAGS):
+ """ Create a new volume """
+ LOG.debug("CREATE VOLUME (%s : %s %s)" %
+ (volume['display_name'], volume['name'],
+ self._get_3par_vol_name(volume['name'])))
+ try:
+ comments = {'name': volume['name'],
+ 'display_name': volume['display_name'],
+ 'type': 'OpenStack'}
+ extras = {'comment': json.dumps(comments),
+ 'snapCPG': FLAGS.hp3par_cpg_snap}
+
+ if not FLAGS.hp3par_cpg_snap:
+ extras['snapCPG'] = FLAGS.hp3par_cpg
+
+ capacity = self._capacity_from_size(volume['size'])
+ volume_name = self._get_3par_vol_name(volume['name'])
+ client.createVolume(volume_name, FLAGS.hp3par_cpg,
+ capacity, extras)
+
+ except hpexceptions.HTTPConflict:
+ raise exception.Duplicate(_("Volume (%s) already exists on array")
+ % volume_name)
+ except hpexceptions.HTTPBadRequest as ex:
+ LOG.error(str(ex))
+ raise exception.Invalid(ex.get_description())
+ except Exception as ex:
+ LOG.error(str(ex))
+ raise exception.CinderException(ex.get_description())
+
+ @lockutils.synchronized('3par', 'cinder-', True)
+ def delete_volume(self, volume, client):
+ """ Delete a volume """
+ try:
+ volume_name = self._get_3par_vol_name(volume['name'])
+ client.deleteVolume(volume_name)
+ except hpexceptions.HTTPNotFound as ex:
+ LOG.error(str(ex))
+ raise exception.NotFound(ex.get_description())
+ except hpexceptions.HTTPForbidden as ex:
+ LOG.error(str(ex))
+ raise exception.NotAuthorized(ex.get_description())
+ except Exception as ex:
+ LOG.error(str(ex))
+ raise exception.CinderException(ex.get_description())
+
+ @lockutils.synchronized('3par', 'cinder-', True)
+ def create_volume_from_snapshot(self, volume, snapshot, client):
+ """
+ Creates a volume from a snapshot.
+
+ TODO: support using the size from the user.
+ """
+ LOG.debug("Create Volume from Snapshot\n%s\n%s" %
+ (pprint.pformat(volume['display_name']),
+ pprint.pformat(snapshot.display_name)))
+ try:
+ snap_name = self._get_3par_snap_name(snapshot.name)
+ vol_name = self._get_3par_vol_name(volume['name'])
+
+ extra = {'name': snapshot.display_name,
+ 'description': snapshot.display_description}
+
+ optional = {'comment': json.dumps(extra),
+ 'readOnly': False}
+
+ client.createSnapshot(vol_name, snap_name, optional)
+ except hpexceptions.HTTPForbidden as ex:
+ raise exception.NotAuthorized()
+ except hpexceptions.HTTPNotFound as ex:
+ raise exception.NotFound()
+
+ @lockutils.synchronized('3par', 'cinder-', True)
+ def create_snapshot(self, snapshot, client, FLAGS):
+ """Creates a snapshot."""
+ LOG.debug("Create Snapshot\n%s" % pprint.pformat(snapshot))
+
+ try:
+ snap_name = self._get_3par_snap_name(snapshot.name)
+ vol_name = self._get_3par_vol_name(snapshot.volume_name)
+
+ extra = {'name': snapshot.display_name,
+ 'vol_name': snapshot.volume_name,
+ 'description': snapshot.display_description}
+
+ optional = {'comment': json.dumps(extra),
+ 'readOnly': True}
+ if FLAGS.hp3par_snapshot_expiration:
+ optional['expirationHours'] = FLAGS.hp3par_snapshot_expiration
+
+ if FLAGS.hp3par_snapshot_retention:
+ optional['retentionHours'] = FLAGS.hp3par_snapshot_retention
+
+ client.createSnapshot(snap_name, vol_name, optional)
+ except hpexceptions.HTTPForbidden:
+ raise exception.NotAuthorized()
+ except hpexceptions.HTTPNotFound:
+ raise exception.NotFound()
+
+ @lockutils.synchronized('3par', 'cinder-', True)
+ def delete_snapshot(self, snapshot, client):
+ """Driver entry point for deleting a snapshot."""
+ LOG.debug("Delete Snapshot\n%s" % pprint.pformat(snapshot))
+
+ try:
+ snap_name = self._get_3par_snap_name(snapshot.name)
+ client.deleteVolume(snap_name)
+ except hpexceptions.HTTPForbidden:
+ raise exception.NotAuthorized()
+ except hpexceptions.HTTPNotFound:
+ raise exception.NotFound()
--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright (c) 2012 Hewlett-Packard, Inc.
+# 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
+"""
+
+from hp3parclient import client
+from hp3parclient import exceptions as hpexceptions
+
+from cinder import exception
+from cinder import flags
+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
+
+LOG = logging.getLogger(__name__)
+
+FLAGS = flags.FLAGS
+
+
+class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
+
+ def __init__(self, *args, **kwargs):
+ super(HP3PARISCSIDriver, 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', 'iscsi_ip_address',
+ 'iscsi_port', '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 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:
+ 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)
+
+ def check_for_setup_error(self):
+ """Returns an error if prerequisites aren't met."""
+ self._check_flags()
+
+ @lockutils.synchronized('3par-vol', 'cinder-', True)
+ def create_volume(self, volume):
+ """ Create a new volume """
+ self.common.create_volume(volume, self.client, FLAGS)
+
+ return {'provider_location': "%s:%s" %
+ (FLAGS.iscsi_ip_address, FLAGS.iscsi_port)}
+
+ @lockutils.synchronized('3par-vol', 'cinder-', True)
+ def delete_volume(self, volume):
+ """ Delete a volume """
+ self.common.delete_volume(volume, self.client)
+
+ @lockutils.synchronized('3par-vol', 'cinder-', True)
+ 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)
+
+ @lockutils.synchronized('3par-snap', 'cinder-', True)
+ def create_snapshot(self, snapshot):
+ """Creates a snapshot."""
+ self.common.create_snapshot(snapshot, self.client, FLAGS)
+
+ @lockutils.synchronized('3par-snap', 'cinder-', True)
+ def delete_snapshot(self, snapshot):
+ """Driver entry point for deleting a snapshot."""
+ self.common.delete_snapshot(snapshot, self.client)
+
+ @lockutils.synchronized('3par-attach', 'cinder-', True)
+ 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.
+
+ This driver returns a driver_volume_type of 'iscsi'.
+ The format of the driver data is defined in _get_iscsi_properties.
+ Example return value:
+
+ {
+ 'driver_volume_type': 'iscsi'
+ 'data': {
+ 'target_discovered': True,
+ 'target_iqn': 'iqn.2010-10.org.openstack:volume-00000001',
+ 'target_protal': '127.0.0.1:3260',
+ 'volume_id': 1,
+ }
+ }
+
+ Steps to export a volume on 3PAR
+ * Get the 3PAR iSCSI iqn
+ * Create a host on the 3par
+ * create vlun on the 3par
+ """
+ # get the target_iqn on the 3par interface.
+ target_iqn = self._iscsi_discover_target_iqn(FLAGS.iscsi_ip_address)
+
+ # 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)
+
+ info = {'driver_volume_type': 'iscsi',
+ 'data': {'target_portal': "%s:%s" %
+ (FLAGS.iscsi_ip_address, FLAGS.iscsi_port),
+ 'target_iqn': target_iqn,
+ 'target_lun': vlun['lun'],
+ 'target_discovered': True
+ }
+ }
+ return info
+
+ @lockutils.synchronized('3par-attach', 'cinder-', True)
+ 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)
+
+ def _iscsi_discover_target_iqn(self, remote_ip):
+ result = self.common._cli_run('showport -ids', None)
+
+ iqn = None
+ if result:
+ # first line is header
+ result = result[1:]
+ for line in result:
+ info = line.split(",")
+ if info and len(info) > 2:
+ if info[1] == remote_ip:
+ iqn = info[2]
+
+ 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)
+ self.common._cli_run(cmd, None)
+
+ def _modify_3par_iscsi_host(self, hostname, iscsi_iqn):
+ # when using -add, you can not send the persona or domain options
+ self.common._cli_run('createhost -iscsi -add %s %s'
+ % (hostname, iscsi_iqn), None)
+
+ def _create_host(self, volume, connector):
+ """
+ This is a 3PAR host entry for exporting volumes
+ via active VLUNs
+ """
+ # make sure we don't have the host already
+ host = None
+ hostname = self.common._safe_hostname(connector['host'])
+ try:
+ host = self.common._get_3par_host(hostname)
+ if not host['iSCSIPaths']:
+ self._modify_3par_iscsi_host(hostname, connector['initiator'])
+ host = self.common._get_3par_host(hostname)
+ except hpexceptions.HTTPNotFound:
+ # host doesn't exist, we have to create it
+ self._create_3par_iscsi_host(hostname, connector['initiator'],
+ FLAGS.hp3par_domain)
+ host = self.common._get_3par_host(hostname)
+
+ return host
+
+ @lockutils.synchronized('3par-exp', 'cinder-', True)
+ def create_export(self, context, volume):
+ pass
+
+ @lockutils.synchronized('3par-exp', 'cinder-', True)
+ def ensure_export(self, context, volume):
+ """Exports the volume."""
+ pass
+
+ @lockutils.synchronized('3par-exp', 'cinder-', True)
+ def remove_export(self, context, volume):
+ """Removes an export for a logical volume."""
+ pass
#### (BoolOpt) Don't halt on deletion of non-existing volumes
+######## defined in cinder.volume.drivers.san.hp.hp_3par_common ########
+
+# hp3par_api_url=<None>
+#### (StrOpt) 3PAR WSAPI Server Url like https://<3par ip>:8080/api/v1
+
+# hp3par_username=<None>
+#### (StrOpt) 3PAR username
+
+# hp3par_password=<None>
+#### (StrOpt) 3PAR password
+
+# hp3par_domain=OpenStack
+#### (StrOpt) The 3par domain to use
+
+# hp3par_cpg=OpenStack
+#### (StrOpt) The CPG to use for volume creation
+
+# hp3par_cpg_snap=<None>
+#### (StrOpt) The CPG to use for snapshots for volumes.
+
+# hp3par_snapshot_retention=<None>
+#### (StrOpt) The time in hours to retain a snapshot
+
+# hp3par_snapshot_expiration=<None>
+#### (StrOpt) The time in ours when a snapshot expires
+
+# hp3par_debug=False
+#### (BoolOpt) Enable REST debugging output
+
+
# Total option count: 219
pylint==0.25.2
sphinx>=1.1.2
MySQL-python
+hp3parclient>=1.0.0