hp3parclient-3.0.0/0000775000175000017500000000000012276222042014343 5ustar stackstack00000000000000hp3parclient-3.0.0/test/0000775000175000017500000000000012276222042015322 5ustar stackstack00000000000000hp3parclient-3.0.0/test/test_HP3ParClient_ports.py0000664000175000017500000000507112276214354022370 0ustar stackstack00000000000000 # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2009-2012 10gen, Inc. # # 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. """Test class of 3Par Client handling Ports""" import sys, os sys.path.insert(0,os.path.realpath(os.path.abspath('../'))) from hp3parclient import client, exceptions import unittest import test_HP3ParClient_base class HP3ParClientPortTestCase(test_HP3ParClient_base.HP3ParClientBaseTestCase): def setUp(self): super(HP3ParClientPortTestCase, self).setUp() def tearDown(self): pass def test_get_ports_all(self): self.printHeader('get_ports_all') ports = self.cl.getPorts() if ports: if len(ports['members']) == ports['total']: self.printFooter('get_ports_all') return else: self.fail('Number of ports in invalid.') else: self.fail('Cannot retrieve ports.') def test_get_ports_fc(self): self.printHeader('get_ports_fc') fc_ports = self.cl.getFCPorts(4) print(fc_ports) if fc_ports: for port in fc_ports: if port['protocol'] != 1: self.fail('Non-FC ports detected.') self.printFooter('get_ports_fc') return else: self.fail('Cannot retrieve FC ports.') def test_get_ports_iscsi(self): self.printHeader('get_ports_iscsi') iscsi = self.cl.getiSCSIPorts(4) if iscsi: for port in iscsi: if port['protocol'] != 2: self.fail('Non-iSCSI ports detected.') self.printFooter('get_ports_iscsi') return else: self.fail('Cannot retrieve iSCSI Ports.') def test_get_ports_ip(self): self.printHeader('get_ports_ip') ip = self.cl.getIPPorts() if ip: for port in ip: if port['protocol'] != 4: self.fail('non-ip ports detected.') self.printFooter('get_ports_ip') else: self.fail('cannot retrieve ip ports.') hp3parclient-3.0.0/test/test_HP3ParClient_system.py0000664000175000017500000000517312276214354022550 0ustar stackstack00000000000000# vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2009-2012 10gen, Inc. # # 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. """Test class of 3PAR Client System Level APIS""" import sys import os sys.path.insert(0, os.path.realpath(os.path.abspath('../'))) import unittest import test_HP3ParClient_base from hp3parclient import client, exceptions class HP3ParClientSystemTestCase(test_HP3ParClient_base.HP3ParClientBaseTestCase): def setUp(self): super(HP3ParClientSystemTestCase, self).setUp() def tearDown(self): # very last, tear down base class super(HP3ParClientSystemTestCase, self).tearDown() def test_getStorageSystemInfo(self): self.printHeader('getStorageSystemInfo') info = self.cl.getStorageSystemInfo() self.assertIsNotNone(info) self.printFooter('getStorageSystemInfo') def test_getWSAPIConfigurationInfo(self): self.printHeader('getWSAPIConfigurationInfo') info = self.cl.getWSAPIConfigurationInfo() self.assertIsNotNone(info) self.printFooter('getWSAPIConfigurationInfo') def test_query_task(self): self.printHeader("query_task") tasks = self.cl.getAllTasks() self.assertIsNotNone(tasks) self.assertGreater(tasks['total'], 0) first_task = tasks['members'].pop() self.assertIsNotNone(first_task) task = self.cl.getTask(first_task['id']) self.assertEqual(first_task, task) self.printFooter("query_task") def test_query_task_negative(self): self.printHeader("query_task_negative") try: self.cl.getTask(-1) except exceptions.HTTPBadRequest as ex: return self.fail("expected an HTTP Bad Request exception") def test_query_task_non_int(self): self.printHeader("query_task_non_int") try: self.cl.getTask("nonIntTask") except exceptions.HTTPBadRequest as ex: return self.fail("expected an HTTP Bad Request exception") #testing #suite = unittest.TestLoader().loadTestsFromTestCase(HP3ParClientSystemTestCase) #unittest.TextTestRunner(verbosity=2).run(suite) hp3parclient-3.0.0/test/test_HP3ParClient_volume.py0000664000175000017500000007700712276214354022540 0ustar stackstack00000000000000# vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2009-2012 10gen, Inc. # # 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. """Test class of 3Par Client handling volume & snapshot """ import sys, os sys.path.insert(0,os.path.realpath(os.path.abspath('../'))) from hp3parclient import client, exceptions import unittest import test_HP3ParClient_base CPG_NAME1 = 'CPG1_UNIT_TEST' CPG_NAME2 = 'CPG2_UNIT_TEST' VOLUME_NAME1 = 'VOLUME1_UNIT_TEST' VOLUME_NAME2 = 'VOLUME2_UNIT_TEST' SNAP_NAME1 = 'SNAP_UNIT_TEST' DOMAIN = 'UNIT_TEST_DOMAIN' VOLUME_SET_NAME1 = 'VOLUME_SET1_UNIT_TEST' VOLUME_SET_NAME2 = 'VOLUME_SET2_UNIT_TEST' class HP3ParClientVolumeTestCase(test_HP3ParClient_base.HP3ParClientBaseTestCase): def setUp(self): super(HP3ParClientVolumeTestCase, self).setUp(withSSH=True) optional = {'domain': DOMAIN} try: self.cl.createCPG(CPG_NAME1, optional) except: pass try: self.cl.createCPG(CPG_NAME2) except: pass def tearDown(self): try: self.cl.deleteVolumeSet(VOLUME_SET_NAME1) except: pass try: self.cl.deleteVolumeSet(VOLUME_SET_NAME2) except: pass try: self.cl.deleteVolume(VOLUME_NAME1) except: pass try: self.cl.deleteVolume(VOLUME_NAME2) except: pass try: self.cl.deleteCPG(CPG_NAME1) except: pass try: self.cl.deleteCPG(CPG_NAME2) except: pass super(HP3ParClientVolumeTestCase, self).tearDown() def test_1_create_volume(self): self.printHeader('create_volume') try: #add one optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) except Exception as ex: print(ex) self.fail('Failed to create volume') return try: #check vol1 = self.cl.getVolume(VOLUME_NAME1) self.assertIsNotNone(vol1) volName = vol1['name'] self.assertEqual(VOLUME_NAME1, volName) except Exception as ex: print(ex) self.fail('Failed to get volume') return try: #add another optional = {'comment': 'test volume2', 'tpvv': True} self.cl.createVolume(VOLUME_NAME2, CPG_NAME2, 1024, optional) except Exception as ex: print(ex) self.fail('Failed to create volume') return try: #check vol2 = self.cl.getVolume(VOLUME_NAME2) self.assertIsNotNone(vol2) volName = vol2['name'] comment = vol2['comment'] self.assertEqual(VOLUME_NAME2, volName) self.assertEqual("test volume2", comment) except Exception as ex: print(ex) self.fail("Failed to get volume") self.printFooter('create_volume') def test_1_create_volume_badParams(self): self.printHeader('create_volume_badParams') try: name = VOLUME_NAME1 cpgName = CPG_NAME1 optional = {'id': 4, 'comment': 'test volume', 'badPram': True} self.cl.createVolume(name, cpgName, 1024, optional) except exceptions.HTTPBadRequest: print("Expected exception") self.printFooter('create_volume_badParams') return except Exception as ex: print(ex) self.fail("Failed with unexpected exception") self.fail('No exception occurred.') def test_1_create_volume_duplicate_name(self): self.printHeader('create_volume_duplicate_name') #add one and check try: optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) except Exception as ex: print(ex) self.fail("Failed to create volume") try: self.cl.createVolume(VOLUME_NAME1, CPG_NAME2, 1024, optional) except exceptions.HTTPConflict: print("Expected exception") self.printFooter('create_volume_duplicate_name') return except Exception as ex: print(ex) self.fail("Failed with unexpected exception") self.fail('No exception occurred.') def test_1_create_volume_tooLarge(self): self.printHeader('create_volume_tooLarge') try: optional = {'id': 3, 'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 16777218, optional) except exceptions.HTTPBadRequest: print("Expected exception") self.printFooter('create_volume_tooLarge') return except Exception as ex: print(ex) self.fail("Failed with unexpected exception") self.fail('No exception occurred') def test_1_create_volume_duplicate_ID(self): self.printHeader('create_volume_duplicate_ID') try: optional = {'id': 10000, 'comment': 'first volume'} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) except Exception as ex: print(ex) self.fail('Failed to create volume') try: optional2 = {'id': 10000, 'comment': 'volume with duplicate ID'} self.cl.createVolume(VOLUME_NAME2, CPG_NAME2, 1024, optional2) except exceptions.HTTPConflict: print('Expected exception') self.printFooter('create_volume_duplicate_ID') return except Exception as ex: print(ex) self.fail('Failed with unexpected exception') self.fail('No exception occurred') def test_1_create_volume_longName(self): self.printHeader('create_volume_longName') try: optional = {'id': 5} LongName = 'ThisVolumeNameIsWayTooLongToMakeAnySenseAndIsDeliberatelySo' self.cl.createVolume(LongName, CPG_NAME1, 1024, optional) except exceptions.HTTPBadRequest: print('Expected exception') self.printFooter('create_volume_longName') return except Exception as ex: print(ex) self.fail('Failed with unexpected exception') self.fail('No exception occurred.') def test_2_get_volume_bad(self): self.printHeader('get_volume_bad') try: self.cl.getVolume('NoSuchVolume') except exceptions.HTTPNotFound: print("Expected exception") self.printFooter('get_volume_bad') return except Exception as ex: print(ex) self.fail("Failed with unexpected exception") self.fail("No exception occurred") def test_2_get_volumes(self): self.printHeader('get_volumes') self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024) self.cl.createVolume(VOLUME_NAME2, CPG_NAME1, 1024) vol1 = self.cl.getVolume(VOLUME_NAME1) vol2 = self.cl.getVolume(VOLUME_NAME2) vols = self.cl.getVolumes() self.assertTrue(self.findInDict(vols['members'], 'name', vol1['name'])) self.assertTrue(self.findInDict(vols['members'], 'name', vol2['name'])) self.printFooter('get_volumes') def test_3_delete_volume_nonExist(self): self.printHeader('delete_volume_nonExist') try: self.cl.deleteVolume(VOLUME_NAME1) except exceptions.HTTPNotFound: print("Expected exception") self.printFooter('delete_volume_nonExist') return except Exception as ex: print(ex) self.fail("Failed with unexpected exception") self.fail('No exception occurred.') def test_3_delete_volumes(self): self.printHeader('delete_volumes') try: optional = {'comment': 'Made by flask.'} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) self.cl.getVolume(VOLUME_NAME1) except Exception as ex: print(ex) self.fail('Failed to create volume') try: optional = {'comment': 'Made by flask.'} self.cl.createVolume(VOLUME_NAME2, CPG_NAME1, 1024, optional) self.cl.getVolume(VOLUME_NAME2) except Exception as ex: print(ex) self.fail('Failed to create volume') try: self.cl.deleteVolume(VOLUME_NAME1) except Exception as ex: print(ex) self.fail('Failed to delete %s' % (VOLUME_NAME1)) try: self.cl.getVolume(VOLUME_NAME1) except exceptions.HTTPNotFound: print('Expected exception') except Exception as ex: print(ex) self.fail('Failed with unexpected exception') try: self.cl.deleteVolume(VOLUME_NAME2) except Exception as ex: print(ex) self.fail('Failed to delete %s' % (VOLUME_NAME2)) try: self.cl.getVolume(VOLUME_NAME2) except exceptions.HTTPNotFound: print('Expected exception') self.printFooter('delete_volumes') return except Exception as ex: print(ex) self.fail('Failed with unexpected exception') def test_4_create_snapshot_no_optional(self): self.printHeader('create_snapshot_no_optional') try: optional = {'snapCPG': CPG_NAME1} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) #add one self.cl.createSnapshot(SNAP_NAME1, VOLUME_NAME1) #no API to get and check except Exception as ex: print(ex) self.fail("Failed with unexpected exception") self.cl.deleteVolume(SNAP_NAME1) self.printFooter('create_snapshot_no_optional') def test_4_create_snapshot(self): self.printHeader('create_snapshot') try: optional = {'snapCPG': CPG_NAME1} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) #add one optional = {'expirationHours': 300} self.cl.createSnapshot(SNAP_NAME1, VOLUME_NAME1, optional) #no API to get and check except Exception as ex: print(ex) self.fail("Failed with unexpected exception") self.cl.deleteVolume(SNAP_NAME1) self.printFooter('create_snapshot') def test_4_create_snapshot_badParams(self): self.printHeader('create_snapshot_badParams') #add one optional = {'snapCPG': CPG_NAME1} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) try: optional = {'Bad': True, 'expirationHours': 300} self.cl.createSnapshot(SNAP_NAME1, VOLUME_NAME1, optional) except exceptions.HTTPBadRequest: print("Expected exception") self.printFooter('create_snapshot_badParams') return except Exception as ex: print(ex) self.fail("Failed with unexpected exception") self.fail("No exception occurred.") def test_4_create_snapshot_nonExistVolume(self): self.printHeader('create_snapshot_nonExistVolume') #add one try: name = 'UnitTestSnapshot' volName = 'NonExistVolume' optional = {'id': 1, 'comment': 'test snapshot', 'readOnly': True, 'expirationHours': 300} self.cl.createSnapshot(name, volName, optional) except exceptions.HTTPNotFound: print("Expected exception") self.printFooter('create_snapshot_nonExistVolume') return except Exception as ex: print(ex) self.fail("Failed with unexpected exception") self.fail("No exception occurred.") def test_5_grow_volume(self): self.printHeader('grow_volume') try: #add one optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) except Exception as ex: print(ex) self.fail('Failed to create volume') return try: #grow it result = self.cl.growVolume(VOLUME_NAME1, 1) except Exception as ex: print(ex) self.fail('Failed to grow volume') return try: result = self.cl.getVolume(VOLUME_NAME1) size_after = result['sizeMiB'] self.assertGreater(size_after, 1024) except Exception as ex: print(ex) self.fail('Failed to get volume after growth') return self.printFooter('grow_volume') def test_5_grow_volume_bad(self): self.printHeader('grow_volume_bad') try: #add one optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) except Exception as ex: print(ex) self.fail('Failed to create volume') return try: #shrink it self.cl.growVolume(VOLUME_NAME1, -1) #3par is returning 409 instead of 400 except exceptions.HTTPBadRequest as ex: print("Expected exception") self.printFooter('grow_volume_bad') return except exceptions.HTTPConflict as ex: print("Expected exception") self.printFooter('grow_volume_bad') return except Exception as ex: print(ex) self.fail("Failed with unexpected exception") self.fail("No exception occurred.") def test_6_copy_volume(self): self.printHeader('copy_volume') # TODO: Add support for ssh/stopPhysical copy in mock mode if self.unitTest: self.printFooter('copy_volume') return try: #add one optional = {'comment': 'test volume', 'tpvv': True, 'snapCPG': CPG_NAME1} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) except Exception as ex: print ex self.fail('Failed to create volume') return try: #copy it optional = {'online': True} self.cl.copyVolume(VOLUME_NAME1, VOLUME_NAME2, CPG_NAME1, optional) except Exception as ex: print ex self.fail('Failed to copy volume') return try: result = self.cl.getVolume(VOLUME_NAME2) except Exception as ex: print ex self.fail('Failed to get cloned volume') return try: self.cl.stopOnlinePhysicalCopy(VOLUME_NAME2) except Exception as ex: print ex self.fail('Failed to stop physical copy. ' + 'This may negatively impact other tests and require manual cleanup!') return try: result = self.cl.getVolume(VOLUME_NAME2) self.fail("Expecting exception, but found 'deleted' volume") except exceptions.HTTPNotFound as ex: self.printFooter('copy_volume') return except Exception as ex: print ex self.fail('Unexpected exception') self.fail('Expecting HTTPNotFound exception') def test_7_create_volume_set(self): self.printHeader('create_volume_set') try: self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=DOMAIN, comment="Unit test volume set 1") except Exception as ex: print(ex) self.fail('Failed to create volume set') return try: resp = self.cl.getVolumeSet(VOLUME_SET_NAME1) print(resp) except Exception as ex: print(ex) self.fail('Failed to get volume set') return self.printFooter('create_volume_set') def test_7_create_volume_set_with_volumes(self): self.printHeader('create_volume_set') try: optional = {'comment': 'test volume 1', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) optional = {'comment': 'test volume 2', 'tpvv': True} self.cl.createVolume(VOLUME_NAME2, CPG_NAME1, 1024, optional) except Exception as ex: print(ex) self.fail('Failed to create volumes') return try: members = [VOLUME_NAME1, VOLUME_NAME2] self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=DOMAIN, comment="Unit test volume set 1", setmembers=members) except Exception as ex: print(ex) self.fail('Failed to create volume set with members') try: resp = self.cl.getVolumeSet(VOLUME_SET_NAME1) self.assertIsNotNone(resp) resp_members = resp['setmembers'] self.assertIn(VOLUME_NAME1, resp_members) self.assertIn(VOLUME_NAME2, resp_members) except Exception as ex: print(ex) self.fail('Failed to get volume set') return self.printFooter('create_volume_set') def test_7_create_volume_set_dup(self): self.printHeader('create_volume_set_dup') try: self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=DOMAIN, comment="Unit test volume set 1") except Exception as ex: print(ex) self.fail('Failed to create volume set') return try: # create it again self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=DOMAIN, comment="Unit test volume set 1") except exceptions.HTTPConflict as ex: print("expected exception") self.printFooter('create_volume_set_dup') return except Exception as ex: print(ex) self.fail("Failed with unexpected exception") self.fail("No exception occured") def test_8_get_volume_sets(self): self.printHeader('get_volume_sets') try: self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=DOMAIN, comment="Unit test volume set 1") self.cl.createVolumeSet(VOLUME_SET_NAME2, domain=DOMAIN) except Exception as ex: print(ex) self.fail('Failed to create volume set') return try: sets = self.cl.getVolumeSets() self.assertIsNotNone(sets) set_names = [vset['name'] for vset in sets['members']] self.assertIn(VOLUME_SET_NAME1, set_names) self.assertIn(VOLUME_SET_NAME2, set_names) except Exception as ex: print(ex) self.fail('Failed to get volume sets') return self.printFooter('get_volume_sets') def test_9_del_volume_set_empty(self): self.printHeader('del_volume_set_empty') try: self.cl.createVolumeSet(VOLUME_SET_NAME2, domain=DOMAIN) except Exception as ex: print(ex) self.fail('Failed to create volume set') return try: self.cl.deleteVolumeSet(VOLUME_SET_NAME2) except Exception as ex: print(ex) self.fail('Failed to delete volume set') return self.printFooter('del_volume_set_empty') def test_9_del_volume_set_with_volumes(self): self.printHeader('delete_volume_set_with_volumes') try: optional = {'comment': 'test volume 1', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) optional = {'comment': 'test volume 2', 'tpvv': True} self.cl.createVolume(VOLUME_NAME2, CPG_NAME1, 1024, optional) except Exception as ex: print(ex) self.fail('Failed to create volumes') return try: members = [VOLUME_NAME1, VOLUME_NAME2] self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=DOMAIN, comment="Unit test volume set 1", setmembers=members) except Exception as ex: print(ex) self.fail('Failed to create volume set with members') try: self.cl.deleteVolumeSet(VOLUME_SET_NAME1) except Exception as ex: print(ex) self.fail('Failed to delete volume set') return self.printFooter('delete_volume_set_with_volumes') def test_10_modify_volume_set_change_name(self): self.printHeader('modify_volume_set_change_name') try: self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=DOMAIN, comment="First") self.cl.modifyVolumeSet(VOLUME_SET_NAME1, newName=VOLUME_SET_NAME2) vset = self.cl.getVolumeSet(VOLUME_SET_NAME2) self.assertEqual("First", vset['comment']) except Exception as ex: print(ex) self.fail('Failed to create or change name of volume set') self.printFooter('modify_volume_set_change_name') def test_10_modify_volume_set_change_comment(self): self.printHeader('modify_volume_set_change_comment') try: self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=DOMAIN, comment="First") self.cl.modifyVolumeSet(VOLUME_SET_NAME1, comment="Second") vset = self.cl.getVolumeSet(VOLUME_SET_NAME1) self.assertEqual("Second", vset['comment']) except Exception as ex: print(ex) self.fail('Failed to create or change comment of volume set') self.printFooter('modify_volume_set_change_comment') pass def test_10_modify_volume_set_add_members_to_empty(self): self.printHeader('modify_volume_set_add_members_to_empty') try: optional = {'comment': 'test volume 1', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) optional = {'comment': 'test volume 2', 'tpvv': True} self.cl.createVolume(VOLUME_NAME2, CPG_NAME1, 1024, optional) except Exception as ex: print(ex) self.fail('Failed to create volume set') try: self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=DOMAIN, comment="Unit test volume set 1") except Exception as ex: print(ex) self.fail('Failed to create volume set') return try: members = [VOLUME_NAME1, VOLUME_NAME2] self.cl.modifyVolumeSet(VOLUME_SET_NAME1, self.cl.SET_MEM_ADD, setmembers=members) except Exception as ex: print(ex) self.fail('Failed to add volumes') return try: resp = self.cl.getVolumeSet(VOLUME_SET_NAME1) print(resp) self.assertTrue(VOLUME_NAME1 in resp['setmembers']) self.assertTrue(VOLUME_NAME2 in resp['setmembers']) except Exception as ex: print(ex) self.fail('Failed to add volumes to volume set') return self.printFooter('modify_volume_set_add_members_to_empty') def test_10_modify_volume_set_add_members(self): self.printHeader('modify_volume_set_add_members') try: optional = {'comment': 'test volume 1', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) optional = {'comment': 'test volume 2', 'tpvv': True} self.cl.createVolume(VOLUME_NAME2, CPG_NAME1, 1024, optional) except Exception as ex: print(ex) self.fail('Failed to create volume set') try: members = [VOLUME_NAME1] self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=DOMAIN, setmembers=members, comment="Unit test volume set 1") except Exception as ex: print(ex) self.fail('Failed to create volume set') return try: members = [VOLUME_NAME2] self.cl.modifyVolumeSet(VOLUME_SET_NAME1, self.cl.SET_MEM_ADD, setmembers=members) except Exception as ex: print(ex) self.fail('Failed to add volumes') return try: resp = self.cl.getVolumeSet(VOLUME_SET_NAME1) print(resp) self.assertTrue(VOLUME_NAME1 in resp['setmembers']) self.assertTrue(VOLUME_NAME2 in resp['setmembers']) except Exception as ex: print(ex) self.fail('Failed to add volumes to volume set') return self.printFooter('modify_volume_set_add_members') def test_10_modify_volume_set_del_members(self): self.printHeader('modify_volume_del_members') try: optional = {'comment': 'test volume 1', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) optional = {'comment': 'test volume 2', 'tpvv': True} self.cl.createVolume(VOLUME_NAME2, CPG_NAME1, 1024, optional) members = [VOLUME_NAME1, VOLUME_NAME2] self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=DOMAIN, comment="Unit test volume set 1", setmembers=members) except Exception as ex: print(ex) self.fail('Failed to create volume set') return try: members = [VOLUME_NAME1] self.cl.modifyVolumeSet(VOLUME_SET_NAME1, action=self.cl.SET_MEM_REMOVE, setmembers=members) except Exception as ex: print(ex) self.fail('Failed to remove volumes from set') return try: resp = self.cl.getVolumeSet(VOLUME_SET_NAME1) self.assertIsNotNone(resp) resp_members = resp['setmembers'] self.assertNotIn(VOLUME_NAME1, resp_members) self.assertIn(VOLUME_NAME2, resp_members) except Exception as ex: print(ex) self.fail('Failed to get volume set') return self.printFooter('modify_volume_del_members') def _create_vv_sets(self): optional = {'comment': 'test volume 1', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) optional = {'comment': 'test volume 2', 'tpvv': True} self.cl.createVolume(VOLUME_NAME2, CPG_NAME1, 1024, optional) members = [VOLUME_NAME1, VOLUME_NAME2] self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=DOMAIN, comment="Unit test volume set 1", setmembers=members) def test_11_add_qos(self): self.printHeader('add_qos') self._create_vv_sets() qos = {'bwMinGoalKB': 1024, 'bwMaxLimitKB': 1024} try: self.cl.createQoSRules(VOLUME_SET_NAME1, qos) except Exception as ex: print(ex) self.fail('Failed to add qos') return try: rule = self.cl.queryQoSRule(VOLUME_SET_NAME1) except Exception as ex: print(ex) self.fail('Failed to query qos') self.assertIsNotNone(rule) self.assertEquals(rule['bwMinGoalKB'], qos['bwMinGoalKB']) self.assertEquals(rule['bwMaxLimitKB'], qos['bwMaxLimitKB']) self.printFooter('add_qos') def test_12_modify_qos(self): self.printHeader('modify_qos') self._create_vv_sets() qos_before = {'bwMinGoalKB': 1024, 'bwMaxLimitKB': 1024} qos_after = {'bwMinGoalKB': 1024, 'bwMaxLimitKB': 2048} try: self.cl.createQoSRules(VOLUME_SET_NAME1, qos_before) self.cl.modifyQoSRules(VOLUME_SET_NAME1, qos_after) except Exception as ex: print(ex) self.fail('Failed to modify qos') return try: rule = self.cl.queryQoSRule(VOLUME_SET_NAME1) except Exception as ex: print(ex) self.fail('Failed to query qos') self.assertIsNotNone(rule) self.assertEquals(rule['bwMinGoalKB'], qos_after['bwMinGoalKB']) self.assertEquals(rule['bwMaxLimitKB'], qos_after['bwMaxLimitKB']) self.printFooter('modify_qos') def test_13_delete_qos(self): self.printHeader('delete_qos') self._create_vv_sets() self.cl.createVolumeSet(VOLUME_SET_NAME2) qos1 = {'bwMinGoalKB': 1024, 'bwMaxLimitKB': 1024} qos2 = {'bwMinGoalKB': 512, 'bwMaxLimitKB': 2048} try: self.cl.createQoSRules(VOLUME_SET_NAME1, qos1) self.cl.createQoSRules(VOLUME_SET_NAME2, qos2) all_qos = self.cl.queryQoSRules() self.assertGreaterEqual(all_qos['total'], 2) self.assertIn(VOLUME_SET_NAME1, [qos['name'] for qos in all_qos['members']]) self.assertIn(VOLUME_SET_NAME2, [qos['name'] for qos in all_qos['members']]) except Exception as ex: print(ex) self.fail('Failed to create/query qos') return try: self.cl.deleteQoSRules(VOLUME_SET_NAME1) all_qos = self.cl.queryQoSRules() except Exception as ex: print(ex) self.fail("Failed to delete/query qos") return self.assertIsNotNone(all_qos) self.assertNotIn(VOLUME_SET_NAME1, [qos['name'] for qos in all_qos['members']]) self.assertIn(VOLUME_SET_NAME2, [qos['name'] for qos in all_qos['members']]) self.printFooter('delete_qos') def test_14_modify_volume_rename(self): self.printHeader('modify volume') try: #add one optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) except Exception as ex: print(ex) self.fail('Failed to create volume') return try: volumeMod = {'newName': VOLUME_NAME2} self.cl.modifyVolume(VOLUME_NAME1, volumeMod) vol2 = self.cl.getVolume(VOLUME_NAME2) self.assertIsNotNone(vol2) self.assertEqual(vol2['comment'], optional['comment']) except Exception as ex: print(ex) self.fail('Failed to modify volume') return self.printFooter('modify volume') #testing #suite = unittest.TestLoader().loadTestsFromTestCase(HP3ParClientVolumeTestCase) #unittest.TextTestRunner(verbosity=2).run(suite) hp3parclient-3.0.0/test/test_HP3ParClient_CPG.py0000664000175000017500000001226012262623412021622 0ustar stackstack00000000000000# vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2009-2012 10gen, Inc. # # 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. """Test class of 3Par Client handling CPG""" import sys, os sys.path.insert(0,os.path.realpath(os.path.abspath('../'))) from hp3parclient import client, exceptions import unittest import test_HP3ParClient_base DOMAIN = 'UNIT_TEST_DOMAIN' CPG_NAME1 = 'CPG1_UNIT_TEST' CPG_NAME2 = 'CPG2_UNIT_TEST' class HP3ParClientCPGTestCase(test_HP3ParClient_base.HP3ParClientBaseTestCase): def setUp(self): super(HP3ParClientCPGTestCase, self).setUp() def tearDown(self): try : self.cl.deleteCPG(CPG_NAME1) except : pass try : self.cl.deleteCPG(CPG_NAME2) except : pass # very last, tear down base class super(HP3ParClientCPGTestCase, self).tearDown() def test_1_create_CPG(self): self.printHeader('create_CPG') #add one optional = {'domain': DOMAIN} name = CPG_NAME1 self.cl.createCPG(name, optional) #check cpg1 = self.cl.getCPG(name) self.assertIsNotNone(cpg1) cpgName = cpg1['name'] self.assertEqual(name, cpgName) #add another name = CPG_NAME2 optional2 = optional.copy() more_optional = {'LDLayout':{'RAIDType':2}} optional2.update(more_optional) self.cl.createCPG(name, optional2) #check cpg2 = self.cl.getCPG(name) self.assertIsNotNone(cpg2) cpgName = cpg2['name'] self.assertEqual(name, cpgName) self.printFooter('create_CPG') def test_1_create_CPG_badDomain(self): self.printHeader('create_CPG_badDomain') optional = {'domain': 'BAD_DOMAIN'} self.assertRaises(exceptions.HTTPNotFound, self.cl.createCPG, CPG_NAME1, optional) self.printFooter('create_CPG_badDomain') def test_1_create_CPG_dup(self): self.printHeader('create_CPG_dup') optional = {'domain': DOMAIN} name = CPG_NAME1 self.cl.createCPG(name, optional) self.assertRaises(exceptions.HTTPConflict, self.cl.createCPG, CPG_NAME1, optional) self.printFooter('create_CPG_dup') def test_1_create_CPG_badParams(self): self.printHeader('create_CPG_badParams') optional = {'domainBad': 'UNIT_TEST'} self.assertRaises(exceptions.HTTPBadRequest, self.cl.createCPG, CPG_NAME1, optional) self.printFooter('create_CPG_badParams') def test_1_create_CPG_badParams2(self): self.printHeader('create_CPG_badParams2') optional = {'domain': 'UNIT_TEST'} more_optional = {'LDLayout':{'RAIDBadType':1}} optional.update(more_optional) self.assertRaises(exceptions.HTTPBadRequest, self.cl.createCPG, CPG_NAME1, optional) self.printFooter('create_CPG_badParams2') def test_2_get_CPG_bad(self): self.printHeader('get_CPG_bad') self.assertRaises(exceptions.HTTPNotFound, self.cl.getCPG, 'BadName') self.printFooter('get_CPG_bad') def test_2_get_CPGs(self): self.printHeader('get_CPGs') optional = {'domain': DOMAIN} name = CPG_NAME1 self.cl.createCPG(name, optional) cpgs = self.cl.getCPGs() self.assertGreater(len(cpgs), 0, 'getCPGs failed with no CPGs') self.assertTrue(self.findInDict(cpgs['members'], 'name', CPG_NAME1)) self.printFooter('get_CPGs') def test_3_delete_CPG_nonExist(self): self.printHeader('delete_CPG_nonExist') self.assertRaises(exceptions.HTTPNotFound, self.cl.deleteCPG, 'NonExistCPG') self.printFooter('delete_CPG_nonExist') def test_3_delete_CPGs(self): self.printHeader('delete_CPGs') #add one optional = {'domain': DOMAIN} self.cl.createCPG(CPG_NAME1, optional) cpg = self.cl.getCPG(CPG_NAME1) self.assertTrue(cpg['name'], CPG_NAME1) cpgs = self.cl.getCPGs() if cpgs and cpgs['total'] > 0: for cpg in cpgs['members']: if cpg['name'] == CPG_NAME1: #pprint.pprint("Deleting CPG %s " % cpg['name']) self.cl.deleteCPG(cpg['name']) #check self.assertRaises(exceptions.HTTPNotFound, self.cl.getCPG, CPG_NAME1) self.printFooter('delete_CPGs') #testing #suite = unittest.TestLoader().loadTestsFromTestCase(HP3ParClientCPGTestCase) #unittest.TextTestRunner(verbosity=2).run(suite) hp3parclient-3.0.0/test/test_HP3ParClient_host.py0000664000175000017500000005766312276214354022214 0ustar stackstack00000000000000 # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2009-2012 10gen, Inc. # # 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. """Test class of 3Par Client handling Host""" import sys, os sys.path.insert(0,os.path.realpath(os.path.abspath('../'))) from hp3parclient import client, exceptions import unittest import test_HP3ParClient_base DOMAIN = 'UNIT_TEST_DOMAIN' HOST_NAME1 = 'HOST1_UNIT_TEST' HOST_NAME2 = 'HOST2_UNIT_TEST' class HP3ParClientHostTestCase(test_HP3ParClient_base.HP3ParClientBaseTestCase): def setUp(self): super(HP3ParClientHostTestCase, self).setUp() def tearDown(self): try : self.cl.deleteHost(HOST_NAME1) except : pass try : self.cl.deleteHost(HOST_NAME2) except : pass # very last, tear down base class super(HP3ParClientHostTestCase, self).tearDown() def test_1_create_host_badParams(self): self.printHeader('create_host_badParams') name = 'UnitTestHostBadParams' optional = {'iSCSIPaths': 'foo bar'} self.assertRaises(exceptions.HTTPBadRequest, self.cl.createHost, name, None, None, optional) self.printFooter('create_host_badParams') def test_1_create_host_no_name(self): self.printHeader('create_host_no_name') optional = {'domain' : 'default'} self.assertRaises(exceptions.HTTPBadRequest, self.cl.createHost, None, None, None, optional) self.printFooter('create_host_no_name') def test_1_create_host_exceed_length(self): self.printHeader('create_host_exceed_length') optional = {'domain': 'ThisDomainNameIsWayTooLongToMakeAnySense'} self.assertRaises(exceptions.HTTPBadRequest, self.cl.createHost, HOST_NAME1, None, None, optional) self.printFooter('create_host_exceed_length') def test_1_create_host_empty_domain(self): self.printHeader('create_host_empty_domain') optional={'domain': ''} self.assertRaises(exceptions.HTTPBadRequest, self.cl.createHost, HOST_NAME1, None, None, optional) self.printFooter('create_host_empty_domain') def test_1_create_host_illegal_string(self): self.printHeader('create_host_illegal_string') optional = {'domain' : 'doma&n'} self.assertRaises(exceptions.HTTPBadRequest, self.cl.createHost, HOST_NAME1, None, None, optional) self.printFooter('create_host_illegal_string') def test_1_create_host_param_conflict(self): self.printHeader('create_host_param_conflict') optional = {'domain': DOMAIN} fc = ['00:00:00:00:00:00:00:00', '11:11:11:11:11:11:11:11'] iscsi = ['iqn.1993-08.org.debian:01:00000000000', 'iqn.bogus.org.debian:01:0000000000'] self.assertRaises(exceptions.HTTPBadRequest, self.cl.createHost, HOST_NAME1, iscsi, fc, optional) self.printFooter('create_host_param_conflict') def test_1_create_host_wrong_type(self): self.printHeader('create_host_wrong_type') optional = {'domain': DOMAIN} fc = ['00:00:00:00:00:00:00'] self.assertRaises(exceptions.HTTPBadRequest, self.cl.createHost, HOST_NAME1, None, fc, optional) self.printFooter('create_host_wrong_type') def test_1_create_host_existent_path(self): self.printHeader('create_host_existent_path') optional = {'domain':DOMAIN} fc = ['00:00:00:00:00:00:00:00', '11:11:11:11:11:11:11:11'] self.cl.createHost(HOST_NAME1, None, fc, optional) self.assertRaises(exceptions.HTTPConflict, self.cl.createHost, HOST_NAME2, None, fc, optional) self.printFooter('create_host_existent_path') def test_1_create_host_duplicate(self): self.printHeader('create_host_duplicate') optional = {'domain' : DOMAIN} self.cl.createHost(HOST_NAME1, None, None, optional) self.assertRaises(exceptions.HTTPConflict, self.cl.createHost, HOST_NAME1, None, None, optional) self.printFooter('create_host_duplicate') def test_1_create_host(self): self.printHeader('create_host') #add one optional = {'domain': DOMAIN} fc = ['00:00:00:00:00:00:00:00', '11:11:11:11:11:11:11:11'] self.cl.createHost(HOST_NAME1, None, fc, optional) #check host1 = self.cl.getHost(HOST_NAME1) self.assertIsNotNone(host1) name1 = host1['name'] self.assertEqual(HOST_NAME1, name1) #add another iscsi = ['iqn.1993-08.org.debian:01:00000000000', 'iqn.bogus.org.debian:01:0000000000'] self.cl.createHost(HOST_NAME2, iscsi, None, optional) #check host2 = self.cl.getHost(HOST_NAME2) self.assertIsNotNone(host2) name3 = host2['name'] self.assertEqual(HOST_NAME2, name3) self.printFooter('create_host') def test_1_create_host_no_optional(self): self.printHeader('create_host_no_optional') #add one fc = ['00:00:00:00:00:00:00:00', '11:11:11:11:11:11:11:11'] self.cl.createHost(HOST_NAME1, None, fc) #check host1 = self.cl.getHost(HOST_NAME1) self.assertIsNotNone(host1) name1 = host1['name'] self.assertEqual(HOST_NAME1, name1) self.printFooter('create_host_no_optional') def test_2_delete_host_nonExist(self): self.printHeader("delete_host_non_exist") self.assertRaises(exceptions.HTTPNotFound, self.cl.deleteHost, "UnitTestNonExistHost") self.printFooter("delete_host_non_exist") def test_2_delete_host(self): self.printHeader("delete_host") optional = {'domain': DOMAIN} fc = ['00:00:00:00:00:00:00:00', '11:11:11:11:11:11:11:11'] self.cl.createHost(HOST_NAME1, None, fc, optional) #check host1 = self.cl.getHost(HOST_NAME1) self.assertIsNotNone(host1) hosts = self.cl.getHosts() if hosts and hosts['total'] > 0: for host in hosts['members']: if 'name' in host and host['name'] == HOST_NAME1: self.cl.deleteHost(host['name']) self.assertRaises(exceptions.HTTPNotFound, self.cl.getHost, HOST_NAME1) self.printFooter("delete_host") def test_3_get_host_bad(self): self.printHeader("get_host_bad") self.assertRaises(exceptions.HTTPNotFound, self.cl.getHost, "BadHostName") self.printFooter("get_host_bad") def test_3_get_host_illegal(self): self.printHeader("get_host_illegal") self.assertRaises(exceptions.HTTPBadRequest, self.cl.getHost, "B&dHostName") self.printFooter("get_host_illegal") def test_3_get_hosts(self): self.printHeader("get_hosts") optional = {'domain': DOMAIN} fc = ['00:00:00:00:00:00:00:00', '11:11:11:11:11:11:11:11'] self.cl.createHost(HOST_NAME1, None, fc, optional) iscsi = ['iqn.1993-08.org.debian:01:00000000000', 'iqn.bogus.org.debian:01:0000000000'] self.cl.createHost(HOST_NAME2, iscsi, None, optional) hosts = self.cl.getHosts() self.assertGreaterEqual(hosts['total'], 2) host_names = [host['name'] for host in hosts['members'] if 'name' in host] self.assertIn(HOST_NAME1, host_names) self.assertIn(HOST_NAME2, host_names) def test_3_get_host(self): self.printHeader("get_host") self.assertRaises(exceptions.HTTPNotFound, self.cl.getHost, HOST_NAME1) optional = {'domain': DOMAIN} fc = ['00:00:00:00:00:00:00:00', '11:11:11:11:11:11:11:11'] self.cl.createHost(HOST_NAME1, None, fc, optional) host1 = self.cl.getHost(HOST_NAME1) self.assertEquals(host1['name'], HOST_NAME1) self.printFooter('get_host') def test_4_modify_host(self): self.printHeader('modify_host') self.assertRaises(exceptions.HTTPNotFound, self.cl.getHost, HOST_NAME1) self.assertRaises(exceptions.HTTPNotFound, self.cl.getHost, HOST_NAME2) optional = {'domain': DOMAIN} fc = ['00:00:00:00:00:00:00:00', '11:11:11:11:11:11:11:11'] self.cl.createHost(HOST_NAME1, None, fc, optional) # validate host was created host1 = self.cl.getHost(HOST_NAME1) self.assertEquals(host1['name'], HOST_NAME1) # change host name mod_request = {'newName' : HOST_NAME2} self.cl.modifyHost(HOST_NAME1, mod_request) # validate host name was changed host2 = self.cl.getHost(HOST_NAME2) self.assertEquals(host2['name'], HOST_NAME2) # host 1 name should be history self.assertRaises(exceptions.HTTPNotFound, self.cl.getHost, HOST_NAME1) self.printFooter('modfiy_host') def test_4_modify_host_no_name(self): self.printHeader('modify_host_no_name') mod_request = {'newName': HOST_NAME1} try: self.cl.modifyHost(None, mod_request) except exceptions.HTTPNotFound: #documentation error print('Expected exception') self.printFooter('modify_host_no_name') return except Exception as ex: print(ex) self.fail('Failed with unexpected exception.') self.fail('No exception occurred.') def test_4_modify_host_param_conflict(self): self.printHeader('modify_host_param_conflict') fc = ['00:00:00:00:00:00:00:00', '11:11:11:11:11:11:11:11'] iscsi = ['iqn.1993-08.org.debian:01:00000000000', 'iqn.bogus.org.debian:01:0000000000'] mod_request = {'newName': HOST_NAME1, 'FCWWNs': fc, 'iSCSINames': iscsi} try: self.cl.modifyHost(HOST_NAME2, mod_request) except exceptions.HTTPBadRequest: print('Expected exception') self.printFooter('modify_host_param_conflict') return except Exception as ex: print(ex) self.fail('Failed with unexpected exception.') self.fail('No exception occurred.') def test_4_modify_host_illegal_char(self): self.printHeader('modify_host_illegal_char') mod_request = { 'newName': 'New#O$TN@ME'} try: self.cl.modifyHost(HOST_NAME2, mod_request) except exceptions.HTTPBadRequest: print('Expected exception') self.printFooter('modify_host_illegal_char') return except Exception as ex: print(ex) self.fail('Failed with unexpected exception.') self.fail('No exception occurred.') def test_4_modify_host_pathOperation_missing1(self): self.printHeader('modify_host_pathOperation_missing1') fc = ['00:00:00:00:00:00:00:00', '11:11:11:11:11:11:11:11'] mod_request = {'FCWWNs': fc} try: self.cl.modifyHost(HOST_NAME1, mod_request) except exceptions.HTTPBadRequest: print('Expected exception') self.printFooter('modify_host_pathOperation_missing1') return except Exception as ex: print(ex) self.fail('Failed with unexpected exception.') self.fail('No exception occurred.') def test_4_modify_host_pathOperation_missing2(self): self.printHeader('modify_host_pathOperation_missing2') iscsi = ['iqn.1993-08.org.debian:01:00000000000', 'iqn.bogus.org.debian:01:0000000000'] mod_request = {'iSCSINames': iscsi} try: self.cl.modifyHost(HOST_NAME1, mod_request) except exceptions.HTTPBadRequest: print('Expected exception') self.printFooter('modify_host_pathOperation_missing2') return except Exception as ex: print(ex) self.fail('Failed with unexpected exception.') self.fail('No exception occurred.') def test_4_modify_host_pathOperationOnly(self): self.printHeader('modify_host_pathOperationOnly') mod_request = {'pathOperation': 1} try: self.cl.modifyHost(HOST_NAME2, mod_request) except exceptions.HTTPBadRequest: print('Expected exception') self.printFooter('modify_host_pathOperationOnly') return except Exception as ex: print(ex) self.fail('Failed with unexpected exception.') self.fail('No exception occurred.') def test_4_modify_host_too_long(self): self.printHeader('modify_host_too_long') optional = {'domain': DOMAIN} fc = ['00:00:00:00:00:00:00:00', '11:11:11:11:11:11:11:11'] self.cl.createHost(HOST_NAME1, None, fc, optional) mod_request = {'newName': 'ThisHostNameIsWayTooLongToMakeAnyRealSenseAndIsDeliberatelySo'} try: self.cl.modifyHost(HOST_NAME1, mod_request) except exceptions.HTTPBadRequest: print('Expected exception') self.printFooter('modify_host_too_long') self.cl.deleteHost(HOST_NAME1) return except Exception as ex: print(ex) self.fail('Failed with unexpected exception') self.fail('No exception occurred.') def test_4_modify_host_dup_newName(self): self.printHeader('modify_host_dup_newName') optional = {'domain': DOMAIN} fc = ['00:00:00:00:00:00:00:00', '11:11:11:11:11:11:11:11'] self.cl.createHost(HOST_NAME1, None, fc, optional) iscsi = ['iqn.1993-08.org.debian:01:00000000000', 'iqn.bogus.org.debian:01:0000000000'] self.cl.createHost(HOST_NAME2, iscsi, None, optional) mod_request = {'newName': HOST_NAME1} try: self.cl.modifyHost(HOST_NAME2, mod_request) except exceptions.HTTPConflict: print('Expected exception') self.cl.deleteHost(HOST_NAME1) self.cl.deleteHost(HOST_NAME2) self.printFooter('modify_host_dup_newName') return except Exception as ex: print(ex) self.fail('Failed with unexpected exception.') self.fail('No exception occurred.') def test_4_modify_host_nonExist(self): self.printHeader('modify_host_nonExist') try: self.cl.deleteHost(HOST_NAME1) except: pass try: mod_request = {'newName': HOST_NAME2} self.cl.modifyHost(HOST_NAME1, mod_request) except exceptions.HTTPNotFound: print('Expected exception') self.printFooter('modify_host_nonExist') return except Exception as ex: print(ex) self.fail('Failed with unexpected exception.') self.fail('No exception occurred.') def test_4_modify_host_existent_path(self): self.printHeader('modify_host_existent_path') optional = {'domain': DOMAIN} fc = ['00:00:00:00:00:00:00:00', '11:11:11:11:11:11:11:11'] iscsi = ['iqn.1993-08.org.debian:01:00000000000', 'iqn.bogus.org.debian:01:0000000000'] self.cl.createHost(HOST_NAME1, None, fc, optional) self.cl.createHost(HOST_NAME2, iscsi, None, optional) try: mod_request = {'pathOperation' : 1, 'iSCSINames': iscsi} self.cl.modifyHost(HOST_NAME1, mod_request) except exceptions.HTTPConflict: print('Expected exception') self.printFooter('modify_host_existent_path') return except Exception as ex: print(ex) self.fail('Failed with unexpected exception.') self.fail('No exception occurred.') def test_4_modify_host_nonExistent_path_iSCSI(self): self.printHeader('modify_host_nonExistent_path_iSCSI') optional = {'domain': DOMAIN} iscsi = ['iqn.1993-08.org.debian:01:00000000000'] self.cl.createHost(HOST_NAME1, iscsi, None, optional) iscsi2 = ['iqn.bogus.org.debian:01:0000000000'] mod_request = {'pathOperation': 2, 'iSCSINames': iscsi2} try: self.cl.modifyHost(HOST_NAME1, mod_request) except exceptions.HTTPNotFound: print('Expected exception') self.printFooter('modify_host_nonExistent_path_iSCSI') return except Exception as ex: print(ex) self.fail('Failed with unexpected exception.') self.fail('No exception occurred.') def test_4_modify_host_nonExistent_path_fc(self): self.printHeader('modify_host_nonExistent_path_fc') optional = {'domain': DOMAIN} fc = ['00:00:00:00:00:00:00:00'] self.cl.createHost(HOST_NAME1, None, fc, optional) fc2 = ['11:11:11:11:11:11:11:11'] mod_request = {'pathOperation': 2, 'FCWWNs': fc2} try: self.cl.modifyHost(HOST_NAME1, mod_request) except exceptions.HTTPNotFound: print('Expected exception') self.printFooter('modify_host_nonExistent_path_fc') return except Exception as ex: print(ex) self.fail('Failed with unexpected exception.') self.fail('No exception occurred.') def test_4_modify_host_add_fc(self): self.printHeader('modify_host_fc') optional = {'domain': DOMAIN} fc = ['00:00:00:00:00:00:00:00'] self.cl.createHost(HOST_NAME1, None, fc, optional) fc2 = ['11:11:11:11:11:11:11:11'] mod_request = {'pathOperation': 1, 'FCWWNs': fc2} try: self.cl.modifyHost(HOST_NAME1, mod_request) except Exception as ex: print(ex) self.fail('Failed with unexpected exception.') newHost = self.cl.getHost(HOST_NAME1) fc_paths = newHost['FCPaths'] for path in fc_paths: if path['wwn'] == '1111111111111111': self.printFooter('modify_host_add_fc') return self.fail('Failed to add FCWWN') def test_4_modify_host_remove_fc(self): self.printHeader('modify_host_remove_fc') optional = {'domain': DOMAIN} fc = ['00:00:00:00:00:00:00:00'] self.cl.createHost(HOST_NAME1, None, fc, optional) mod_request = {'pathOperation': 2, 'FCWWNs': fc} try: self.cl.modifyHost(HOST_NAME1, mod_request) except Exception as ex: print(ex) self.fail('Failed with unexpected exception.') newHost = self.cl.getHost(HOST_NAME1) fc_paths = newHost['FCPaths'] for path in fc_paths: if path['wwn'] == '0000000000000000': self.fail('Failed to remove FCWWN') return self.printFooter('modify_host_remove_fc') def test_4_modify_host_add_iscsi(self): self.printHeader('modify_host_add_iscsi') try: self.cl.deleteHost(HOST_NAME1) except: pass optional = {'domain': DOMAIN} iscsi = ['iqn.1993-08.org.debian:01:00000000000'] self.cl.createHost(HOST_NAME1, iscsi, None, optional) iscsi2 = ['iqn.bogus.org.debian:01:0000000000'] mod_request = {'pathOperation': 1, 'iSCSINames': iscsi2} try: self.cl.modifyHost(HOST_NAME1, mod_request) except Exception as ex: print(ex) self.fail('Failed with unexpected exception') newHost = self.cl.getHost(HOST_NAME1) iscsi_paths = newHost['iSCSIPaths'] for path in iscsi_paths: print(path) if path['name'] == "iqn.bogus.org.debian:01:0000000000": self.printFooter('modify_host_add_iscsi') return self.fail('Failed to add iSCSI') def test_4_modify_host_remove_iscsi(self): self.printHeader('modify_host_remove_iscsi') optional = {'domain': DOMAIN} iscsi = ['iqn.1993-08.org.debian:01:00000000000'] self.cl.createHost(HOST_NAME1, iscsi, None, optional) mod_request = {'pathOperation': 2, 'iSCSINames': iscsi} try: self.cl.modifyHost(HOST_NAME1, mod_request) except Exception as ex: print(ex) self.fail('Failed with unexpected exception') newHost = self.cl.getHost(HOST_NAME1) iscsi_paths = newHost['iSCSIPaths'] for path in iscsi_paths: if path['name'] == 'iqn.bogus.org.debian:01:0000000000': self.fail('Failed to remove iSCSI') return self.printFooter('modify_host_remove_iscsi') def test_5_query_host_iqn(self): self.printHeader('query_host_iqn') optional = {'domain': DOMAIN} iscsi = ['iqn.1993-08.org.debian:01:00000000000'] self.cl.createHost(HOST_NAME1, iscsi, None, optional) hosts = self.cl.queryHost(iqns = [iscsi.pop()]) self.assertIsNotNone(hosts) self.assertEqual(1, hosts['total']) self.assertEqual(hosts['members'].pop()['name'], HOST_NAME1) self.printFooter('query_host_iqn') def test_5_query_host_iqn2(self): # TODO test multiple iqns in one query pass def test_5_query_host_wwn(self): self.printHeader('query_host_wwn') optional = {'domain': DOMAIN} fc = ['00:00:00:00:00:00:00:00'] self.cl.createHost(HOST_NAME1, None, fc, optional) hosts = self.cl.queryHost(wwns=[fc.pop().replace(':', '')]) self.assertIsNotNone(hosts) self.assertEqual(1, hosts['total']) self.assertEqual(hosts['members'].pop()['name'], HOST_NAME1) self.printFooter('query_host_wwn') def test_5_query_host_wwn2(self): # TODO test multiple wwns in one query pass def test_5_query_host_iqn_and_wwn(self): self.printHeader('query_host_iqn_and_wwn') optional = {'domain': DOMAIN} iscsi = ['iqn.1993-08.org.debian:01:00000000000'] self.cl.createHost(HOST_NAME1, iscsi, None, optional) fc = ['00:00:00:00:00:00:00:00'] self.cl.createHost(HOST_NAME2, None, fc, optional) hosts = self.cl.queryHost(iqns=["iqn.1993-08.org.debian:01:00000000000"], wwns=["00:00:00:00:00:00:00:00".replace(':', '')]) self.assertIsNotNone(hosts) self.assertEqual(2, hosts['total']) self.assertIn(HOST_NAME1, [host['name'] for host in hosts['members']]) self.assertIn(HOST_NAME2, [host['name'] for host in hosts['members']]) self.printFooter('query_host_iqn_and_wwn') hp3parclient-3.0.0/test/test_HP3ParClient_base.py0000664000175000017500000001034512276214354022133 0ustar stackstack00000000000000# vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2009-2012 10gen, Inc. # # 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. """Test base class of 3Par Client""" import sys, os sys.path.insert(0,os.path.realpath(os.path.abspath('../'))) from hp3parclient import client, exceptions import unittest import subprocess import time import pprint import inspect from testconfig import config try: # For Python 3.0 and later from urllib.parse import urlparse except ImportError: # Fall back to Python 2's urllib2 from urlparse import urlparse # pip install nose-testconfig # e.g. # nosetests test_HP3ParClient_host.py -v --tc-file config.ini class HP3ParClientBaseTestCase(unittest.TestCase): user = config['TEST']['user'] password = config['TEST']['pass'] flask_url = config['TEST']['flask_url'] url_3par = config['TEST']['3par_url'] debug = config['TEST']['debug'].lower() == 'true' unitTest = config['TEST']['unit'].lower() == 'true' def setUp(self, withSSH=False): cwd = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) if self.unitTest : self.printHeader('Using flask ' + self.flask_url) parsed_url = urlparse(self.flask_url) userArg = '-user=%s' % self.user passwordArg = '-password=%s' % self.password portArg = '-port=%s' % parsed_url.port script = 'test_HP3ParMockServer_flask.py' path = "%s/%s" % (cwd, script) try : self.mockServer = subprocess.Popen([sys.executable, path, userArg, passwordArg, portArg], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE ) except Exception as e: pass time.sleep(1) self.cl = client.HP3ParClient(self.flask_url) # SSH is not supported in flask, so not initializing # those tests are expected to fail else : self.printHeader('Using 3PAR ' + self.url_3par) self.cl = client.HP3ParClient(self.url_3par) if withSSH: # This seems to slow down the test cases, so only use this when # requested parsed_3par_url = urlparse(self.url_3par) ip = parsed_3par_url.hostname.split(':').pop() try: # Set the conn_timeout to None so that the ssh connections will # use the default transport values which will allow the test # case process to terminate after completing self.cl.setSSHOptions(ip, self.user, self.password, conn_timeout=None) except Exception as ex: print ex self.fail("failed to start ssh client") if self.debug : self.cl.debug_rest(True) self.cl.login(self.user, self.password) def tearDown(self): self.cl.logout() if self.unitTest : #TODO: it seems to kill all the process except the last one... #don't know why self.mockServer.kill() def printHeader(self, name): print("\n##Start testing '%s'" % name) def printFooter(self, name): print("##Compeleted testing '%s\n" % name) def findInDict(self, dic, key, value): for i in dic : if key in i and i[key] == value : return True hp3parclient-3.0.0/test/test_HP3ParMockServer_flask.py0000775000175000017500000013527512276214354023200 0ustar stackstack00000000000000#from flask import Flask, request, abort, make_response, session, escape from flask import * import re import pprint import json import os import random import string import argparse import uuid from werkzeug.exceptions import default_exceptions from werkzeug.exceptions import HTTPException parser = argparse.ArgumentParser() parser.add_argument("-debug", help="Turn on http debugging", default=False, action="store_true") parser.add_argument("-user", help="User name") parser.add_argument("-password", help="User password") parser.add_argument("-port", help="Port to listen on", type=int, default=5000) args = parser.parse_args() user_name = args.user user_pass = args.password debugRequests = False if "debug" in args and args.debug: debugRequests = True #__all__ = ['make_json_app'] def id_generator(size=6, chars=string.ascii_uppercase + string.digits): return ''.join(random.choice(chars) for x in range(size)) def make_json_app(import_name, **kwargs): """ Create a JSON-oriented Flask app. All error responses that you don't specifically manage yourself will have application/json content type, and will contain JSON like this (just an example): { "message": "405: Method Not Allowed" } """ def make_json_error(ex): pprint.pprint(ex) # pprint.pprint(ex.code) response = jsonify(message=str(ex)) #response = jsonify(ex) response.status_code = (ex.code if isinstance(ex, HTTPException) else 500) pprint.pprint(response) return response app = Flask(import_name, **kwargs) #app.debug = True app.secret_key = id_generator(24) for code in default_exceptions.keys(): app.error_handler_spec[None][code] = make_json_error return app app = make_json_app(__name__) session_key = id_generator(24) def debugRequest(request): if debugRequests: print("\n") pprint.pprint(request) pprint.pprint(request.headers) pprint.pprint(request.data) def throw_error(http_code, error_code=None, desc=None, debug1=None, debug2=None): if error_code: info = {'code': error_code, 'desc': desc} if debug1: info['debug1'] = debug1 if debug2: info['debug2'] = debug2 abort(Response(json.dumps(info), status=http_code)) else: abort(http_code) @app.route('/') def index(): debugRequest(request) if 'username' in session: return 'Logged in as %s' % escape(session['username']) abort(401) @app.route('/api/v1/throwerror') def errtest(): debugRequest(request) throw_error(405, 'ERR_TEST', 'testing throwing an error', 'debug1 message', 'debug2 message') @app.errorhandler(404) def not_found(error): debugRequest(request) return Response("%s has not been implemented" % request.path, status=501) @app.route('/api/v1/credentials', methods=['GET', 'POST']) def credentials(): debugRequest(request) if request.method == 'GET': return 'GET credentials called' elif request.method == 'POST': data = json.loads(request.data) if data['user'] == user_name and data['password'] == user_pass: # do something good here try: resp = make_response(json.dumps({'key': session_key}), 201) resp.headers['Location'] = '/api/v1/credentials/%s' % session_key session['username'] = data['user'] session['password'] = data['password'] session['session_key'] = session_key return resp except Exception as ex: pprint.pprint(ex) else: # authentication failed! throw_error(401, "HTTP_AUTH_FAIL", "Username and or Password was incorrect") @app.route('/api/v1/credentials/', methods=['DELETE']) def logout_credentials(session_key): debugRequest(request) session.clear() return 'DELETE credentials called' #### CPG #### @app.route('/api/v1/cpgs', methods=['POST']) def create_cpgs(): debugRequest(request) data = json.loads(request.data) valid_keys = {'name': None, 'growthIncrementMB': None, 'growthLimitMB': None, 'usedLDWarningAlertMB': None, 'domain': None, 'LDLayout': None} valid_LDLayout_keys = {'RAIDType': None, 'setSize': None, 'HA': None, 'chuckletPosRef': None, 'diskPatterns': None} for key in list(data.keys()): if key not in list(valid_keys.keys()): throw_error(400, 'INV_INPUT', "Invalid Parameter '%s'" % key) elif 'LDLayout' in list(data.keys()): layout = data['LDLayout'] for subkey in list(layout.keys()): if subkey not in valid_LDLayout_keys: throw_error(400, 'INV_INPUT', "Invalid Parameter '%s'" % subkey) if 'domain' in data and data['domain'] == 'BAD_DOMAIN': throw_error(404, 'NON_EXISTENT_DOMAIN', "Non-existing domain specified.") for cpg in cpgs['members']: if data['name'] == cpg['name']: throw_error(409, 'EXISTENT_CPG', "CPG '%s' already exist." % data['name']) cpgs['members'].append(data) cpgs['total'] = cpgs['total'] + 1 return make_response("", 200) @app.route('/api/v1/cpgs', methods=['GET']) def get_cpgs(): debugRequest(request) # should get it from global cpgs resp = make_response(json.dumps(cpgs), 200) return resp @app.route('/api/v1/cpgs/', methods=['GET']) def get_cpg(cpg_name): debugRequest(request) for cpg in cpgs['members']: if cpg['name'] == cpg_name: resp = make_response(json.dumps(cpg), 200) return resp throw_error(404, 'NON_EXISTENT_CPG', "CPG '%s' doesn't exist" % cpg_name) @app.route('/api/v1/cpgs/', methods=['DELETE']) def delete_cpg(cpg_name): debugRequest(request) for cpg in cpgs['members']: if cpg['name'] == cpg_name: cpgs['members'].remove(cpg) return make_response("", 200) throw_error(404, 'NON_EXISTENT_CPG', "CPG '%s' doesn't exist" % cpg_name) #### Host #### @app.route('/api/v1/hosts', methods=['POST']) def create_hosts(): debugRequest(request) data = json.loads(request.data) valid_members = ['FCWWNs', 'descriptors', 'domain', 'iSCSINames', 'id', 'name'] for member_key in list(data.keys()): if member_key not in valid_members: throw_error(400, 'INV_INPUT', "Invalid Parameter '%s'" % member_key) if data['name'] is None: throw_error(400, 'INV_INPUT_MISSING_REQUIRED', 'Name not specified.') elif len(data['name']) > 31: throw_error(400, 'INV_INPUT_EXCEEDS_LENGTH', 'Host name is too long.') elif 'domain' in data and len(data['domain']) > 31: throw_error(400, 'INV_INPUT_EXCEEDS_LENGTH', 'Domain name is too long.') elif 'domain' in data and data['domain'] == '': throw_error(400, 'INV_INPUT_EMPTY_STR', 'Input string (for domain, iSCSI etc.) is empty.') charset = {'!', '@', '#', '$', '%', '&', '^'} for char in charset: if char in data['name']: throw_error(400, 'INV_INPUT_ILLEGAL_CHAR', 'Error parsing host-name or domain-name') elif 'domain' in data and char in data['domain']: throw_error(400, 'INV_INPUT_ILLEGAL_CHAR', 'Error parsing host-name or domain-name') if 'FCWWNs' in list(data.keys()): if 'iSCSINames' in list(data.keys()): throw_error(400, 'INV_INPUT_PARAM_CONFLICT', 'FCWWNS and iSCSINames are both specified.') if 'FCWWNs' in list(data.keys()): fc = data['FCWWNs'] for wwn in fc: if len(wwn.replace(':', '')) != 16: throw_error(400, 'INV_INPUT_WRONG_TYPE', 'Length of WWN is not 16.') if 'FCWWNs' in data: for host in hosts['members']: if 'FCWWNs' in host: for fc_path in data['FCWWNs']: if fc_path in host['FCWWNs']: throw_error(409, 'EXISTENT_PATH', 'WWN already claimed by other host.') if 'iSCSINames' in data: for host in hosts: if 'iSCSINames' in host: for iqn in data['iSCSINames']: if iqn in host['iSCSINames']: throw_error(409, 'EXISTENT_PATH', 'iSCSI name already claimed by other host.') for host in hosts['members']: if data['name'] == host['name']: throw_error(409, 'EXISTENT_HOST', "HOST '%s' already exist." % data['name']) hosts['members'].append(data) hosts['total'] = hosts['total'] + 1 resp = make_response("", 201) return resp @app.route('/api/v1/hosts/', methods=['PUT']) def modify_host(host_name): debugRequest(request) data = json.loads(request.data) if host_name == 'None': throw_error(404, 'INV_INPUT', 'Missing host name.') if 'FCWWNs' in list(data.keys()): if 'iSCSINames' in list(data.keys()): throw_error(400, 'INV_INPUT_PARAM_CONFLICT', 'FCWWNS and iSCSINames are both specified.') elif 'pathOperation' not in list(data.keys()): throw_error(400, 'INV_INPUT_ONE_REQUIRED', 'pathOperation is missing and WWN is specified.') if 'iSCSINames' in list(data.keys()): if 'pathOperation' not in list(data.keys()): throw_error(400, 'INV_INPUT_ONE_REQUIRED', 'pathOperation is missing and iSCSI Name is specified.') if 'newName' in list(data.keys()): charset = {'!', '@', '#', '$', '%', '&', '^'} for char in charset: if char in data['newName']: throw_error(400, 'INV_INPUT_ILLEGAL_CHAR', 'Error parsing host-name or domain-name') if len(data['newName']) > 32: throw_error(400, 'INV_INPUT_EXCEEDS_LENGTH', 'New host name is too long.') for host in hosts['members']: if host['name'] == data['newName']: throw_error(409, 'EXISTENT_HOST', 'New host name is already used.') if 'pathOperation' in list(data.keys()): if 'iSCSINames' in list(data.keys()): for host in hosts['members']: if host['name'] == host_name: if data['pathOperation'] == 1: for host in hosts['members']: if 'iSCSINames' in list(host.keys()): for path in data['iSCSINames']: for h_paths in host['iSCSINames']: if path == h_paths: throw_error(409, 'EXISTENT_PATH', 'iSCSI name is already claimed by other host.') for path in data['iSCSINames']: host['iSCSINames'].append(path) resp = make_response(json.dumps(host), 200) return resp elif data['pathOperation'] == 2: for path in data['iSCSINames']: for h_paths in host['iSCSINames']: if path == h_paths: host['iSCSINames'].remove(h_paths) resp = make_response(json.dumps(host), 200) return resp throw_error(404, 'NON_EXISTENT_PATH', 'Removing a non-existent path.') else: throw_error(400, 'INV_INPUT_BAD_ENUM_VALUE', 'pathOperation: Invalid enum value.') throw_error(404, 'NON_EXISTENT_HOST', 'Host to be modified does not exist.') elif 'FCWWNs' in list(data.keys()): for host in hosts['members']: if host['name'] == host_name: if data['pathOperation'] == 1: for host in hosts['members']: if 'FCWWNs' in list(host.keys()): for path in data['FCWWNs']: for h_paths in host['FCWWNs']: if path == h_paths: throw_error(409, 'EXISTENT_PATH', 'WWN is already claimed by other host.') for path in data['FCWWNs']: host['FCWWNs'].append(path) resp = make_response(json.dumps(host), 200) return resp elif data['pathOperation'] == 2: for path in data['FCWWNs']: for h_paths in host['FCWWNs']: if path == h_paths: host['FCWWNs'].remove(h_paths) resp = make_response(json.dumps(host), 200) return resp throw_error(404, 'NON_EXISTENT_PATH', 'Removing a non-existent path.') else: throw_error(400, 'INV_INPUT_BAD_ENUM_VALUE', 'pathOperation: Invalid enum value.') throw_error(404, 'NON_EXISTENT_HOST', 'Host to be modified does not exist.') else: throw_error(400, 'INV_INPUT_ONE_REQUIRED', 'pathOperation specified and no WWNs or iSCSNames specified.') for host in hosts['members']: if host['name'] == host_name: for member_key in list(data.keys()): if member_key == 'newName': host['name'] = data['newName'] else: host[member_key] = data[member_key] resp = make_response(json.dumps(host), 200) return resp throw_error(404, 'NON_EXISTENT_HOST', 'Host to be modified does not exist.') @app.route('/api/v1/hosts/', methods=['DELETE']) def delete_host(host_name): debugRequest(request) for host in hosts['members']: if host['name'] == host_name: hosts['members'].remove(host) return make_response("", 200) throw_error(404, 'NON_EXISTENT_HOST', "The host '%s' doesn't exist" % host_name) @app.route('/api/v1/hosts', methods=['GET']) def get_hosts(): debugRequest(request) query = request.args.get('query') matched_hosts = [] if query is not None: parsed_query = _parse_query(query) for host in hosts['members']: pprint.pprint(host) if 'FCWWNs' in host: pprint.pprint(host['FCWWNs']) for hostwwn in host['FCWWNs']: if hostwwn.replace(':', '') in parsed_query['wwns']: matched_hosts.append(host) break elif 'iSCSINames' in host: pprint.pprint(host['iSCSINames']) for iqn in host['iSCSINames']: if iqn in parsed_query['iqns']: matched_hosts.append(host) break result = {'total': len(matched_hosts), 'members': matched_hosts} resp = make_response(json.dumps(result), 200) else: resp = make_response(json.dumps(hosts), 200) return resp def _parse_query(query): wwns = re.findall("wwn==([0-9A-Z]*)", query) iqns = re.findall("name==([\w.:-]*)", query) parsed_query = {"wwns": wwns, "iqns": iqns} return parsed_query @app.route('/api/v1/hosts/', methods=['GET']) def get_host(host_name): debugRequest(request) charset = {'!', '@', '#', '$', '%', '&', '^'} for char in charset: if char in host_name: throw_error(400, 'INV_INPUT_ILLEGAL_CHAR', 'Host name contains invalid character.') if host_name == 'InvalidURI': throw_error(400, 'INV_INPUT', 'Invalid URI Syntax.') for host in hosts['members']: if host['name'] == host_name: if 'iSCSINames' in list(host.keys()): iscsi_paths = [] for path in host['iSCSINames']: iscsi_paths.append({'name': path}) host['iSCSIPaths'] = iscsi_paths elif 'FCWWNs' in list(host.keys()): fc_paths = [] for path in host['FCWWNs']: fc_paths.append({'wwn': path.replace(':', '')}) host['FCPaths'] = fc_paths resp = make_response(json.dumps(host), 200) return resp throw_error(404, 'NON_EXISTENT_HOST', "Host '%s' doesn't exist" % host_name) #### Port #### @app.route('/api/v1/ports', methods=['GET']) def get_ports(): debugRequest(request) resp = make_response(json.dumps(ports), 200) return resp #### VLUN #### @app.route('/api/v1/vluns', methods=['POST']) def create_vluns(): debugRequest(request) data = json.loads(request.data) valid_keys = {'volumeName': None, 'lun': 0, 'hostname': None, 'portPos': None, 'noVcn': False, 'overrideLowerPriority': False} valid_port_keys = {'node': 1, 'slot': 1, 'cardPort': 0} # do some fake errors here depending on data for key in list(data.keys()): if key not in list(valid_keys.keys()): throw_error(400, 'INV_INPUT', "Invalid Parameter '%s'" % key) elif 'portPos' in list(data.keys()): portP = data['portPos'] for subkey in list(portP.keys()): if subkey not in valid_port_keys: throw_error(400, 'INV_INPUT', "Invalid Parameter '%s'" % subkey) if 'lun' in data: if data['lun'] > 16384: throw_error(400, 'TOO_LARGE', 'LUN is greater than 16384.') else: throw_error(400, 'INV_INPUT', 'Missing LUN.') if 'volumeName' not in data: throw_error(400, 'INV_INPUT_MISSING_REQUIRED', 'Missing volumeName.') else: for volume in volumes['members']: if volume['name'] == data['volumeName']: vluns['members'].append(data) resp = make_response(json.dumps(vluns), 201) resp.headers['location'] = '/api/v1/vluns/' return resp throw_error(404, 'NON_EXISTENT_VOL', 'Specified volume does not exist.') @app.route('/api/v1/vluns/', methods=['DELETE']) def delete_vluns(vlun_str): # is like volumeName,lun,host,node:slot:port debugRequest(request) params = vlun_str.split(',') for vlun in vluns['members']: if vlun['volumeName'] == params[0] and vlun['lun'] == int(params[1]): if len(params) == 4: if str(params[2]) != vlun['hostname']: throw_error(404, 'NON_EXISTENT_HOST', "The host '%s' doesn't exist" % params[2]) print(vlun['portPos']) port = getPort(vlun['portPos']) if not port == params[3]: throw_error(400, 'INV_INPUT_PORT_SPECIFICATION', "Specified port is invalid %s" % params[3]) elif len(params) == 3: if ':' in params[2]: port = getPort(vlun['portPos']) if not port == params[2]: throw_error(400, 'INV_INPUT_PORT_SPECIFICATION', "Specified port is invalid %s" % params[2]) else: if str(params[2]) != vlun['hostname']: throw_error(404, 'NON_EXISTENT_HOST', "The host '%s' doesn't exist" % params[2]) vluns['members'].remove(vlun) return make_response(json.dumps(params), 200) throw_error(404, 'NON_EXISTENT_VLUN', "The volume '%s' doesn't exist" % vluns) def getPort(portPos): port = "%s:%s:%s" % (portPos['node'], portPos['slot'], portPos['cardPort']) print(port) return port @app.route('/api/v1/vluns', methods=['GET']) def get_vluns(): debugRequest(request) resp = make_response(json.dumps(vluns), 200) return resp #### VOLUMES & SNAPSHOTS #### @app.route('/api/v1/volumes/', methods=['POST']) def create_snapshot(volume_name): debugRequest(request) data = json.loads(request.data) valid_keys = {'action': None, 'parameters': None} valid_parm_keys = {'name': None, 'destVolume': None, 'destCPG': None, 'id': None, 'comment': None, 'online': None, 'readOnly': None, 'expirationHours': None, 'retentionHours': None} # do some fake errors here depending on data for key in list(data.keys()): if key not in list(valid_keys.keys()): throw_error(400, 'INV_INPUT', "Invalid Parameter '%s'" % key) elif 'parameters' in list(data.keys()): parm = data['parameters'] for subkey in list(parm.keys()): if subkey not in valid_parm_keys: throw_error(400, 'INV_INPUT', "Invalid Parameter '%s'" % subkey) for volume in volumes['members']: if volume['name'] == volume_name: if data['action'] == "createPhysicalCopy": new_name = data['parameters'].get('destVolume') else: new_name = data['parameters'].get('name') volumes['members'].append({'name': new_name}) resp = make_response(json.dumps(volume), 200) return resp throw_error(404, 'NON_EXISTENT_VOL', "volume doesn't exist") @app.route('/api/v1/volumes', methods=['POST']) def create_volumes(): debugRequest(request) data = json.loads(request.data) valid_keys = {'name': None, 'cpg': None, 'sizeMiB': None, 'id': None, 'comment': None, 'policies': None, 'snapCPG': None, 'ssSpcAllocWarningPct': None, 'ssSpcAllocLimitPct': None, 'tpvv': None, 'usrSpcAllocWarningPct': None, 'usrSpcAllocLimitPct': None, 'isCopy': None, 'copyOfName': None, 'copyRO': None, 'expirationHours': None, 'retentionHours': None} for key in list(data.keys()): if key not in list(valid_keys.keys()): throw_error(400, 'INV_INPUT', "Invalid Parameter '%s'" % key) if 'name' in list(data.keys()): for vol in volumes['members']: if vol['name'] == data['name']: throw_error(409, 'EXISTENT_VOL', 'The volume already exists.') if len(data['name']) > 31: throw_error(400, 'INV_INPUT_EXCEEDS_LENGTH', 'Invalid Input: String length exceeds limit : Name') else: throw_error(400, 'INV_INPUT', 'No volume name provided.') if 'sizeMiB' in list(data.keys()): if data['sizeMiB'] < 256: throw_error(400, 'TOO_SMALL', 'Minimum volume size is 256 MiB') elif data['sizeMiB'] > 16777216: throw_error(400, 'TOO_LARGE', 'Volume size is above architectural limit : 16TiB') if 'id' in list(data.keys()): for vol in volumes['members']: if vol['id'] == data['id']: throw_error(409, 'EXISTENT_ID', 'Specified volume ID already exists.') volumes['members'].append(data) return make_response("", 200) @app.route('/api/v1/volumes/', methods=['DELETE']) def delete_volumes(volume_name): debugRequest(request) for volume in volumes['members']: if volume['name'] == volume_name: volumes['members'].remove(volume) return make_response("", 200) throw_error(404, 'NON_EXISTENT_VOL', "The volume '%s' does not exists." % volume_name) @app.route('/api/v1/volumes', methods=['GET']) def get_volumes(): debugRequest(request) resp = make_response(json.dumps(volumes), 200) return resp @app.route('/api/v1/volumes/', methods=['GET']) def get_volume(volume_name): debugRequest(request) charset = {'!', '@', '#', '$', '%', '&', '^'} for char in charset: if char in volume_name: throw_error(400, 'INV_INPUT_ILLEGAL_CHAR', 'Invalid character for volume name.') for volume in volumes['members']: if volume['name'] == volume_name: resp = make_response(json.dumps(volume), 200) return resp throw_error(404, 'NON_EXISTENT_VOL', "volume doesn't exist") @app.route('/api/v1/volumes/', methods=['PUT']) def modify_volume(volume_name): debugRequest(request) if volume_name not in [volume['name'] for volume in volumes['members']]: throw_error(404, 'NON_EXISTENT_VOL', "The volume does not exist") for volume in volumes['members']: if volume['name'] == volume_name: break data = json.loads(request.data) _grow_volume(volume, data) #do volume renames last if 'newName' in data: volume['name'] = data['newName'] resp = make_response(json.dumps(volume), 200) return resp def _grow_volume(volume, data): # Only grow if there is a need if 'sizeMiB' in data: size = data['sizeMiB'] if size <= 0: throw_error(400, 'INV_INPUT_VV_GROW_SIZE', 'Invalid grow size') cur_size = volume['sizeMiB'] new_size = cur_size + size if new_size > 16777216: throw_error(403, 'VV_NEW_SIZE_EXCEED_CPG_LIMIT', 'New volume size exceeds CPG limit.') volume['sizeMiB'] = new_size @app.route('/api/v1/volumesets', methods=['GET']) def get_volume_sets(): debugRequest(request) resp = make_response(json.dumps(volume_sets), 200) return resp @app.route('/api/v1/volumesets', methods=['POST']) def create_volume_set(): debugRequest(request) data = json.loads(request.data) valid_keys = {'name': None, 'comment': None, 'domain': None, 'setmembers': None} for key in list(data.keys()): if key not in list(valid_keys.keys()): throw_error(400, 'INV_INPUT', "Invalid Parameter '%s'" % key) if 'name' in list(data.keys()): for vset in volume_sets['members']: if vset['name'] == data['name']: throw_error(409, 'EXISTENT_SET', 'The set already exists.') # Seems the 3par is throwing a 409 instead of 400 # {"code":101,"desc":"Set exists"} error # throw_error(400, 'EXISTENT_SET', # 'The set already exists.') if len(data['name']) > 31: throw_error(400, 'INV_INPUT_EXCEEDS_LENGTH', 'Invalid Input: String length exceeds limit : Name') else: throw_error(400, 'INV_INPUT', 'No volume set name provided.') volume_sets['members'].append(data) return make_response("", 200) @app.route('/api/v1/volumesets/', methods=['GET']) def get_volume_set(volume_set_name): debugRequest(request) charset = {'!', '@', '#', '$', '%', '&', '^'} for char in charset: if char in volume_set_name: throw_error(400, 'INV_INPUT_ILLEGAL_CHAR', 'Invalid character for volume set name.') for vset in volume_sets['members']: if vset['name'] == volume_set_name: resp = make_response(json.dumps(vset), 200) return resp throw_error(404, 'NON_EXISTENT_SET', "volume set doesn't exist") @app.route('/api/v1/volumesets/', methods=['PUT']) def modify_volume_set(volume_set_name): debugRequest(request) data = json.loads(request.data) for vset in volume_sets['members']: if vset['name'] == volume_set_name: if 'newName' in data: vset['name'] = data['newName'] if 'comment' in data: vset['comment'] = data['comment'] if 'setmembers' in data and 'action' in data: members = data['setmembers'] if 1 == data['action']: # 1 is memAdd - Adds a member to the set if 'setmembers' not in vset: vset['setmembers'] = [] vset['setmembers'].extend(members) elif 2 == data['action']: # 2 is memRemove- Removes a member from the set for member in members: vset['setmembers'].remove(member) else: # TODO, throw error for now throw_error(400, 'TODO Action', 'Action not implemented in mock server') resp = make_response(json.dumps(vset), 200) return resp throw_error(404, 'NON_EXISTENT_SET', "volume set doesn't exist") @app.route('/api/v1/volumesets/', methods=['DELETE']) def delete_volume_set(volume_set_name): debugRequest(request) for vset in volume_sets['members']: if vset['name'] == volume_set_name: volume_sets['members'].remove(vset) if 'qos' in vset: try: _delete_qos_db(vset['qos']) except Exception as ex: print(vars(ex)) return make_response("", 200) throw_error(404, 'NON_EXISTENT_SET', "The volume set '%s' does not exists." % volume_set_name) def _validate_qos_input(data): valid_keys = {'name': None, 'type': None, 'priority': None, 'bwMinGoalKB': None, 'bwMaxLimitKB': None, 'ioMinGoal': None, 'ioMaxLimit': None, 'enable': None, 'bwMinGoalOP': None, 'bwMaxLimitOP': None, 'ioMinGoalOP': None, 'ioMaxLimitOP': None, 'latencyGoal': None, 'defaultLatency': None} for key in list(data.keys()): if key not in list(valid_keys.keys()): throw_error(400, 'INV_INPUT', "Invalid Parameter '%s'" % key) @app.route('/api/v1/qos', methods=['GET']) def query_all_qos(): debugRequest(request) return make_response(json.dumps(qos_db), 200) @app.route('/api/v1/qos/:', methods=['GET']) def query_qos(target_type, target_name): debugRequest(request) qos = _get_qos_db(target_name) return make_response(json.dumps(qos)) def _get_qos_db(name): for qos in qos_db['members']: if qos['name'] == name: return qos throw_error(400, "Bad Request", "Could not find qos name '%s'." % name) def debug_qos(title): if debugRequest: print(title) pprint.pprint(qos_db) def _add_qos_db(qos): debug_qos("_add_qos_db start") qos['id'] = uuid.uuid1().urn qos_db['members'].append(qos) qos_db['total'] = len(qos_db['members']) debug_qos("_add_qos_db end") return qos['id'] def _modify_qos_db(qos_id, new_qos): debug_qos("_modify_qos_db start") for qos in qos_db['members']: if qos['id'] == qos_id: qos.update(new_qos) debug_qos("_modify_qos_db end") return debug_qos("_modify_qos_db end error") throw_error(500, "Internal error", "could not modify qos id '%s'" % qos_id) def _delete_qos_db(qos_id): debug_qos("_delete_qos_db start") for qos in qos_db['members']: if qos['id'] == qos_id: qos_db['members'].remove(qos) debug_qos("_delete_qos_db end") @app.route('/api/v1/qos', methods=['POST']) def create_qos(): debugRequest(request) qos = json.loads(request.data) if 'name' not in qos: throw_error(404, 'INV_INPUT', "Missing required parameter 'name'") if 'type' not in qos: throw_error(404, 'INV_INPUT', "Missing required parameter 'type'") elif qos['type'] != 1: throw_error(404, 'INV_INPUT', "Flask currently only supports type = 1 (VVSET). " + "Type unsuppored: %s" % qos['type']) _validate_qos_input(qos) for vset in volume_sets['members']: if vset['name'] == qos['name']: if 'qos' in vset: throw_error(400, 'BAD_REQUEST', "QoS rule already exists") else: qos_id = _add_qos_db(qos) vset['qos'] = qos_id return make_response("", 201) throw_error(400, 'BAD_REQUEST', "Target not found: '%s'" % data['name']) @app.route('/api/v1/qos/:', methods=['PUT']) def modify_qos(target_type, name): debugRequest(request) qos = json.loads(request.data) _validate_qos_input(qos) for vset in volume_sets['members']: if vset['name'] == name: if 'qos' not in vset: throw_error(404, 'NOT_FOUND', "QoS rule does not exists") else: _modify_qos_db(vset['qos'], qos) return make_response("", 200) throw_error(400, 'BAD_REQUEST', "Target not found: '%s'" % name) @app.route('/api/v1/qos/:', methods=['DELETE']) def delete_qos(target_type, target_name): debugRequest(request) for vset in volume_sets['members']: if vset['name'] == target_name: if 'qos' not in vset: throw_error(404, 'NOT_FOUND', "QoS rule does not exists") else: _delete_qos_db(vset['qos']) return make_response("", 200) throw_error(400, 'BAD_REQUEST', "Target not found: '%s'" % target_name) @app.route('/api/v1/wsapiconfiguration', methods=['GET']) def get_wsapi_configuration(): debugRequest(request) # TODO: these are copied from the pdf config = {"httpState": "Enabled", "httpPort": 8008, "httpsState": "Enabled", "httpsPort": 8080, "version": "1.3", "sessionsInUse": 0, "systemResourceUsage": 144} return make_response(json.dumps(config)) @app.route('/api/v1/system', methods=['GET']) def get_system(): debugRequest(request) system_info = {"id": 12345, "name": "Flask", "systemVersion": "3.1.3.168", "IPv4Addr": "10.10.10.10", "model": "HP_3PAR 7400", "serialNumber": "1234567", "totalNodes": 2, "masterNode": 0, "onlineNodes": [0, 1], "clusterNodes": [0, 1], "chunkletSizeMiB": 1024, "totalCapacityMiB": 35549184.0, "allocatedCapacityMiB": 4318208.0, "freeCapacityMiB": 31230976.0, "failedCapacityMiB": 0.0, "location": "Flask Test Virtual", "owner": "Flask Owner", "contact": "flask@flask.com", "comment": "flask test env", "timeZone": "America/Los_Angeles"} resp = make_response(json.dumps(system_info), 200) return resp @app.route('/api', methods=['GET']) def get_version(): debugRequest(request) version = {'major': 1, 'minor': 3, 'build': 30103168} resp = make_response(json.dumps(version), 200) return resp @app.route('/api/v1/tasks/', methods=['GET']) def get_task(task_id): debugRequest(request) try: task_id = int(task_id) except ValueError: throw_error(400, 'INV_INPUT_WRONG_TYPE', "Task ID is not an integer") if task_id <= 0: throw_error(400, 'INV_INPUT_BELOW_RANGE', "Task ID must be a positive value") if task_id > 65535: throw_error(400, 'INV_INPUT_EXCEEDS_RANGE', "Task ID is too large") for task in tasks['members']: if task['id'] == task_id: return make_response(json.dumps(task), 200) throw_error(400, 'BAD_REQUEST', "Task not found: '%s'" % task_id) @app.route('/api/v1/tasks', methods=['GET']) def get_tasks(): debugRequest(request) resp = make_response(json.dumps(tasks), 200) return resp if __name__ == "__main__": # fake 2 CPGs global cpgs cpgs = {'members': [{'SAGrowth': {'LDLayout': {'diskPatterns': [{'diskType': 1}]}, 'incrementMiB': 8192}, 'SAUsage': {'rawTotalMiB': 24576, 'rawUsedMiB': 768, 'totalMiB': 8192, 'usedMiB': 256}, 'SDGrowth': {'LDLayout': {'diskPatterns': [{'diskType': 1}]}, 'incrementMiB': 16384, 'limitMiB': 256000, 'warningMiB': 204800}, 'SDUsage': {'rawTotalMiB': 32768, 'rawUsedMiB': 2048, 'totalMiB': 16384, 'usedMiB': 1024}, 'UsrUsage': {'rawTotalMiB': 239616, 'rawUsedMiB': 229376, 'totalMiB': 119808, 'usedMiB': 114688}, 'additionalStates': [], 'degradedStates': [], 'domain': 'UNIT_TEST', 'failedStates': [], 'id': 0, 'name': 'UnitTestCPG', 'numFPVVs': 12, 'numTPVVs': 0, 'state': 1, 'uuid': 'f9b018cc-7cb6-4358-a0bf-93243f853d96'}, {'SAGrowth': {'LDLayout': {'diskPatterns': [{'diskType': 1}]}, 'incrementMiB': 8192}, 'SAUsage': {'rawTotalMiB': 24576, 'rawUsedMiB': 768, 'totalMiB': 8192, 'usedMiB': 256}, 'SDGrowth': {'LDLayout': {'diskPatterns': [{'diskType': 1}]}, 'incrementMiB': 16384, 'limitMiB': 256000, 'warningMiB': 204800}, 'SDUsage': {'rawTotalMiB': 32768, 'rawUsedMiB': 2048, 'totalMiB': 16384, 'usedMiB': 1024}, 'UsrUsage': {'rawTotalMiB': 239616, 'rawUsedMiB': 229376, 'totalMiB': 119808, 'usedMiB': 114688}, 'additionalStates': [], 'degradedStates': [], 'domain': 'UNIT_TEST', 'failedStates': [], 'id': 0, 'name': 'UnitTestCPG2', 'numFPVVs': 12, 'numTPVVs': 0, 'state': 1, 'uuid': 'f9b018cc-7cb6-4358-a0bf-93243f853d97'}], 'total': 2} # fake volumes global volumes volumes = {'members': [{'additionalStates': [], 'adminSpace': {'freeMiB': 0, 'rawReservedMiB': 384, 'reservedMiB': 128, 'usedMiB': 128}, 'baseId': 1, 'copyType': 1, 'creationTime8601': '2012-09-24T15:12:13-07:00', 'creationTimeSec': 1348524733, 'degradedStates': [], 'domain': 'UNIT_TEST', 'failedStates': [], 'id': 91, 'name': 'UnitTestVolume', 'policies': {'caching': True, 'oneHost': False, 'staleSS': True, 'system': False, 'zeroDetect': False}, 'provisioningType': 1, 'readOnly': False, 'sizeMiB': 102400, 'snapCPG': 'UnitTestCPG', 'snapshotSpace': {'freeMiB': 0, 'rawReservedMiB': 1024, 'reservedMiB': 512, 'usedMiB': 512}, 'ssSpcAllocLimitPct': 0, 'ssSpcAllocWarningPct': 95, 'state': 1, 'userCPG': 'UnitTestCPG', 'userSpace': {'freeMiB': 0, 'rawReservedMiB': 204800, 'reservedMiB': 102400, 'usedMiB': 102400}, 'usrSpcAllocLimitPct': 0, 'usrSpcAllocWarningPct': 0, 'uuid': '8bc9394e-f87a-4c1a-8777-11cba75af94c', 'wwn': '50002AC00001383D'}, {'additionalStates': [], 'adminSpace': {'freeMiB': 0, 'rawReservedMiB': 384, 'reservedMiB': 128, 'usedMiB': 128}, 'baseId': 41, 'comment': 'test volume', 'copyType': 1, 'creationTime8601': '2012-09-27T14:11:56-07:00', 'creationTimeSec': 1348780316, 'degradedStates': [], 'domain': 'UNIT_TEST', 'failedStates': [], 'id': 92, 'name': 'UnitTestVolume2', 'policies': {'caching': True, 'oneHost': False, 'staleSS': True, 'system': False, 'zeroDetect': False}, 'provisioningType': 1, 'readOnly': False, 'sizeMiB': 10240, 'snapCPG': 'UnitTestCPG', 'snapshotSpace': {'freeMiB': 0, 'rawReservedMiB': 1024, 'reservedMiB': 512, 'usedMiB': 512}, 'ssSpcAllocLimitPct': 0, 'ssSpcAllocWarningPct': 0, 'state': 1, 'userCPG': 'UnitTestCPG', 'userSpace': {'freeMiB': 0, 'rawReservedMiB': 20480, 'reservedMiB': 10240, 'usedMiB': 10240}, 'usrSpcAllocLimitPct': 0, 'usrSpcAllocWarningPct': 0, 'uuid': '6d5542b2-f06a-4788-879e-853ad0a3be42', 'wwn': '50002AC00029383D'}], 'total': 26} # fake ports global ports ports = {'members': [{'linkState': 4, 'mode': 2, 'nodeWwn': None, 'portPos': {'cardPort': 1, 'node': 1, 'slot': 7}, 'portWwn': '2C27D75375D5', 'protocol': 2, 'type': 7}, {'linkState': 4, 'mode': 2, 'nodeWwn': None, 'portPos': {'cardPort': 2, 'node': 2, 'slot': 8}, 'portWwn': '2C27D75375D6', 'protocol': 2, 'type': 7}, {'linkState': 4, 'mode': 2, 'nodeWwn': None, 'portPos': {'cardPort': 3, 'node': 3, 'slot': 5}, 'portWwn': '2C27D75375D7', 'protocol': 1, 'type': 7}, {'linkState': 4, 'mode': 2, 'nodeWwn': None, 'portPos': {'cardPort': 4, 'node': 4, 'slot': 6}, 'portWwn': '2C27D75375D8', 'protocol': 1, 'type': 7}, {'portPos': {'node': 0, 'slot': 3, 'cardPort': 1}, 'protocol': 4, 'linkState': 10, 'label': 'RCIP0', 'device': [], 'mode': 4, 'HWAddr': 'B4B52FA76931', 'type': 7}, {'portPos': {'node': 1, 'slot': 3, 'cardPort': 1}, 'protocol': 4, 'linkState': 10, 'label': 'RCIP1', 'device': [], 'mode': 4, 'HWAddr': 'B4B52FA768B1', 'type': 7}], 'total': 6} # fake hosts global hosts hosts = {'members': [{'FCWWNs': [], 'descriptors': None, 'domain': 'UNIT_TEST', 'iSCSINames': [{'driverVersion': '1.0', 'firmwareVersion': '1.0', 'hostSpeed': 100, 'ipAddr': '10.10.221.59', 'model': 'TestModel', 'name': 'iqnTestName', 'portPos': {'cardPort': 1, 'node': 1, 'slot': 8}, 'vendor': 'HP'}], 'id': 11, 'name': 'UnitTestHost'}, {'FCWWNs': [], 'descriptors': None, 'domain': 'UNIT_TEST', 'iSCSINames': [{'driverVersion': '1.0', 'firmwareVersion': '1.0', 'hostSpeed': 100, 'ipAddr': '10.10.221.58', 'model': 'TestMode2', 'name': 'iqnTestName2', 'portPos': {'cardPort': 1, 'node': 1, 'slot': 8}, 'vendor': 'HP'}], 'id': 12, 'name': 'UnitTestHost2'}], 'total': 2} # fake create vluns global vluns vluns = {'members': [{'active': True, 'failedPathInterval': 0, 'failedPathPol': 1, 'hostname': 'UnitTestHost', 'lun': 31, 'multipathing': 1, 'portPos': {'cardPort': 1, 'node': 1, 'slot': 2}, 'remoteName': '100010604B0174F1', 'type': 4, 'volumeName': 'UnitTestVolume', 'volumeWWN': '50002AC00001383D'}, {'active': False, 'failedPathInterval': 0, 'failedPathPol': 1, 'hostname': 'UnitTestHost2', 'lun': 32, 'multipathing': 1, 'portPos': {'cardPort': 2, 'node': 2, 'slot': 3}, 'type': 3, 'volumeName': 'UnitTestVolume2', 'volumeWWN': '50002AC00029383D'}], 'total': 2} global volume_sets volume_sets = {'members': [], 'total': 0} global qos_db qos_db = {'members': [], 'total': 0} global tasks tasks = {"total":2,"members":[ {"id":8933,"type":15,"name":"check_slow_disk","status":1,"startTime":"2014-02-06 13:07:03 PST","finishTime":"2014-02-06 14:03:04 PST","priority":-1,"user":"3parsvc"}, {"id":8934,"type":15,"name":"remove_expired_vvs","status":1,"startTime":"2014-02-06 13:27:03 PST","finishTime":"2014-02-06 13:27:03 PST","priority":-1,"user":"3parsvc"} ]} app.run(port=args.port, debug=debugRequests) hp3parclient-3.0.0/test/test_HP3ParClient_VLUN.py0000664000175000017500000002204712276214354022007 0ustar stackstack00000000000000# vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2009-2012 10gen, Inc. # # 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. """Test class of 3Par Client handling VLUN""" import sys, os sys.path.insert(0,os.path.realpath(os.path.abspath('../'))) from hp3parclient import client, exceptions import unittest import test_HP3ParClient_base CPG_NAME1 = 'CPG1_VLUN_UNIT_TEST' CPG_NAME2 = 'CPG2_VLUN_UNIT_TEST' VOLUME_NAME1 = 'VOLUME1_VLUN_UNIT_TEST' VOLUME_NAME2 = 'VOLUME2_VLUN_UNIT_TEST' DOMAIN = 'UNIT_TEST_DOMAIN' HOST_NAME1 = 'HOST1_VLUN_UNIT_TEST' HOST_NAME2 = 'HOST2_VLUN_UNIT_TEST' LUN_1 = 1 LUN_2 = 2 PORT_1 = {'node':1, 'cardPort':1, 'slot':1} class HP3ParClientVLUNTestCase(test_HP3ParClient_base.HP3ParClientBaseTestCase): def setUp(self): super(HP3ParClientVLUNTestCase, self).setUp() try : optional = {'domain': DOMAIN} self.cl.createCPG(CPG_NAME1, optional) except : pass try : optional = {'domain': DOMAIN} self.cl.createCPG(CPG_NAME2, optional) except : pass try : self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024) except : pass try : self.cl.createVolume(VOLUME_NAME2, CPG_NAME2, 1024) except : pass try : optional = {'domain': DOMAIN} self.cl.createHost(HOST_NAME1, None, None, optional) except : pass try : optional = {'domain': DOMAIN} self.cl.createHost(HOST_NAME2, None, None, optional) except : pass def tearDown(self): try: self.cl.deleteVLUN(VOLUME_NAME1, LUN_1, HOST_NAME1, PORT_1) except Exception: pass try: self.cl.deleteVLUN(VOLUME_NAME1, LUN_1, HOST_NAME2) except Exception: pass try: self.cl.deleteVLUN(VOLUME_NAME2, LUN_2, HOST_NAME2) except: pass try: self.cl.deleteVLUN(VOLUME_NAME2, LUN_2, HOST_NAME2, PORT_1) except: pass try: self.cl.deleteVolume(VOLUME_NAME1) except: pass try: self.cl.deleteVolume(VOLUME_NAME2) except: pass try: self.cl.deleteCPG(CPG_NAME1) except: pass try: self.cl.deleteCPG(CPG_NAME2) except: pass try: self.cl.deleteHost(HOST_NAME1) except: pass try: self.cl.deleteHost(HOST_NAME2) except: pass # very last, tear down base class super(HP3ParClientVLUNTestCase, self).tearDown() def test_1_create_VLUN(self): self.printHeader('create_VLUN') #add one noVcn = False overrideObjectivePriority = True self.cl.createVLUN(VOLUME_NAME1, LUN_1, HOST_NAME1, PORT_1, noVcn, overrideObjectivePriority) #check vlun1 = self.cl.getVLUN(VOLUME_NAME1) self.assertIsNotNone(vlun1) volName = vlun1['volumeName'] self.assertEqual(VOLUME_NAME1, volName) #add another self.cl.createVLUN(VOLUME_NAME2, LUN_2, HOST_NAME2) #check vlun2 = self.cl.getVLUN(VOLUME_NAME2) self.assertIsNotNone(vlun2) volName = vlun2['volumeName'] self.assertEqual(VOLUME_NAME2, volName) self.printFooter('create_VLUN') def test_1_create_VLUN_tooLarge(self): self.printHeader('create_VLUN_tooLarge') lun = 100000 self.assertRaises(exceptions.HTTPBadRequest, self.cl.createVLUN, VOLUME_NAME1, lun, HOST_NAME1, PORT_1) self.printFooter('create_VLUN_tooLarge') def test_1_create_VLUN_volulmeNonExist(self): self.printHeader('create_VLUN_volumeNonExist') self.assertRaises(exceptions.HTTPNotFound, self.cl.createVLUN, 'Some_Volume', LUN_1, HOST_NAME1, PORT_1) self.printFooter('create_VLUN_volumeNonExist') def test_1_create_VLUN_badParams(self): self.printHeader('create_VLUN_badParams') portPos = {'badNode':1, 'cardPort':1, 'slot':2} self.assertRaises(exceptions.HTTPBadRequest, self.cl.createVLUN, VOLUME_NAME1, LUN_1, HOST_NAME1, portPos) self.printFooter('create_VLUN_badParams') def test_2_get_VLUN_bad(self): self.printHeader('get_VLUN_bad') self.assertRaises(exceptions.HTTPNotFound, self.cl.getVLUN, 'badName') self.printFooter('get_VLUN_bad') def test_2_get_VLUNs(self): self.printHeader('get_VLUNs') # add 2 noVcn = False overrideLowerPriority = True self.cl.createVLUN(VOLUME_NAME1, LUN_1, HOST_NAME1, PORT_1, noVcn, overrideLowerPriority) self.cl.createVLUN(VOLUME_NAME2, LUN_2, HOST_NAME2) # get all vluns = self.cl.getVLUNs() v1 = self.cl.getVLUN(VOLUME_NAME1) v2 = self.cl.getVLUN(VOLUME_NAME2) self.assertTrue(self.findInDict(vluns['members'], 'lun', v1['lun'])) self.assertTrue(self.findInDict(vluns['members'], 'volumeName', v1['volumeName'])) self.assertTrue(self.findInDict(vluns['members'], 'lun', v2['lun'])) self.assertTrue(self.findInDict(vluns['members'], 'volumeName', v2['volumeName'])) self.printFooter('get_VLUNs') def test_3_delete_VLUN_volumeNonExist(self): self.printHeader('delete_VLUN_volumeNonExist') self.cl.createVLUN(VOLUME_NAME1, LUN_1, HOST_NAME1, PORT_1) self.cl.getVLUN(VOLUME_NAME1) self.assertRaises(exceptions.HTTPNotFound, self.cl.deleteVLUN, 'UnitTestVolume', LUN_1, HOST_NAME1, PORT_1) self.printFooter('delete_VLUN_volumeNonExist') def test_3_delete_VLUN_hostNonExist(self): self.printHeader('delete_VLUN_hostNonExist') self.cl.createVLUN(VOLUME_NAME1, LUN_1, HOST_NAME1, PORT_1) self.cl.getVLUN(VOLUME_NAME1) self.assertRaises(exceptions.HTTPNotFound, self.cl.deleteVLUN, VOLUME_NAME1, LUN_1, 'BoggusHost', PORT_1) self.printFooter('delete_VLUN_hostNonExist') def test_3_delete_VLUN_portNonExist(self): self.printHeader('delete_VLUN_portNonExist') self.cl.createVLUN(VOLUME_NAME2, LUN_2, HOST_NAME2, PORT_1) self.cl.getVLUN(VOLUME_NAME2) port = {'node': 8, 'cardPort': 8, 'slot': 8} try: self.cl.deleteVLUN(VOLUME_NAME2, LUN_2, HOST_NAME2, port) except exceptions.HTTPBadRequest: print('Expected exception') self.printFooter('delete_VLUN_portNonExist') return except Exception as ex: print(ex) self.fail('Failed with unexpected exception') self.fail('No exception occurred.') def test_3_delete_VLUNs(self): self.printHeader('delete_VLUNs') self.cl.createVLUN(VOLUME_NAME1, LUN_1, HOST_NAME1, PORT_1) self.cl.getVLUN(VOLUME_NAME1) self.cl.deleteVLUN(VOLUME_NAME1 ,LUN_1, HOST_NAME1, PORT_1) try: self.cl.deleteVLUN(VOLUME_NAME1, LUN_1, HOST_NAME1, PORT_1) except exceptions.HTTPNotFound: print('Expected exception') self.printFooter('delete_VLUNs') return except Exception as ex: print(ex) self.fail('Failed with unexpected exception') self.fail('No exception occurred.') def test_4_get_host_VLUNs(self): self.printHeader('get_host_vluns') self.cl.createVLUN(VOLUME_NAME2, LUN_2, HOST_NAME2) self.cl.createVLUN(VOLUME_NAME1, LUN_1, HOST_NAME2) host_vluns = self.cl.getHostVLUNs(HOST_NAME2) self.assertIn(VOLUME_NAME1, [vlun['volumeName'] for vlun in host_vluns]) self.assertIn(VOLUME_NAME2, [vlun['volumeName'] for vlun in host_vluns]) self.assertIn(LUN_1, [vlun['lun'] for vlun in host_vluns]) self.assertIn(LUN_2, [vlun['lun'] for vlun in host_vluns]) self.printFooter('get_host_vluns') def test_4_get_host_VLUNs_unknown_host(self): self.printHeader('get_host_vluns_unknown_host') try: self.cl.getHostVLUNs('bogusHost') except exceptions.HTTPNotFound: self.printFooter('get_host_vluns_unknown_host') return except Exception as ex: print(ex) self.fail('Failed with unexpected exception') self.fail('Expected an exception') #testing #suite = unittest.TestLoader().loadTestsFromTestCase(HP3ParClientVLUNTestCase) #unittest.TextTestRunner(verbosity=2).run(suite) hp3parclient-3.0.0/setup.py0000664000175000017500000000213712276214354016067 0ustar stackstack00000000000000import hp3parclient try: from setuptools import setup, find_packages except ImportError: from distutils.core import setup, find_packages setup( name='hp3parclient', version=hp3parclient.version, description="HP 3PAR HTTP REST Client", author="Walter A. Boring IV", author_email="walter.boring@hp.com", maintainer="Walter A. Boring IV", keywords=["hp", "3par", "rest"], requires=['httplib2(>=0.6.0)', 'paramiko', 'eventlet'], install_requires=['httplib2 >= 0.6.0'], tests_require=["nose", "werkzeug", "nose-testconfig"], license="Apache License, Version 2.0", packages=find_packages(), provides=['hp3parclient'], url="http://packages.python.org/hp3parclient", classifiers=[ 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Environment :: Web Environment', 'Programming Language :: Python', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.0', 'Topic :: Internet :: WWW/HTTP', ] ) hp3parclient-3.0.0/setup.cfg0000664000175000017500000000007312276222042016164 0ustar stackstack00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 hp3parclient-3.0.0/hp3parclient/0000775000175000017500000000000012276222042016737 5ustar stackstack00000000000000hp3parclient-3.0.0/hp3parclient/client.py0000664000175000017500000021425612276217315020610 0ustar stackstack00000000000000# (c) Copyright 2012-2014 Hewlett Packard Development Company, L.P. # 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. """ HP3PAR REST Client. .. module: client .. moduleauthor: Walter A. Boring IV .. moduleauthor: Kurt Martin :Author: Walter A. Boring IV :Description: This is the 3PAR Client that talks to 3PAR's REST WSAPI Service. It provides the ability to provision 3PAR volumes, VLUNs, CPGs. This version also supports running actions on the 3PAR that use SSH. This client requires and works with 3PAR InForm 3.1.3 firmware """ import re import time try: # For Python 3.0 and later from urlib.parse import quote except ImportError: # Fall back to Python 2's urllib2 from urllib2 import quote from hp3parclient import exceptions, http, ssh class HP3ParClient(object): """ The 3PAR REST API Client. :param api_url: The url to the WSAPI service on 3PAR ie. http://<3par server>:8080/api/v1 :type api_url: str """ PORT_MODE_TARGET = 2 PORT_MODE_INITIATOR = 3 PORT_MODE_PEER = 4 PORT_TYPE_HOST = 1 PORT_TYPE_DISK = 2 PORT_TYPE_FREE = 3 PORT_TYPE_RCIP = 6 PORT_TYPE_ISCSI = 7 PORT_PROTO_FC = 1 PORT_PROTO_ISCSI = 2 PORT_PROTO_IP = 4 PORT_STATE_READY = 4 PORT_STATE_SYNC = 5 PORT_STATE_OFFLINE = 10 HOST_EDIT_ADD = 1 HOST_EDIT_REMOVE = 2 SET_MEM_ADD = 1 SET_MEM_REMOVE = 2 STOP_PHYSICAL_COPY = 1 RESYNC_PHYSICAL_COPY = 2 GROW_VOLUME = 3 TARGET_TYPE_VVSET = 1 TARGET_TYPE_SYS = 2 PRIORITY_LOW = 1 PRIORITY_NORMAL = 2 PRIORITY_HIGH = 3 TASK_DONE = 1 TASK_ACTIVE = 2 TASK_CANCELLED = 3 TASK_FAILED = 4 # build contains major minor mj=3 min=01 main=03 build=168 HP3PAR_WS_MIN_BUILD_VERSION = 30103168 def __init__(self, api_url): self.api_url = api_url self.http = http.HTTPJSONRESTClient(self.api_url) api_version = None self.ssh = None try: api_version = self.getWsApiVersion() except Exception: msg = ('Either, the 3PAR WS is not running or the' ' version of the WS is invalid.') raise exceptions.UnsupportedVersion(msg) # Note the build contains major, minor, maintenance and build # e.g. 30102422 is 3 01 02 422 # therefore all we need to compare is the build if (api_version is None or api_version['build'] < self.HP3PAR_WS_MIN_BUILD_VERSION): raise exceptions.UnsupportedVersion('Invalid 3PAR WS API, requires' ' version, 3.1.3') def setSSHOptions(self, ip, login, password, port=22, conn_timeout=None, privatekey=None): """Set SSH Options for ssh calls. This is used to set the SSH credentials for calls that use SSH instead of REST HTTP. """ self.ssh = ssh.HP3PARSSHClient(ip, login, password, port, conn_timeout, privatekey) def _run(self, cmd): if self.ssh is None: raise exceptions.SSHException('SSH is not initialized. Initialize it by calling "setSSHOptions".') else: return self.ssh.run(cmd) def getWsApiVersion(self): """ Get the 3PAR WS API version. :returns: Version dict """ try: # remove everything down to host:port host_url = self.api_url.split('/api') self.http.set_url(host_url[0]) # get the api version response, body = self.http.get('/api') return body finally: # reset the url self.http.set_url(self.api_url) def debug_rest(self, flag): """ This is useful for debugging requests to 3PAR. :param flag: set to True to enable debugging :type flag: bool """ self.http.set_debug_flag(flag) if self.ssh: self.ssh.set_debug_flag(flag) def login(self, username, password, optional=None): """ This authenticates against the 3PAR wsapi server and creates a session. :param username: The username :type username: str :param password: The Password :type password: str :returns: None """ self.http.authenticate(username, password, optional) def logout(self): """ This destroys the session and logs out from the 3PAR server. :returns: None """ self.http.unauthenticate() def getStorageSystemInfo(self): """ Get the Storage System Information :returns: Dictionary of Storage System Info """ response, body = self.http.get('/system') return body def getWSAPIConfigurationInfo(self): """ Get the WSAPI Configuration Information :returns: Dictionary of WSAPI configurations """ response, body = self.http.get('/wsapiconfiguration') return body ##Volume methods def getVolumes(self): """ Get the list of Volumes :returns: list of Volumes """ response, body = self.http.get('/volumes') return body def getVolume(self, name): """ Get information about a volume :param name: The name of the volume to find :type name: str :returns: volume :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - volume doesn't exist """ response, body = self.http.get('/volumes/%s' % name) return body def createVolume(self, name, cpgName, sizeMiB, optional=None): """ Create a new volume :param name: the name of the volume :type name: str :param cpgName: the name of the destination CPG :type cpgName: str :param sizeMiB: size in MiB for the volume :type sizeMiB: int :param optional: dict of other optional items :type optional: dict .. code-block:: python optional = { 'id': 12, 'comment': 'some comment', 'snapCPG' :'CPG name', 'ssSpcAllocWarningPct' : 12, 'ssSpcAllocLimitPct': 22, 'tpvv' : True, 'usrSpcAllocWarningPct': 22, 'usrSpcAllocLimitPct': 22, 'expirationHours': 256, 'retentionHours': 256 } :returns: List of Volumes :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT - Invalid Parameter :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - TOO_LARGE - Volume size above limit :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - NO_SPACE - Not Enough space is available :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - PERM_DENIED - Permission denied :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - EXISTENT_SV - Volume Exists already """ info = {'name': name, 'cpg': cpgName, 'sizeMiB': sizeMiB} if optional: info = self._mergeDict(info, optional) response, body = self.http.post('/volumes', body=info) return body def deleteVolume(self, name): """ Delete a volume :param name: the name of the volume :type name: str :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - The volume does not exist :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - PERM_DENIED - Permission denied :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - RETAINED - Volume retention time has not expired :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - HAS_RO_CHILD - Volume has read-only child """ response, body = self.http.delete('/volumes/%s' % name) return body def modifyVolume(self, name, volumeMods): """ Modify a volume :param name: the name of the volume :type name: str :param volumeMods: dictionary of volume attributes to change :type volumeMods: dict :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_WARN_GT_LIMIT - Allocation warning level is higher than the limit. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_USR_ALRT_NON_TPVV - User space allocation alerts are valid only with a TPVV. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_RETAIN_GT_EXPIRE - Retention time is greater than expiration time. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_VV_POLICY - Invalid policy specification (for example, caching or system is set to true). :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_EXCEEDS_LENGTH - Invalid input: string length exceeds limit. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_TIME - Invalid time specified. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_MODIFY_USR_CPG_TPVV - usr_cpg cannot be modified on a TPVV. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - UNLICENSED_FEATURE - Retention time cannot be modified on a system without the Virtual Lock license. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - CPG_NOT_IN_SAME_DOMAIN - Snap CPG is not in the same domain as the user CPG. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_PEER_VOLUME - Cannot modify a peer volume. :raises: :class:`~hp3parclient.exceptions.HTTPInternalServerError` - INT_SERV_ERR - Metadata of the VV is corrupted. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_SYS_VOLUME - Cannot modify retention time on a system volume. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_INTERNAL_VOLUME - Cannot modify an internal volume :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_VOLUME_NOT_DEFINED_ALL_NODES - Cannot modify a volume until the volume is defined on all volumes. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INVALID_OPERATION_VV_ONLINE_COPY_IN_PROGRESS - Cannot modify a volume when an online copy for that volume is in progress. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INVALID_OPERATION_VV_VOLUME_CONV_IN_PROGRESS - Cannot modify a volume in the middle of a conversion operation. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INVALID_OPERATION_VV_SNAPSPACE_NOT_MOVED_TO_CPG - Snapshot space of a volume needs to be moved to a CPG before the user space. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_VOLUME_ACCOUNTING_IN_PROGRESS - The volume cannot be renamed until snapshot accounting has finished. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_ZERO_DETECT_TPVV - The zero_detect policy can be used only on TPVVs. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_CPG_ON_SNAPSHOT - CPG cannot be assigned to a snapshot. """ response = self.http.put('/volumes/%s' % name, body=volumeMods) return response def growVolume(self, name, amount): """ Grow an existing volume by 'amount' Mebibytes. :param name: the name of the volume :type name: str :param amount: the additional size in MiB to add, rounded up to the next chunklet size (e.g. 256 or 1000 MiB) :type amount: int :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - VV_NOT_IN_SAME_DOMAIN - The volume is not in the same domain. :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - The volume does not exist. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_UNSUPPORTED_VV_TYPE - Invalid operation: Cannot grow this type of volume. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_TUNE_IN_PROGRESS - Invalid operation: Volume tuning is in progress. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_EXCEEDS_LENGTH - Invalid input: String length exceeds limit. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_VV_GROW_SIZE - Invalid grow size. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - VV_NEW_SIZE_EXCEEDS_CPG_LIMIT - New volume size exceeds CPG limit. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_INTERNAL_VOLUME - This operation is not allowed on an internal volume. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_VOLUME_CONV_IN_PROGRESS - Invalid operation: VV conversion is in progress. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_VOLUME_COPY_IN_PROGRESS - Invalid operation: online copy is in progress. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_CLEANUP_IN_PROGRESS - Internal volume cleanup is in progress. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - VV_IS_BEING_REMOVED - The volume is being removed. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - VV_IN_INCONSISTENT_STATE - The volume has an internal consistency error. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - VV_SIZE_CANNOT_REDUCE - New volume size is smaller than the current size. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - VV_NEW_SIZE_EXCEEDS_LIMITS - New volume size exceeds the limit. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_SA_SD_SPACE_REMOVED - Invalid operation: Volume SA/SD space is being removed. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_IS_BUSY - Invalid operation: Volume is currently busy. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - VV_NOT_STARTED - Volume is not started. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_IS_PCOPY - Invalid operation: Volume is a physical copy. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_NOT_IN_NORMAL_STATE - Volume state is not normal. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_PROMOTE_IN_PROGRESS - Invalid operation: Volume promotion is in progress. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_PARENT_OF_PCOPY - Invalid operation: Volume is the parent of physical copy. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - NO_SPACE - Insufficent space for requested operation. """ info = {'action': self.GROW_VOLUME, 'sizeMiB': amount} response, body = self.http.put('/volumes/%s' % name, body=info) return body def copyVolume(self, src_name, dest_name, dest_cpg, optional=None): """ Copy/Clone a volume. :param src_name: the source volume name :type src_name: str :param dest_name: the destination volume name :type dest_name: str :param dest_cpg: the destination CPG :type dest_cpg: str :param optional: Dictionary of optional params :type optional: dict .. code-block:: python optional = { 'online': False, # should physical copy be performed online? 'tpvv': False, # use thin provisioned space for destination? (online copy only) 'snapCPG' : "OpenStack_SnapCPG, # snapshot CPG for the destination (online copy only) 'saveSnapshot': False, # save the snapshot of the source volume after the copy id complete? 'priority' : 1 # taskPriorityEnum (does not apply to online copy) } :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Invalid VV name or CPG name. :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_CPG - The CPG does not exists. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - CPG_NOT_IN SAME_DOMAIN - The CPG is not in the current domain. :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - The volume does not exist :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - VV_NOT_IN_SAME_DOMAIN - The volume is not in the same domain. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_BAD_ENUM_VALUE - The priority value in not in the valid range(1-3). :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - EXISTENT_VOLUME - The volume already exists. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_SYS_VOLUME - The operation is not allowed on a system volume. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_NON_BASE_VOLUME - The destination volume is not a base volume. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_IN_REMOTE_COPY - The destination volume is involved in a remote copy. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_EXPORTED - The volume is exported. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_COPY_TO_SELF - The destination volume is the same as the parent. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_READONLY_SNAPSHOT - The parent volume is a read-only snapshot. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_COPY_TO_BASE - The destination volume is the base volume of a parent volume. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_VOLUME_CONV_IN_PROGRESS - The volume is in a conversion operation. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_NO_SNAPSHOT_ALLOWED - The parent volume must allow snapshots. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_ONLINE_COPY_IN_PROGRESS - The volume is the target of an online copy. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_CLEANUP_IN_PROGRESS - Cleanup of internal volume for the volume is in progress. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_CIRCULAR_COPY - The parent volume is a copy of the destination volume. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_PEER_VOLUME - The operation is not allowed on a peer volume. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_INTERNAL_VOLUME - The operation is not allowed on an internal volume. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - VV_IS_BEING_REMOVED - The volume is being removed. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_NOT_IN_NORMAL_STATE - The volume is not in the normal state. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - VV_IN_INCONSISTENT_STATE - The volume has an internal consistency error. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_PCOPY_IN_PROGRESS - The destination volume has a physical copy in progress. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_FAILED_ONLINE_COPY - Online copying of the destination volume has failed. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_COPY_PARENT_TOO_BIG - The size of the parent volume is larger than the size of the destination volume. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_NO_PARENT - The volume has no physical parent. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - IN_USE - The resynchronization snapshot is in a stale state. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - VV_IN_STALE_STATE - The volume is in a stale state. :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VVCOPY - Physical copy not found. """ # Virtual volume sets are not supported with the -online option parameters = {'destVolume': dest_name, 'destCPG': dest_cpg} if optional: parameters = self._mergeDict(parameters, optional) info = {'action': 'createPhysicalCopy', 'parameters': parameters} response, body = self.http.post('/volumes/%s' % src_name, body=info) return body def isOnlinePhysicalCopy(self, name): """ Is the volume being created by process of online copy? :param name: the name of the volume :type name: str """ task = self._findTask(name, active=True) if task is None: return False else: return True def stopOnlinePhysicalCopy(self, name): """ Stopping a online physical copy operation. :param name: the name of the volume :type name: str """ # first we have to find the active copy task = self._findTask(name) task_id = None if task is None: # couldn't find the task msg = "Couldn't find the copy task for '%s'" % name raise exceptions.HTTPNotFound(error={'desc': msg}) else: task_id = task[0] # now stop the copy if task_id is not None: cmd = ['canceltask', '-f', task_id] self._run(cmd) else: msg = "Couldn't find the copy task for '%s'" % name raise exceptions.HTTPNotFound(error={'desc': msg}) # we have to make sure the task is cancelled # before moving on. This can sometimes take a while. ready = False while not ready: time.sleep(1) task = self._findTask(name, True) if task is None: ready = True # now cleanup the dead snapshots vol = self.getVolume(name) if vol: snap1 = self.getVolume(vol['copyOf']) snap2 = self.getVolume(snap1['copyOf']) self.deleteVolume(name) self.deleteVolume(snap1['name']) self.deleteVolume(snap2['name']) def getAllTasks(self): """ Get the list of all Tasks :returns: list of all Tasks """ response, body = self.http.get('/tasks') return body def getTask(self, taskId): """ Get the status of a task. :param taskId: the task id :type taskId: int :returns: the status of the task :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_BELOW_RANGE - Bad Request Task ID must be a positive value. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_EXCEEDS_RANGE - Bad Request Task ID is too large. :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_TASK - Task with the specified task ID does not exist. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_WRONG_TYPE - Task ID is not an integer. """ response, body = self.http.get('/tasks/%s' % taskId) return body def _findTask(self, name, active=True): cmd = ['showtask'] if active: cmd.append('-active') cmd.append(name) result = self._run(cmd) if result and len(result) == 1: if 'No tasks' in result[0]: return None elif len(result) == 2: return result[1].split(',') return result def stopOfflinePhysicalCopy(self, name): """ Stopping a offline physical copy operation. :param name: the name of the volume :type name: str :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Invalid VV name or CPG name. :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_CPG - The CPG does not exists. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - CPG_NOT_IN SAME_DOMAIN - The CPG is not in the current domain. :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - The volume does not exist :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - VV_NOT_IN_SAME_DOMAIN - The volume is not in the same domain. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_BAD_ENUM_VALUE - The priority value in not in the valid range(1-3). :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - EXISTENT_VOLUME - The volume already exists. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_SYS_VOLUME - The operation is not allowed on a system volume. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_NON_BASE_VOLUME - The destination volume is not a base volume. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_IN_REMOTE_COPY - The destination volume is involved in a remote copy. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_EXPORTED - The volume is exported. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_COPY_TO_SELF - The destination volume is the same as the parent. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_READONLY_SNAPSHOT - The parent volume is a read-only snapshot. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_COPY_TO_BASE - The destination volume is the base volume of a parent volume. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_VOLUME_CONV_IN_PROGRESS - The volume is in a conversion operation. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_NO_SNAPSHOT_ALLOWED - The parent volume must allow snapshots. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_ONLINE_COPY_IN_PROGRESS - The volume is the target of an online copy. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_CLEANUP_IN_PROGRESS - Cleanup of internal volume for the volume is in progress. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_CIRCULAR_COPY - The parent volume is a copy of the destination volume. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_PEER_VOLUME - The operation is not allowed on a peer volume. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_INTERNAL_VOLUME - The operation is not allowed on an internal volume. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - VV_IS_BEING_REMOVED - The volume is being removed. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_NOT_IN_NORMAL_STATE - The volume is not in the normal state. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - VV_IN_INCONSISTENT_STATE - The volume has an internal consistency error. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_PCOPY_IN_PROGRESS - The destination volume has a physical copy in progress. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_FAILED_ONLINE_COPY - Online copying of the destination volume has failed. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_COPY_PARENT_TOO_BIG - The size of the parent volume is larger than the size of the destination volume. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_NO_PARENT - The volume has no physical parent. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - IN_USE - The resynchronization snapshot is in a stale state. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - VV_IN_STALE_STATE - The volume is in a stale state. :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VVCOPY - Physical copy not found. """ info = {'action': self.STOP_PHYSICAL_COPY} response, body = self.http.put('/volumes/%s' % name, body=info) return body def createSnapshot(self, name, copyOfName, optional=None): """ Create a snapshot of an existing Volume :param name: Name of the Snapshot :type name: str :param copyOfName: The volume you want to snapshot :type copyOfName: str :param optional: Dictionary of optional params :type optional: dict .. code-block:: python optional = { 'id' : 12, # Specifies the ID of the volume, next by default 'comment' : "some comment", 'readOnly' : True, # Read Only 'expirationHours' : 36 # time from now to expire 'retentionHours' : 12 # time from now to expire } :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - The volume does not exist :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - PERM_DENIED - Permission denied """ parameters = {'name': name} if optional: parameters = self._mergeDict(parameters, optional) info = {'action': 'createSnapshot', 'parameters': parameters} response, body = self.http.post('/volumes/%s' % copyOfName, body=info) return body ##Host methods def getHosts(self): """ Get information about every Host on the 3Par array :returns: list of Hosts """ response, body = self.http.get('/hosts') return body def getHost(self, name): """ Get information about a Host :param name: The name of the Host to find :type name: str :returns: host dict :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_HOST - HOST doesn't exist """ response, body = self.http.get('/hosts/%s' % name) return body def createHost(self, name, iscsiNames=None, FCWwns=None, optional=None): """ Create a new Host entry TODO: get the list of thrown exceptions :param name: The name of the host :type name: str :param iscsiNames: Array if iscsi iqns :type name: array :param FCWwns: Array if Fibre Channel World Wide Names :type name: array :param optional: The optional stuff :type optional: dict .. code-block:: python optional = { 'domain' : 'myDomain', # Create the host in the specified domain, or default domain if unspecified. 'forceTearDown' : False, # If True, force to tear down low-priority VLUN exports. 'iSCSINames' : True, # Read Only 'descriptors' : {'location' : 'earth', 'IPAddr' : '10.10.10.10', 'os': 'linux', 'model' : 'ex', 'contact': 'Smith', 'comment' : 'Joe's box} } :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - PERM_DENIED - Permission denied :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_MISSING_REQUIRED - Name not specified. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_PARAM_CONFLICT - FCWWNs and iSCSINames are both specified. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_EXCEEDS_LENGTH - Host name, domain name, or iSCSI name is too long. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_EMPTY_STR - Input string (for domain name, iSCSI name, etc.) is empty. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Any error from host-name or domain-name parsing. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_TOO_MANY_WWN_OR_iSCSI - More than 1024 WWNs or iSCSI names are specified. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_WRONG_TYPE - The length of WWN is not 16. WWN specification contains non-hexadecimal digit. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - EXISTENT_PATH - host WWN/iSCSI name already used by another host :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - EXISTENT_HOST - host name is already used. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - NO_SPACE - No space to create host. """ info = {'name': name} if iscsiNames: iscsi = {'iSCSINames': iscsiNames} info = self._mergeDict(info, iscsi) if FCWwns: fc = {'FCWWNs': FCWwns} info = self._mergeDict(info, fc) if optional: info = self._mergeDict(info, optional) response, body = self.http.post('/hosts', body=info) return body def modifyHost(self, name, mod_request): """ Modify an existing Host entry :param name: The name of the host :type name: str :param mod_request: Objects for Host Modification Request :type mod_request: dict .. code-block:: python mod_request = { 'newName' : 'myNewName', # New name of the host 'pathOperation' : 1, # If adding, adds the WWN or iSCSI name to the existing host. 'FCWWNs' : [], # One or more WWN to set for the host. 'iSCSINames' : [], # One or more iSCSI names to set for the host. } :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT - Missing host name. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_PARAM_CONFLICT - Both iSCSINames & FCWWNs are specified. (lot of other possibilities) :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ONE_REQUIRED - iSCSINames or FCWwns missing. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ONE_REQUIRED - No path operation specified. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_BAD_ENUM_VALUE - Invalid enum value. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_MISSING_REQUIRED - Required fields missing. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_EXCEEDS_LENGTH - Host descriptor argument length, new host name, or iSCSI name is too long. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Error parsing host or iSCSI name. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - EXISTENT_HOST - New host name is already used. :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_HOST - Host to be modified does not exist. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_TOO_MANY_WWN_OR_iSCSI - More than 1024 WWNs or iSCSI names are specified. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_WRONG_TYPE - Input value is of the wrong type. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - EXISTENT_PATH - WWN or iSCSI name is already claimed by other host. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_BAD_LENGTH - CHAP hex secret length is not 16 bytes, or chap ASCII secret length is not 12 to 16 characters. :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NO_INITIATOR_CHAP - Setting target CHAP without initiator CHAP. :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_CHAP - Remove non-existing CHAP. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - NON_UNIQUE_CHAP_SECRET - CHAP secret is not unique. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - EXPORTED_VLUN - Setting persona with active export; remove a host path on an active export. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - NON_EXISTENT_PATH - Remove a non-existing path. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - LUN_HOSTPERSONA_CONFLICT - LUN number and persona capability conflict. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_DUP_PATH - Duplicate path specified. """ response = self.http.put('/hosts/%s' % name, body=mod_request) return response def deleteHost(self, name): """ Delete a Host :param name: Host Name :type name: str :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_HOST - HOST Not Found :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - IN_USE - The HOST Cannot be removed because it's in use. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - PERM_DENIED - Permission denied """ response, body = self.http.delete('/hosts/%s' % name) def findHost(self, iqn=None, wwn=None): """ Find a host from an iSCSI initiator or FC WWN :param iqn: lookup based on iSCSI initiator :type iqn: str :param wwn: lookup based on WWN :type wwn: str """ # for now there is no search in the REST API # so we can do a create looking for a specific # error. If we don't get that error, we nuke the # fake host. cmd = ['createhost'] #create a random hostname hostname = 'zxy-delete-vxz' if iqn: cmd.append('-iscsi') cmd.append(hostname) if iqn: cmd.append(iqn) else: cmd.append(wwn) result = self._run(cmd) test = ' '.join(result) search_str = "already used by host " if search_str in test: # host exists, return name used by 3par hostname_3par = self._get_next_word(test, search_str) return hostname_3par else: # host creation worked...so we need to remove it. # this means we didn't find an existing host that # is using the iqn or wwn. self.deleteHost(hostname) return None def queryHost(self, iqns=None, wwns=None): """ Find a host from an iSCSI initiator or FC WWN :param iqn: lookup based on iSCSI initiator list :type iqns: list :param wwn: lookup based on WWN list :type wwns: list :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT - Invalid URI syntax. :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_HOST - HOST Not Found :raises: :class:`~hp3parclient.exceptions.HTTPInternalServerError` - INTERNAL_SERVER_ERR - Internal server error. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Host name contains invalid character. """ wwnsQuery = '' if wwns: tmpQuery = [] for wwn in wwns: tmpQuery.append('wwn==%s' % wwn) wwnsQuery = ('FCPaths[%s]' % ' OR '.join(tmpQuery)) iqnsQuery = '' if iqns: tmpQuery = [] for iqn in iqns: tmpQuery.append('name==%s' % iqn) iqnsQuery = ('iSCSIPaths[%s]' % ' OR '.join(tmpQuery)) query = '' if wwnsQuery and iqnsQuery: query = ('%(wwns)s OR %(iqns)s' % ({'wwns': wwnsQuery, 'iqns': iqnsQuery})) elif wwnsQuery: query = wwnsQuery elif iqnsQuery: query = iqnsQuery query = '"%s"' % query response, body = self.http.get('/hosts?query=%s' % quote(query.encode("utf8"))) return body def getHostVLUNs(self, hostName): """ Get all of the VLUNs on a specific Host :param hostName: Host name :type hostNane: str :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_HOST - HOST Not Found """ # calling getHost to see if the host exists and raise not found # exception if it's not found. self.getHost(hostName) allVLUNs = self.getVLUNs() vluns = [] if allVLUNs: for vlun in allVLUNs['members']: if vlun['hostname'] == hostName: vluns.append(vlun) if len(vluns) < 1: raise exceptions.HTTPNotFound({'code': 'NON_EXISTENT_HOST', 'desc': 'HOST Not Found'}) return vluns ## PORT Methods def getPorts(self): """ Get the list of ports on the 3Par :returns: list of Ports """ response, body = self.http.get('/ports') return body def _getProtocolPorts(self, protocol, state=None): return_ports = [] ports = self.getPorts() if ports: for port in ports['members']: if port['protocol'] == protocol: if state is None: return_ports.append(port) elif port['linkState'] == state: return_ports.append(port) return return_ports def getFCPorts(self, state=None): """ Get a list of Fibre Channel Ports :returns: list of Fibre Channel Ports """ return self._getProtocolPorts(1, state) def getiSCSIPorts(self, state=None): """ Get a list of iSCSI Ports :returns: list of iSCSI Ports """ return self._getProtocolPorts(2, state) def getIPPorts(self, state=None): """ Get a list of IP Ports :returns: list of IP Ports """ return self._getProtocolPorts(4, state) ## CPG methods def getCPGs(self): """ Get entire list of CPGs :returns: list of cpgs """ response, body = self.http.get('/cpgs') return body def getCPG(self, name): """ Get information about a CPG :param name: The name of the CPG to find :type name: str :returns: cpg dict :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_CPG - CPG doesn't exist """ response, body = self.http.get('/cpgs/%s' % name) return body def createCPG(self, name, optional=None): """ Create a CPG :param name: CPG Name :type name: str :param optional: Optional parameters :type optional: dict .. code-block:: python optional = { 'growthIncrementMiB' : 100, 'growthLimitMiB' : 1024, 'usedLDWarningAlertMiB' : 200, 'domain' : 'MyDomain', 'LDLayout' : {'RAIDType' : 1, 'setSize' : 100, 'HA': 0, 'chunkletPosPref' : 2, 'diskPatterns': []} } :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT Invalid URI Syntax. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - NON_EXISTENT_DOMAIN - Domain doesn't exist. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - NO_SPACE - Not Enough space is available. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - BAD_CPG_PATTERN A Pattern in a CPG specifies illegal values. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - PERM_DENIED - Permission denied :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - EXISTENT_CPG - CPG Exists already """ info = {'name': name} if optional: info = self._mergeDict(info, optional) response, body = self.http.post('/cpgs', body=info) return body def deleteCPG(self, name): """ Delete a CPG :param name: CPG Name :type name: str :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_CPG - CPG Not Found :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - IN_USE - The CPG Cannot be removed because it's in use. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - PERM_DENIED - Permission denied """ response, body = self.http.delete('/cpgs/%s' % name) ## VLUN methods ## Virtual-LUN, or VLUN, is a pairing between a virtual volume and a ## logical unit number (LUN), expressed as either a VLUN template or ## an active ## VLUN ## A VLUN template sets up an association between a virtual volume and a ## LUN-host, LUN-port, or LUN-host-port combination by establishing the ## export rule or the manner in which the Volume is exported. def getVLUNs(self): """ Get VLUNs :returns: Array of VLUNs """ response, body = self.http.get('/vluns') return body def getVLUN(self, volumeName): """ Get information about a VLUN :param volumeName: The volume name of the VLUN to find :type name: str :returns: VLUN :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VLUN - VLUN doesn't exist """ vluns = self.getVLUNs() if vluns: for vlun in vluns['members']: if vlun['volumeName'] == volumeName: return vlun raise exceptions.HTTPNotFound({'code': 'NON_EXISTENT_VLUN', 'desc': "VLUN '%s' was not found" % volumeName}) def createVLUN(self, volumeName, lun=None, hostname=None, portPos=None, noVcn=None, overrideLowerPriority=None, auto=False): """ Create a new VLUN When creating a VLUN, the volumeName is required. The lun member is not required if auto is set to True. Either hostname or portPos (or both in the case of matched sets) is also required. The noVcn and overrideLowerPriority members are optional. :param volumeName: Name of the volume to be exported :type volumeName: str :param lun: The new LUN id :type lun: int :param hostname: Name of the host which the volume is to be exported. :type hostname: str :param portPos: 'portPos' (dict) - System port of VLUN exported to. It includes node number, slot number, and card port number :type portPos: dict :param noVcn: A VLUN change notification (VCN) not be issued after export (-novcn). Default: False. :type noVcn: bool :param overrideLowerPriority: Existing lower priority VLUNs will be overridden (-ovrd). Use only if hostname member exists. Default: False. :type overrideLowerPriority: bool :returns: the location of the VLUN """ info = {'volumeName': volumeName} if lun: info['lun'] = lun if hostname: info['hostname'] = hostname if portPos: info['portPos'] = portPos if noVcn: info['noVcn'] = noVcn if overrideLowerPriority: info['overrideLowerPriority'] = overrideLowerPriority if auto: info['autoLun'] = True info['maxAutoLun'] = 0 info['lun'] = 0 headers, body = self.http.post('/vluns', body=info) if headers: location = headers['location'].replace('/api/v1/vluns/', '') return location else: return None def deleteVLUN(self, volumeName, lunID, hostname=None, port=None): """ Delete a VLUN :param volumeName: the volume name of the VLUN :type name: str :param lunID: The LUN ID :type lunID: int :param hostname: Name of the host which the volume is exported. For VLUN of port type,the value is empty :type hostname: str :param port: Specifies the system port of the VLUN export. It includes the system node number, PCI bus slot number, and card port number on the FC card in the format :: :type port: dict :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_MISSING_REQUIRED - Incomplete VLUN info. Missing volumeName or lun, or both hostname and port. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_PORT_SELECTION - Specified port is invalid. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_EXCEEDS_RANGE - The LUN specified exceeds expected range. :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_HOST - The host does not exist :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VLUN - The VLUN does not exist :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_PORT - The port does not exist :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - PERM_DENIED - Permission denied """ vlun = "%s,%s" % (volumeName, lunID) if hostname: vlun += ",%s" % hostname if port: vlun += ",%s:%s:%s" % (port['node'], port['slot'], port['cardPort']) response, body = self.http.delete('/vluns/%s' % vlun) ## VolumeSet methods def findVolumeSet(self, name): """ Find the Volume Set name for a volume. :param name: the volume name :type name: str """ cmd = ['showvvset', '-vv', name] out = self._run(cmd) vvset_name = None if out and len(out) > 1: info = out[1].split(",") vvset_name = info[1] return vvset_name def getVolumeSets(self): """ Get Volume Sets :returns: Array of Volume Sets """ response, body = self.http.get('/volumesets') return body def getVolumeSet(self, name): """ Get information about a Volume Set :param name: The name of the Volume Set to find :type name: str :returns: Volume Set :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_SET - The set doesn't exist """ response, body = self.http.get('/volumesets/%s' % name) return body def createVolumeSet(self, name, domain=None, comment=None, setmembers=None): """ This creates a new volume set :param name: the volume set to create :type set_name: str :param domain: the domain where the set lives :type domain: str :param comment: the comment for on the vv set :type comment: str :param setmembers: the vv to add to the set, the existence of the vv will not be checked :type setmembers: array :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - EXISTENT_SET - The set already exits. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - MEMBER_IN_DOMAINSET - The host is in a domain set. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - MEMBER_IN_SET - The object is already part of the set. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - MEMBER_NOT_IN_SAME_DOMAIN - Objects must be in the same domain to perform this operation. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - VV_IN_INCONSISTENT_STATE - The volume has an internal inconsistency error. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - VV_IS_BEING_REMOVED - The volume is being removed. :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOLUME - The volume does not exists. :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_HOST - The host does not exists. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_SYS_VOLUME - The operation is not allowed on a system volume. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_INTERNAL_VOLUME - The operation is not allowed on an internal volume. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_DUP_NAME - Invalid input (duplicate name). """ info = {'name': name} if domain: info['domain'] = domain if comment: info['comment'] = comment if setmembers: members = {'setmembers': setmembers} info = self._mergeDict(info, members) response, body = self.http.post('/volumesets', body=info) def deleteVolumeSet(self, name): """ This removes a volume set. You must clear all QOS rules before a volume set can be deleted. :param name: the volume set to remove :type name: str :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_SET - The set does not exists. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - EXPORTED_VLUN - The host set has exported VLUNs. The VV set was exported. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - VVSET_QOS_TARGET - The object is already part of the set. """ response, body = self.http.delete('/volumesets/%s' % name) def modifyVolumeSet(self, name, action=None, newName=None, comment=None, setmembers=None): """ This modifies a volume set by adding or remove a volume from the volume set. It's actions is based on the enums SET_MEM_ADD or SET_MEM_REMOVE. :param action: add or remove volume from the set :type action: enum :param name: the volume set name :type name: str :param newName: new name of set :type newName: str :param comment: the comment for on the vv set :type comment: str :param setmembers: the vv to add to the set, the existence of the vv will not be checked :type setmembers: array :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - EXISTENT_SET - The set already exits. :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_SET - The set does not exists. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - MEMBER_IN_DOMAINSET - The host is in a domain set. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - MEMBER_IN_SET - The object is already part of the set. :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - MEMBER_NOT_IN_SET - The object is not part of the set. :raises: :class:`~hp3parclient.exceptions.HTTPConflict` - MEMBER_NOT_IN_SAME_DOMAIN - Objects must be in the same domain to perform this operation. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - VV_IN_INCONSISTENT_STATE - The volume has an internal inconsistency error. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - VV_IS_BEING_REMOVED - The volume is being removed. :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOLUME - The volume does not exists. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_SYS_VOLUME - The operation is not allowed on a system volume. :raises: :class:`~hp3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_INTERNAL_VOLUME - The operation is not allowed on an internal volume. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_DUP_NAME - Invalid input (duplicate name). :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_PARAM_CONFLICT - Invalid input (parameters cannot be present at the same time). :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Invalid contains one or more illegal characters. """ info = {} if action: info['action'] = action if newName: info['newName'] = newName if comment: info['comment'] = comment if setmembers: members = {'setmembers': setmembers} info = self._mergeDict(info, members) response = self.http.put('/volumesets/%s' % name, body=info) return response # QoS Priority Optimization methods def addVolumeToVolumeSet(self, set_name, name): """ This adds a volume to a volume set :param set_name: the volume set name :type set_name: str :param name: the volume name to add :type name: str """ return self.modifyVolumeSet(set_name, action=self.SET_MEM_ADD, setmembers=[name]) def removeVolumeFromVolumeSet(self, set_name, name): """ Remove a volume from a volume set :param set_name: the volume set name :type set_name: str :param name: the volume name to add :type name: str """ return self.modifyVolumeSet(set_name, action=self.SET_MEM_REMOVE, setmembers=[name]) # QoS Priority Optimization methods def setQOSRule(self, set_name, max_io=None, max_bw=None): """ Set a QOS Rule on a volume set :param set_name: the volume set name for the rule. :type set_name: str :param max_io: the maximum IOPS value :type max_io: int :param max_bw: The maximum Bandwidth :type max_bw: """ cmd = ['setqos'] if max_io is not None: cmd.extend(['-io', '%s' % max_io]) if max_bw is not None: cmd.extend(['-bw', '%sM' % max_bw]) cmd.append('vvset:' + set_name) result = self._run(cmd) if result: msg = result[0] else: msg = None if msg: if 'no matching QoS target found' in msg: raise exceptions.HTTPNotFound(error={'desc': msg}) else: raise exceptions.SetQOSRuleException(message=msg) def queryQoSRules(self): """ Get QoS Rules :returns: Array of QoS Rules """ response, body = self.http.get('/qos') return body def queryQoSRule(self, targetName, targetType='vvset'): """ Query a QoS rule :param targetType: target type is vvset or sys :type targetType: str :param targetName: the name of the target. When targetType is sys, target name must be sys:all_others. :type targetName: str :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_QOS_RULE - QoS rule does not exist. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Illegal character in the input. """ response, body = self.http.get('/qos/%(targetType)s:%(targetName)s' % {'targetType': targetType, 'targetName': targetName}) return body def createQoSRules(self, targetName, qosRules, target_type=TARGET_TYPE_VVSET): """ Create QOS rules The QoS rule can be applied to VV sets. By using sys:all_others, you can apply the rule to all volumes in the system for which no QoS rule has been defined. ioMinGoal and ioMaxLimit must be used together to set I/O limits. Similarly, bwMinGoalKB and bwMaxLimitKB must be used together. If ioMaxLimitOP is set to 2 (no limit), ioMinGoalOP must also be to set to 2 (zero), and vice versa. They cannot be set to 'none' individually. Similarly, if bwMaxLimitOP is set to 2 (no limit), then bwMinGoalOP must also be set to 2. If ioMaxLimitOP is set to 1 (no limit), ioMinGoalOP must also be to set to 1 (zero) and vice versa. Similarly, if bwMaxLimitOP is set to 1 (zero), then bwMinGoalOP must also be set to 1. The ioMinGoalOP and ioMaxLimitOP fields take precedence over the ioMinGoal and ioMaxLimit fields. The bwMinGoalOP and bwMaxLimitOP fields take precedence over the bwMinGoalKB and bwMaxLimitKB fields :param target_type: Type of QoS target, either enum TARGET_TYPE_VVS or TARGET_TYPE_SYS. :type target_type: enum :param targetName: the name of the target object on which the QoS rule will be created. :type targetName: str :param qosRules: QoS options :type qosRules: dict .. code-block:: python qosRules = { 'priority': 2, # priority enum 'bwMinGoalKB': 1024, # bandwidth rate minimum goal in kilobytes per second 'bwMaxLimitKB': 1024, # bandwidth rate maximum limit in kilobytes per second 'ioMinGoal': 10000, # I/O-per-second minimum goal 'ioMaxLimit': 2000000, # I/0-per-second maximum limit 'enable': True, # QoS rule for target enabled? 'bwMinGoalOP': 1, # zero none operation enum, when set to 1, bandwidth minimum goal is 0 # when set to 2, the bandwidth mimumum goal is none (NoLimit) 'bwMaxLimitOP': 1, # zero none operation enum, when set to 1, bandwidth maximum limit is 0 # when set to 2, the bandwidth maximum limit is none (NoLimit) 'ioMinGoalOP': 1, # zero none operation enum, when set to 1, I/O minimum goal is 0 # when set to 2, the I/O minimum goal is none (NoLimit) 'ioMaxLimitOP': 1, # zero none operation enum, when set to 1, I/O maximum limit is 0 # when set to 2, the I/O maximum limit is none (NoLimit) 'latencyGoal': 5000, # Latency goal in milliseconds 'defaultLatency': False # Use latencyGoal or defaultLatency? } :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_EXCEEDS_RANGE - Invalid input: number exceeds expected range. :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_QOS_RULE - QoS rule does not exists. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Illegal character in the input. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - EXISTENT_QOS_RULE - QoS rule already exists. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_MIN_GOAL_GRT_MAX_LIMIT - I/O-per-second maximum limit should be greater than the minimum goal. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_BW_MIN_GOAL_GRT_MAX_LIMIT - Bandwidth maximum limit should be greater than the mimimum goal. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_BELOW_RANGE - I/O-per-second limit is below range. Bandwidth limit is below range. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - UNLICENSED_FEATURE - The system is not licensed for QoS. """ info = {'name': targetName, 'type': target_type} info = self._mergeDict(info, qosRules) response, body = self.http.post('/qos', body=info) return body def modifyQoSRules(self, targetName, qosRules, targetType='vvset'): """ Modify an existing QOS rules The QoS rule can be applied to VV sets. By using sys:all_others, you can apply the rule to all volumes in the system for which no QoS rule has been defined. ioMinGoal and ioMaxLimit must be used together to set I/O limits. Similarly, bwMinGoalKB and bwMaxLimitKB must be used together. If ioMaxLimitOP is set to 2 (no limit), ioMinGoalOP must also be to set to 2 (zero), and vice versa. They cannot be set to 'none' individually. Similarly, if bwMaxLimitOP is set to 2 (no limit), then bwMinGoalOP must also be set to 2. If ioMaxLimitOP is set to 1 (no limit), ioMinGoalOP must also be to set to 1 (zero) and vice versa. Similarly, if bwMaxLimitOP is set to 1 (zero), then bwMinGoalOP must also be set to 1. The ioMinGoalOP and ioMaxLimitOP fields take precedence over the ioMinGoal and ioMaxLimit fields. The bwMinGoalOP and bwMaxLimitOP fields take precedence over the bwMinGoalKB and bwMaxLimitKB fields :param targetName: the name of the target object on which the QoS rule will be created. :type targetName: str :param targetType: Type of QoS target, either vvset or sys :type targetType: str :param qosRules: QoS options :type qosRules: dict .. code-block:: python qosRules = { 'priority': 2, # priority enum 'bwMinGoalKB': 1024, # bandwidth rate minimum goal in kilobytes per second 'bwMaxLimitKB': 1024, # bandwidth rate maximum limit in kilobytes per second 'ioMinGoal': 10000, # I/O-per-second minimum goal. 'ioMaxLimit': 2000000, # I/0-per-second maximum limit 'enable': True, # QoS rule for target enabled? 'bwMinGoalOP': 1, # zero none operation enum, when set to 1, bandwidth minimum goal is 0 # when set to 2, the bandwidth minimum goal is none (NoLimit) 'bwMaxLimitOP': 1, # zero none operation enum, when set to 1, bandwidth maximum limit is 0 # when set to 2, the bandwidth maximum limit is none (NoLimit) 'ioMinGoalOP': 1, # zero none operation enum, when set to 1, I/O minimum goal minimum goal is 0 # when set to 2, the I/O minimum goal is none (NoLimit) 'ioMaxLimitOP': 1, # zero none operation enum, when set to 1, I/O maximum limit is 0 # when set to 2, the I/O maximum limit is none (NoLimit) 'latencyGoal': 5000, # Latency goal in milliseconds 'defaultLatency': False # Use latencyGoal or defaultLatency? } :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_EXCEEDS_RANGE - Invalid input: number exceeds expected range. :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_QOS_RULE - QoS rule does not exists. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Illegal character in the input. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - EXISTENT_QOS_RULE - QoS rule already exists. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_IO_MIN_GOAL_GRT_MAX_LIMIT - I/O-per-second maximum limit should be greater than the minimum goal. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_BW_MIN_GOAL_GRT_MAX_LIMIT - Bandwidth maximum limit should be greater than the minimum goal. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_BELOW_RANGE - I/O-per-second limit is below range. Bandwidth limit is below range. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - UNLICENSED_FEATURE - The system is not licensed for QoS. """ response = self.http.put('/qos/%(targetType)s:%(targetName)s' % {'targetType': targetType, 'targetName': targetName}, body=qosRules) return response def deleteQoSRules(self, targetName, targetType='vvset'): """ Clear and Delete QoS rules :param targetType: target type is vvset or sys :type targetType: str :param targetName: the name of the target. When targetType is sys, target name must be sys:all_others. :type targetName: str :raises: :class:`~hp3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_QOS_RULE - QoS rule does not exist. :raises: :class:`~hp3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Illegal character in the input. """ response, body = self.http.delete('/qos/%(targetType)s:%(targetName)s' % {'targetType': targetType, 'targetName': targetName}) return body def setVolumeMetaData(self, name, key, value): """ This is used to set a key/value pair metadata into a volume. :param name: the volume name :type name: str :param key: the metadata key name :type key: str :param value: the metadata value :type value: str """ cmd = ['setvv', '-setkv', key + '=' + value, name] result = self._run(cmd) if result and len(result) == 1: if 'does not exist' in result[0]: raise exceptions.HTTPNotFound(error={'desc': result[0]}) def removeVolumeMetaData(self, name, key): """ This is used to remove a metadata key/value pair from a volume. :param name: the volume name :type name: str :param key: the metadata key name :type key: str """ cmd = ['setvv', '-clrkey', key, name] result = self._run(cmd) if result and len(result) == 1: if 'does not exist' in result[0]: raise exceptions.HTTPNotFound(error={'desc': result[0]}) def _mergeDict(self, dict1, dict2): """ Safely merge 2 dictionaries together :param dict1: The first dictionary :type dict1: dict :param dict2: The second dictionary :type dict2: dict :returns: dict :raises Exception: dict1, dict2 is not a dictionary """ if type(dict1) is not dict: raise Exception("dict1 is not a dictionary") if type(dict2) is not dict: raise Exception("dict2 is not a dictionary") dict3 = dict1.copy() dict3.update(dict2) return dict3 def _get_next_word(self, s, search_string): """Return the next word. Search 's' for 'search_string', if found return the word preceding 'search_string' from 's'. """ word = re.search(search_string.strip(' ') + ' ([^ ]*)', s) return word.groups()[0].strip(' ') hp3parclient-3.0.0/hp3parclient/http.py0000664000175000017500000002433612276214354020307 0ustar stackstack00000000000000# vim: tabstop=4 shiftwidth=4 softtabstop=4 # # Copyright 2012 Hewlett Packard Development Company, L.P. # 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. """ HTTPJSONRESTClient. .. module: http :Author: Walter A. Boring IV :Description: This is the HTTP Client that is used to make the actual calls. It includes the authentication that knows the cookie name for 3PAR. """ import logging import httplib2 import time import pprint try: import json except ImportError: import simplejson as json from hp3parclient import exceptions class HTTPJSONRESTClient(httplib2.Http): """ An HTTP REST Client that sends and recieves JSON data as the body of the HTTP request :param api_url: The url to the WSAPI service on 3PAR ie. http://<3par server>:8080 :type api_url: str :param insecure: Use https? requires a local certificate :type insecure: bool """ USER_AGENT = 'python-3parclient' SESSION_COOKIE_NAME = 'X-Hp3Par-Wsapi-Sessionkey' def __init__(self, api_url, insecure=False, http_log_debug=False): super(HTTPJSONRESTClient, self).__init__(disable_ssl_certificate_validation=True) self.session_key = None #should be http:///api/v1 self.set_url(api_url) self.set_debug_flag(http_log_debug) self.times = [] # [("item", starttime, endtime), ...] # httplib2 overrides self.force_exception_to_status_code = True #self.disable_ssl_certificate_validation = insecure self._logger = logging.getLogger(__name__) def set_url(self, api_url): #should be http:///api/v1 self.api_url = api_url.rstrip('/') self.api_url = self.api_url def set_debug_flag(self, flag): """ This turns on/off http request/response debugging output to console :param flag: Set to True to enable debugging output :type flag: bool """ self.http_log_debug = flag if self.http_log_debug: ch = logging.StreamHandler() self._logger.setLevel(logging.DEBUG) self._logger.addHandler(ch) def authenticate(self, user, password, optional=None): """ This tries to create an authenticated session with the 3PAR server :param user: The username :type user: str :param password: Password :type password: str """ #this prevens re-auth attempt if auth fails self.auth_try = 1 self.session_key = None info = {'user':user, 'password':password} self._auth_optional = None if optional: self._auth_optional = optional info.update(optional) resp, body = self.post('/credentials', body=info) if body and 'key' in body: self.session_key = body['key'] self.auth_try = 0 self.user = user self.password = password def _reauth(self): self.authenticate(self.user, self.password, self._auth_optional) def unauthenticate(self): """ This clears the authenticated session with the 3PAR server. It logs out. """ #delete the session on the 3Par self.delete('/credentials/%s' % self.session_key) self.session_key = None def get_timings(self): """ Ths gives an array of the request timings since last reset_timings call """ return self.times def reset_timings(self): """ This resets the request/response timings array """ self.times = [] def _http_log_req(self, args, kwargs): if not self.http_log_debug: return string_parts = ['curl -i'] for element in args: if element in ('GET', 'POST'): string_parts.append(' -X %s' % element) else: string_parts.append(' %s' % element) for element in kwargs['headers']: header = ' -H "%s: %s"' % (element, kwargs['headers'][element]) string_parts.append(header) self._logger.debug("\nREQ: %s\n" % "".join(string_parts)) if 'body' in kwargs: self._logger.debug("REQ BODY: %s\n" % (kwargs['body'])) def _http_log_resp(self, resp, body): if not self.http_log_debug: return self._logger.debug("RESP:%s\n", pprint.pformat(resp)) self._logger.debug("RESP BODY:%s\n", body) def request(self, *args, **kwargs): """ This makes an HTTP Request to the 3Par server. You should use get, post, delete instead. """ if self.session_key and self.auth_try != 1 : kwargs.setdefault('headers', {})[self.SESSION_COOKIE_NAME] = self.session_key kwargs.setdefault('headers', kwargs.get('headers', {})) kwargs['headers']['User-Agent'] = self.USER_AGENT kwargs['headers']['Accept'] = 'application/json' if 'body' in kwargs: kwargs['headers']['Content-Type'] = 'application/json' kwargs['body'] = json.dumps(kwargs['body']) self._http_log_req(args, kwargs) resp, body = super(HTTPJSONRESTClient, self).request(*args, **kwargs) self._http_log_resp(resp, body) # Try and conver the body response to an object # This assumes the body of the reply is JSON if body: try: body = json.loads(body) except ValueError: #pprint.pprint("failed to decode json\n") pass else: body = None if resp.status >= 400: raise exceptions.from_response(resp, body) return resp, body def _time_request(self, url, method, **kwargs): start_time = time.time() resp, body = self.request(url, method, **kwargs) self.times.append(("%s %s" % (method, url), start_time, time.time())) return resp, body def _do_reauth(self, url, method, ex, **kwargs): print("_do_reauth called") try: if self.auth_try != 1: self._reauth() resp, body = self._time_request(self.api_url + url, method, **kwargs) return resp, body else: raise ex except exceptions.HTTPUnauthorized: raise ex def _cs_request(self, url, method, **kwargs): # Perform the request once. If we get a 401 back then it # might be because the auth token expired, so try to # re-authenticate and try again. If it still fails, bail. try: resp, body = self._time_request(self.api_url + url, method, **kwargs) return resp, body except exceptions.HTTPUnauthorized as ex: print("_CS_REQUEST HTTPUnauthorized") resp, body = self._do_reauth(url, method, ex, **kwargs) return resp, body except exceptions.HTTPForbidden as ex: print("_CS_REQUEST HTTPForbidden") resp, body = self._do_reauth(url, method, ex, **kwargs) return resp, body def get(self, url, **kwargs): """ Make an HTTP GET request to the server. .. code-block:: python #example call try { headers, body = http.get('/volumes') } except exceptions.HTTPUnauthorized as ex: print "Not logged in" } :param url: The relative url from the 3PAR api_url :type url: str :returns: headers - dict of HTTP Response headers :returns: body - the body of the response. If the body was JSON, it will be an object """ return self._cs_request(url, 'GET', **kwargs) def post(self, url, **kwargs): """ Make an HTTP POST request to the server. .. code-block:: python #example call try { info = {'name': 'new volume name', 'cpg': 'MyCPG', 'sizeMiB': 300} headers, body = http.post('/volumes', body=info) } except exceptions.HTTPUnauthorized as ex: print "Not logged in" } :param url: The relative url from the 3PAR api_url :type url: str :returns: headers - dict of HTTP Response headers :returns: body - the body of the response. If the body was JSON, it will be an object """ return self._cs_request(url, 'POST', **kwargs) def put(self, url, **kwargs): """ Make an HTTP PUT request to the server. .. code-block:: python #example call try { info = {'name': 'something'} headers, body = http.put('/volumes', body=info) } except exceptions.HTTPUnauthorized as ex: print "Not logged in" } :param url: The relative url from the 3PAR api_url :type url: str :returns: headers - dict of HTTP Response headers :returns: body - the body of the response. If the body was JSON, it will be an object """ return self._cs_request(url, 'PUT', **kwargs) def delete(self, url, **kwargs): """ Make an HTTP DELETE request to the server. .. code-block:: python #example call try { headers, body = http.delete('/volumes/%s' % name) } except exceptions.HTTPUnauthorized as ex: print "Not logged in" } :param url: The relative url from the 3PAR api_url :type url: str :returns: headers - dict of HTTP Response headers :returns: body - the body of the response. If the body was JSON, it will be an object """ return self._cs_request(url, 'DELETE', **kwargs) hp3parclient-3.0.0/hp3parclient/__init__.py0000664000175000017500000000214012276214444021054 0ustar stackstack00000000000000# Copyright 2012-2014 Hewlett Packard Development Company, L.P. # 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. """ HP 3PAR Client :Author: Walter A. Boring IV :Author: Kurt Martin :Copyright: Copyright 2012-2014, Hewlett Packard Development Company, L.P. :License: Apache v2.0 """ version_tuple = (3, 0, 0) def get_version_string(): if isinstance(version_tuple[-1], str): return '.'.join(map(str, version_tuple[:-1])) + version_tuple[-1] return '.'.join(map(str, version_tuple)) version = get_version_string() """Current version of HP3PARClient.""" hp3parclient-3.0.0/hp3parclient/ssh.py0000664000175000017500000002052512276217477020132 0ustar stackstack00000000000000# Copyright 2014 Hewlett Packard Development Company, L.P. # 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. """ HP3Par SSH Client .. module: ssh :Author: Walter A. Boring IV :Description: This is the SSH Client that is used to make calls to the 3PAR where an existing REST API doesn't exist. """ import logging import os import paramiko from random import randint import re from eventlet import greenthread from hp3parclient import exceptions class HP3PARSSHClient(object): """This class is used to execute SSH commands on a 3PAR.""" def __init__(self, ip, login, password, port=22, conn_timeout=None, privatekey=None): self.san_ip = ip self.san_ssh_port = port self.ssh_conn_timeout = conn_timeout self.san_login = login self.san_password = password self.san_private_key = privatekey self._logger = logging.getLogger(__name__) self._create_ssh() def _create_ssh(self): try: ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) if self.san_password: ssh.connect(self.san_ip, port=self.san_ssh_port, username=self.san_login, password=self.san_password, timeout=self.ssh_conn_timeout) elif self.san_privatekey: pkfile = os.path.expanduser(self.san_privatekey) privatekey = paramiko.RSAKey.from_private_key_file(pkfile) ssh.connect(self.san_ip, port=self.san_ssh_port, username=self.san_login, pkey=privatekey, timeout=self.ssh_conn_timeout) else: msg = "Specify a password or private_key" raise exceptions.SSHException(msg) # Paramiko by default sets the socket timeout to 0.1 seconds, # ignoring what we set through the sshclient. This doesn't help for # keeping long lived connections. Hence we have to bypass it, by # overriding it after the transport is initialized. We are setting # the sockettimeout to None and setting a keepalive packet so that, # the server will keep the connection open. All that does is send # a keepalive packet every ssh_conn_timeout seconds. if self.ssh_conn_timeout: transport = ssh.get_transport() transport.sock.settimeout(None) transport.set_keepalive(self.ssh_conn_timeout) self.ssh = ssh except Exception as e: msg = "Error connecting via ssh: %s" % e self._logger.error(msg) raise paramiko.SSHException(msg) def close(self): if self.ssh: print("closing ssh") self.ssh.close() def set_debug_flag(self, flag): """ This turns on/off http request/response debugging output to console :param flag: Set to True to enable debugging output :type flag: bool """ self.log_debug = flag if self.log_debug: ch = logging.StreamHandler() self._logger.setLevel(logging.DEBUG) self._logger.addHandler(ch) def run(self, cmd): """Runs a CLI command over SSH, without doing any result parsing.""" self._logger.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] self._logger.debug("OUT = %s" % out) return out def _ssh_execute(self, 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. """ self._logger.debug('Running cmd (SSH): %s', cmd) channel = self.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: self._logger.debug('Result was %s' % exit_status) if check_exit_code and exit_status != 0: msg = "command %s failed" % cmd self._logger.error(msg) raise exceptions.ProcessExecutionError(exit_code=exit_status, stdout=stdout, stderr=stderr, cmd=cmd) channel.close() return (stdout, stderr) def _run_ssh(self, cmd_list, check_exit=True, attempts=1): self.check_ssh_injection(cmd_list) command = ' '. join(cmd_list) try: total_attempts = attempts while attempts > 0: attempts -= 1 try: return self._ssh_execute(command, check_exit_code=check_exit) except Exception as e: self._logger.error(e) greenthread.sleep(randint(20, 500) / 100.0) msg = ("SSH Command failed after '%(total_attempts)r' " "attempts : '%(command)s'" % {'total_attempts': total_attempts, 'command': command}) self._logger.error(msg) raise exceptions.SSHException(message=msg) except Exception: self._logger.error("Error running ssh command: %s" % command) def check_ssh_injection(self, cmd_list): ssh_injection_pattern = ['`', '$', '|', '||', ';', '&', '&&', '>', '>>', '<'] # Check whether injection attacks exist for arg in cmd_list: arg = arg.strip() # Check for matching quotes on the ends is_quoted = re.match('^(?P[\'"])(?P.*)(?P=quote)$', arg) if is_quoted: # Check for unescaped quotes within the quoted argument quoted = is_quoted.group('quoted') if quoted: if (re.match('[\'"]', quoted) or re.search('[^\\\\][\'"]', quoted)): raise exceptions.SSHInjectionThreat(command= str(cmd_list)) else: # We only allow spaces within quoted arguments, and that # is the only special character allowed within quotes if len(arg.split()) > 1: raise exceptions.SSHInjectionThreat(command=str(cmd_list)) # Second, check whether danger character in command. So the shell # special operator must be a single argument. for c in ssh_injection_pattern: if arg == c: continue result = arg.find(c) if not result == -1: if result == 0 or not arg[result - 1] == '\\': raise exceptions.SSHInjectionThreat(command=cmd_list) hp3parclient-3.0.0/hp3parclient/exceptions.py0000664000175000017500000002503412276214354021505 0ustar stackstack00000000000000# vim: tabstop=4 shiftwidth=4 softtabstop=4 # # Copyright 2012 Hewlett Packard Development Company, L.P. # 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. """ Exceptions for the client .. module: exceptions :Author: Walter A. Boring IV :Description: This contains the HTTP exceptions that can come back from the REST calls to 3PAR """ import logging import sys LOG = logging.getLogger(__name__) class UnsupportedVersion(Exception): """ Indicates that the user is trying to use an unsupported version of the API """ pass class CommandError(Exception): pass class AuthorizationFailure(Exception): pass class NoUniqueMatch(Exception): pass class ClientException(Exception): """ The base exception class for all exceptions this library raises. :param error: The error array :type error: array """ _error_code = None _error_desc = None _error_ref = None _debug1 = None _debug2 = None def __init__(self, error=None): if 'code' in error: self._error_code = error['code'] if 'desc' in error: self._error_desc = error['desc'] if 'ref' in error: self._error_ref = error['ref'] if 'debug1' in error: self._debug1 = error['debug1'] if 'debug2' in error: self._debug2 = error['debug2'] def get_code(self): return self._error_code def get_description(self): return self._error_desc def get_ref(self): return self._error_ref def __str__(self): formatted_string = "%s (HTTP %s)" % (self.message, self.http_status) if self._error_code: formatted_string += " %s" % self._error_code if self._error_desc: formatted_string += " - %s" % self._error_desc if self._error_ref: formatted_string += " - %s" % self._error_ref if self._debug1: formatted_string += " (1: '%s')" % self._debug1 if self._debug2: formatted_string += " (2: '%s')" % self._debug2 return formatted_string ## ## 400 Errors ## class HTTPBadRequest(ClientException): """ HTTP 400 - Bad request: you sent some malformed data. """ http_status = 400 message = "Bad request" class HTTPUnauthorized(ClientException): """ HTTP 401 - Unauthorized: bad credentials. """ http_status = 401 message = "Unauthorized" class HTTPForbidden(ClientException): """ HTTP 403 - Forbidden: your credentials don't give you access to this resource. """ http_status = 403 message = "Forbidden" class HTTPNotFound(ClientException): """ HTTP 404 - Not found """ http_status = 404 message = "Not found" class HTTPMethodNotAllowed(ClientException): """ HTTP 405 - Method not Allowed """ http_status = 405 message = "Method Not Allowed" class HTTPNotAcceptable(ClientException): """ HTTP 406 - Method not Acceptable """ http_status = 406 message = "Method Not Acceptable" class HTTPProxyAuthRequired(ClientException): """ HTTP 407 - The client must first authenticate itself with the proxy. """ http_status = 407 message = "Proxy Authentication Required" class HTTPRequestTimeout(ClientException): """ HTTP 408 - The server timed out waiting for the request. """ http_status = 408 message = "Request Timeout" class HTTPConflict(ClientException): """ HTTP 409 - Conflict: A Conflict happened on the server """ http_status = 409 message = "Conflict" class HTTPGone(ClientException): """ HTTP 410 - Indicates that the resource requested is no longer available and will not be available again. """ http_status = 410 message = "Gone" class HTTPLengthRequired(ClientException): """ HTTP 411 - The request did not specify the length of its content, which is required by the requested resource. """ http_status = 411 message = "Length Required" class HTTPPreconditionFailed(ClientException): """ HTTP 412 - The server does not meet one of the preconditions that the requester put on the request. """ http_status = 412 message = "Over limit" class HTTPRequestEntityTooLarge(ClientException): """ HTTP 413 - The request is larger than the server is willing or able to process """ http_status = 413 message = "Request Entity Too Large" class HTTPRequestURITooLong(ClientException): """ HTTP 414 - The URI provided was too long for the server to process. """ http_status = 414 message = "Request URI Too Large" class HTTPUnsupportedMediaType(ClientException): """ HTTP 415 - The request entity has a media type which the server or resource does not support. """ http_status = 415 message = "Unsupported Media Type" class HTTPRequestedRangeNotSatisfiable(ClientException): """ HTTP 416 - The client has asked for a portion of the file, but the server cannot supply that portion. """ http_status = 416 message = "Requested Range Not Satisfiable" class HTTPExpectationFailed(ClientException): """ HTTP 417 - The server cannot meet the requirements of the Expect request-header field. """ http_status = 417 message = "Expectation Failed" class HTTPTeaPot(ClientException): """ HTTP 418 - I'm a Tea Pot """ http_status = 418 message = "I'm A Teapot. (RFC 2324)" ## ## 500 Errors ## class HTTPInternalServerError(ClientException): """ HTTP 500 - Internal Server Error: an internal error occured. """ http_status = 500 message = "Internal Server Error" # NotImplemented is a python keyword. class HTTPNotImplemented(ClientException): """ HTTP 501 - Not Implemented: the server does not support this operation. """ http_status = 501 message = "Not Implemented" class HTTPBadGateway(ClientException): """ HTTP 502 - The server was acting as a gateway or proxy and received an invalid response from the upstream server. """ http_status = 502 message = "Bad Gateway" class HTTPServiceUnavailable(ClientException): """ HTTP 503 - The server is currently unavailable """ http_status = 503 message = "Service Unavailable" class HTTPGatewayTimeout(ClientException): """ HTTP 504 - The server was acting as a gateway or proxy and did not receive a timely response from the upstream server. """ http_status = 504 message = "Gateway Timeout" class HTTPVersionNotSupported(ClientException): """ HTTP 505 - The server does not support the HTTP protocol version used in the request. """ http_status = 505 message = "Version Not Supported" # In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__() # so we can do this: # _code_map = dict((c.http_status, c) # for c in ClientException.__subclasses__()) # # Instead, we have to hardcode it: _code_map = dict((c.http_status, c) for c in [HTTPBadRequest, HTTPUnauthorized, HTTPForbidden, HTTPNotFound, HTTPMethodNotAllowed, HTTPNotAcceptable, HTTPProxyAuthRequired, HTTPRequestTimeout, HTTPConflict, HTTPGone, HTTPLengthRequired, HTTPPreconditionFailed, HTTPRequestEntityTooLarge, HTTPRequestURITooLong, HTTPUnsupportedMediaType, HTTPRequestedRangeNotSatisfiable, HTTPExpectationFailed, HTTPTeaPot, HTTPNotImplemented, HTTPBadGateway, HTTPServiceUnavailable, HTTPGatewayTimeout, HTTPVersionNotSupported]) def from_response(response, body): """ Return an instance of an ClientException or subclass based on an httplib2 response. Usage:: resp, body = http.request(...) if resp.status != 200: raise exception_from_response(resp, body) """ cls = _code_map.get(response.status, ClientException) return cls(body) class SSHException(Exception): """This is the basis for the SSH Exceptions.""" code = 500 message = "An unknown exception occurred." def __init__(self, message=None, **kwargs): self.kwargs = kwargs if 'code' not in self.kwargs: try: self.kwargs['code'] = self.code except AttributeError: pass if not message: try: message = self.message % kwargs except Exception: exc_info = sys.exc_info() # kwargs doesn't match a variable in the message # log the issue and the kwargs LOG.exception('Exception in string format operation') for name, value in kwargs.items(): LOG.error("%s: %s" % (name, value)) # at least get the core message out if something happened message = self.message self.msg = message super(SSHException, self).__init__(message) class SSHInjectionThreat(SSHException): message = "SSH command injection detected: %(command)s" class GrowVolumeException(SSHException): message = "SSH grow volume failed: %(command)s" class CopyVolumeException(SSHException): message = "SSH copy volume failed: %(command)s" class SetQOSRuleException(SSHException): message = "SSH set QOS rule failed: %(command)s" class ProcessExecutionError(Exception): def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None, description=None): self.exit_code = exit_code self.stderr = stderr self.stdout = stdout self.cmd = cmd self.description = description if description is None: description = "Unexpected error while running command." if exit_code is None: exit_code = '-' message = ("%s\nCommand: %s\nExit code: %s\nStdout: %r\nStderr: %r" % (description, cmd, exit_code, stdout, stderr)) super(ProcessExecutionError, self).__init__(message) hp3parclient-3.0.0/hp3parclient.egg-info/0000775000175000017500000000000012276222042020431 5ustar stackstack00000000000000hp3parclient-3.0.0/hp3parclient.egg-info/requires.txt0000664000175000017500000000002112276222042023022 0ustar stackstack00000000000000httplib2 >= 0.6.0hp3parclient-3.0.0/hp3parclient.egg-info/dependency_links.txt0000664000175000017500000000000112276222042024477 0ustar stackstack00000000000000 hp3parclient-3.0.0/hp3parclient.egg-info/SOURCES.txt0000664000175000017500000000107312276222042022316 0ustar stackstack00000000000000README.rst setup.py hp3parclient/__init__.py hp3parclient/client.py hp3parclient/exceptions.py hp3parclient/http.py hp3parclient/ssh.py hp3parclient.egg-info/PKG-INFO hp3parclient.egg-info/SOURCES.txt hp3parclient.egg-info/dependency_links.txt hp3parclient.egg-info/requires.txt hp3parclient.egg-info/top_level.txt test/test_HP3ParClient_CPG.py test/test_HP3ParClient_VLUN.py test/test_HP3ParClient_base.py test/test_HP3ParClient_host.py test/test_HP3ParClient_ports.py test/test_HP3ParClient_system.py test/test_HP3ParClient_volume.py test/test_HP3ParMockServer_flask.pyhp3parclient-3.0.0/hp3parclient.egg-info/PKG-INFO0000664000175000017500000000146512276222042021534 0ustar stackstack00000000000000Metadata-Version: 1.1 Name: hp3parclient Version: 3.0.0 Summary: HP 3PAR HTTP REST Client Home-page: http://packages.python.org/hp3parclient Author: Walter A. Boring IV Author-email: walter.boring@hp.com License: Apache License, Version 2.0 Description: UNKNOWN Keywords: hp,3par,rest Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Environment :: Web Environment Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.0 Classifier: Topic :: Internet :: WWW/HTTP Requires: httplib2(>=0.6.0) Requires: paramiko Requires: eventlet Provides: hp3parclient hp3parclient-3.0.0/hp3parclient.egg-info/top_level.txt0000664000175000017500000000001512276222042023157 0ustar stackstack00000000000000hp3parclient hp3parclient-3.0.0/README.rst0000664000175000017500000000225012276214354016040 0ustar stackstack00000000000000HP 3PAR REST Client =================== This is a Client library that can talk to the HP 3PAR Storage array. The 3PAR storage array has a REST web service interface. This client library implements a simple interface to talking with that REST interface using the python httplib2 http library. Requirements ============ This branch requires 3.1.3 version of the 3par firmware. Capabilities ============ * Create Volume * Delete Volume * Get all volumes * Get a volume * Create CPG * Delete CPG * Get all CPGs * Get a CPG * Create a VLUN * Delete a VLUN * Get all VLUNs * Get a VLUN * Create a Host * Delete a Host * Get all Hosts * Get a host * Get all Ports * Get iSCSI Ports * Get FC Ports * Get IP Ports Installation ============ :: $ python setup.py install Unit Tests ========== :: $ pip install nose $ pip install nose-testconfig $ cd test $ nosetests --tc-file config.ini Folders ======= * docs -- contains the documentation. * hp3parclient -- the actual client.py library * test -- unit tests * samples -- some sample uses Documentation ============= To view the built documentation point your browser to :: python-3parclient/docs/_build/html/index.html hp3parclient-3.0.0/PKG-INFO0000664000175000017500000000146512276222042015446 0ustar stackstack00000000000000Metadata-Version: 1.1 Name: hp3parclient Version: 3.0.0 Summary: HP 3PAR HTTP REST Client Home-page: http://packages.python.org/hp3parclient Author: Walter A. Boring IV Author-email: walter.boring@hp.com License: Apache License, Version 2.0 Description: UNKNOWN Keywords: hp,3par,rest Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Environment :: Web Environment Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.0 Classifier: Topic :: Internet :: WWW/HTTP Requires: httplib2(>=0.6.0) Requires: paramiko Requires: eventlet Provides: hp3parclient