diff --git a/cinder/tests/unit/volume/drivers/open_e/test_common.py b/cinder/tests/unit/volume/drivers/open_e/test_common.py new file mode 100644 index 00000000000..2d71c4a3805 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/open_e/test_common.py @@ -0,0 +1,305 @@ +# Copyright (c) 2023 Open-E, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy + +from cinder import exception +from cinder.tests.unit import test +from cinder.volume.drivers.open_e.jovian_common import jdss_common as jcom + +UUID_1 = '12345678-1234-1234-1234-000000000001' +UUID_2 = '12345678-1234-1234-1234-000000000002' +UUID_S1 = '12345678-1234-1234-1234-100000000001' +UUID_S2 = '12345678-1234-1234-1234-100000000002' + +V_UUID_1 = 'v_12345678-1234-1234-1234-000000000001' +V_UUID_2 = 'v_12345678-1234-1234-1234-000000000002' +V_UUID_3 = 'v_12345678-1234-1234-1234-000000000003' +S_UUID_1 = f's_{UUID_S1}_{UUID_1}' +S_UUID_2 = f's_{UUID_S2}_{UUID_1}' +T_UUID_1 = 't_12345678-1234-1234-1234-000000000001' + + +VOLUME_GET_THAT_IS_CLONE = { + "origin": f"Pool-0/{V_UUID_1}@{S_UUID_1}", + "relatime": None, + "acltype": None, + "vscan": None, + "full_name": f"Pool-0/{jcom.vname(UUID_2)}", + "userrefs": None, + "primarycache": "all", + "logbias": "latency", + "creation": "1695078560", + "sync": "always", + "is_clone": True, + "dedup": "off", + "sharenfs": None, + "receive_resume_token": None, + "volsize": "1073741824", + "referenced": "57344", + "sharesmb": None, + "createtxg": "19812058", + "reservation": "0", + "scontext": None, + "mountpoint": None, + "casesensitivity": None, + "guid": "4947994863040470005", + "usedbyrefreservation": "0", + "dnodesize": None, + "written": "0", + "logicalused": "0", + "compressratio": "1.00", + "rootcontext": "none", + "default_scsi_id": "5c02d042ed8dbce2", + "type": "volume", + "compression": "lz4", + "snapdir": None, + "overlay": None, + "encryption": "off", + "xattr": None, + "volmode": "default", + "copies": "1", + "snapshot_limit": "18446744073709551615", + "aclinherit": None, + "defcontext": "none", + "readonly": "off", + "version": None, + "recordsize": None, + "filesystem_limit": None, + "mounted": None, + "mlslabel": "none", + "secondarycache": "all", + "refreservation": "0", + "available": "954751713280", + "san:volume_id": "5c02d042ed8dbce2570c8d5dc276dd6a2431e138", + "encryptionroot": None, + "exec": None, + "refquota": None, + "refcompressratio": "1.00", + "quota": None, + "utf8only": None, + "keylocation": "none", + "snapdev": "hidden", + "snapshot_count": "18446744073709551615", + "fscontext": "none", + "clones": None, + "canmount": None, + "keystatus": None, + "atime": None, + "usedbysnapshots": "0", + "normalization": None, + "usedbychildren": "0", + "volblocksize": "65536", + "usedbydataset": "0", + "objsetid": "19228", + "name": "a2", + "defer_destroy": None, + "pbkdf2iters": "0", + "checksum": "on", + "redundant_metadata": "all", + "filesystem_count": None, + "devices": None, + "keyformat": "none", + "setuid": None, + "used": "0", + "logicalreferenced": "28672", + "context": "none", + "zoned": None, + "nbmand": None, +} + +SNAPSHOT_GET = { + 'referenced': '57344', + 'userrefs': '0', + 'primarycache': 'all', + 'creation': '2023-06-28 16:49:33', + 'volsize': '1073741824', + 'createtxg': '18402390', + 'guid': '15554334551928551694', + 'compressratio': '1.00', + 'rootcontext': 'none', + 'encryption': 'off', + 'defcontext': 'none', + 'written': '0', + 'type': 'snapshot', + 'secondarycache': 'all', + 'used': '0', + 'refcompressratio': '1.00', + 'fscontext': 'none', + 'objsetid': '106843', + 'name': S_UUID_1, + 'defer_destroy': 'off', + 'san:volume_id': 'e82c7fcbd78df0ffe67d363412e5091421d313ca', + 'mlslabel': 'none', + 'logicalreferenced': '28672', + 'context': 'none'} + +SNAPSHOT_MULTIPLE_CLONES = { + 'referenced': '57344', + 'userrefs': '0', + 'primarycache': 'all', + 'creation': '2023-06-28 18:44:49', + 'volsize': '1073741824', + 'createtxg': '18403768', + 'guid': '18319280142829358721', + 'compressratio': '1.00', + 'rootcontext': 'none', + 'encryption': 'off', + 'defcontext': 'none', + 'written': '0', + 'type': 'snapshot', + 'secondarycache': 'all', + 'used': '0', + 'refcompressratio': '1.00', + 'fscontext': 'none', + 'objsetid': '107416', + 'clones': f'Pool-0/{V_UUID_2},Pool-0/{V_UUID_3}', + 'name': S_UUID_1, + 'defer_destroy': 'off', + 'san:volume_id': 'e82c7fcbd78df0ffe67d363412e5091421d313ca', + 'mlslabel': 'none', + 'logicalreferenced': '28672', + 'context': 'none'} + +SNAPSHOTS_GET_NO_CLONES = [ + {'referenced': '57344', + 'userrefs': '0', + 'primarycache': 'all', + 'creation': '2023-06-28 16:49:33', + 'volsize': '1073741824', + 'createtxg': '18402390', + 'guid': '15554334551928551694', + 'compressratio': '1.00', + 'rootcontext': 'none', + 'encryption': 'off', + 'defcontext': 'none', + 'written': '0', + 'type': 'snapshot', + 'secondarycache': 'all', + 'used': '0', + 'refcompressratio': '1.00', + 'fscontext': 'none', + 'objsetid': '106843', + 'name': S_UUID_1, + 'defer_destroy': 'off', + 'san:volume_id': 'e82c7fcbd78df0ffe67d363412e5091421d313ca', + 'mlslabel': 'none', + 'logicalreferenced': '28672', + 'context': 'none'}, + {'referenced': '57344', + 'userrefs': '0', + 'primarycache': 'all', + 'creation': '2023-06-28 18:44:49', + 'volsize': '1073741824', + 'createtxg': '18403768', + 'guid': '18319280142829358721', + 'compressratio': '1.00', + 'rootcontext': 'none', + 'encryption': 'off', + 'defcontext': 'none', + 'written': '0', + 'type': 'snapshot', + 'secondarycache': 'all', + 'used': '0', + 'refcompressratio': '1.00', + 'fscontext': 'none', + 'objsetid': '107416', + 'name': S_UUID_2, + 'defer_destroy': 'off', + 'san:volume_id': 'e82c7fcbd78df0ffe67d363412e5091421d313ca', + 'mlslabel': 'none', + 'logicalreferenced': '28672', + 'context': 'none'}] + + +class TestOpenEJovianDSSCommon(test.TestCase): + + def test_is_volume(self): + + self.assertFalse(jcom.is_volume("asdasd")) + self.assertFalse(jcom.is_volume(UUID_1)) + self.assertTrue(jcom.is_volume(V_UUID_1)) + + def test_is_snapshot(self): + + self.assertFalse(jcom.is_snapshot("asdasd")) + self.assertFalse(jcom.is_snapshot(UUID_S1)) + self.assertTrue(jcom.is_snapshot(S_UUID_1)) + + def test_idname(self): + + self.assertEqual(UUID_1, jcom.idname(V_UUID_1)) + self.assertEqual(UUID_S1, jcom.idname(S_UUID_1)) + self.assertEqual(UUID_1, jcom.idname(T_UUID_1)) + + self.assertRaises(exception.VolumeDriverException, jcom.idname, 'asd') + + def test_vname(self): + + self.assertEqual(V_UUID_1, jcom.vname(UUID_1)) + self.assertEqual(V_UUID_1, jcom.vname(V_UUID_1)) + self.assertRaises(exception.VolumeDriverException, + jcom.vname, S_UUID_1) + + def test_sname_to_id(self): + + self.assertEqual((UUID_S1, UUID_1), jcom.sname_to_id(S_UUID_1)) + + def test_sid_from_sname(self): + + self.assertEqual(UUID_S1, jcom.sid_from_sname(S_UUID_1)) + + def test_vid_from_sname(self): + self.assertEqual(UUID_1, jcom.vid_from_sname(S_UUID_1)) + + def test_sname(self): + self.assertEqual(S_UUID_1, jcom.sname(UUID_S1, UUID_1)) + + def test_sname_from_snap(self): + + snap = copy.deepcopy(SNAPSHOT_GET) + self.assertEqual(S_UUID_1, jcom.sname_from_snap(snap)) + + def test_is_hidden(self): + + self.assertTrue(jcom.is_hidden(T_UUID_1)) + self.assertFalse(jcom.is_hidden(S_UUID_1)) + + def test_origin_snapshot(self): + + vol = copy.deepcopy(VOLUME_GET_THAT_IS_CLONE) + + self.assertEqual(S_UUID_1, jcom.origin_snapshot(vol)) + + def test_origin_volume(self): + + vol = copy.deepcopy(VOLUME_GET_THAT_IS_CLONE) + + self.assertEqual(V_UUID_1, jcom.origin_volume(vol)) + + def test_snapshot_clones(self): + + clones = [V_UUID_2, V_UUID_3] + snap = copy.deepcopy(SNAPSHOT_MULTIPLE_CLONES) + self.assertEqual(clones, jcom.snapshot_clones(snap)) + + def test_hidden(self): + + self.assertEqual(T_UUID_1, jcom.hidden(V_UUID_1)) + + def test_get_newest_snapshot_name(self): + snaps = copy.deepcopy(SNAPSHOTS_GET_NO_CLONES) + + self.assertEqual(S_UUID_2, jcom.get_newest_snapshot_name(snaps)) diff --git a/cinder/tests/unit/volume/drivers/open_e/test_driver.py b/cinder/tests/unit/volume/drivers/open_e/test_driver.py new file mode 100644 index 00000000000..4b8c2a8ea90 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/open_e/test_driver.py @@ -0,0 +1,1781 @@ +# Copyright (c) 2020 Open-E, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +from unittest import mock + +from oslo_utils import units as o_units + +from cinder import context +from cinder.tests.unit import fake_volume +from cinder.tests.unit import test +from cinder.volume.drivers.open_e.jovian_common import driver +from cinder.volume.drivers.open_e.jovian_common import exception as jexc +from cinder.volume.drivers.open_e.jovian_common import jdss_common as jcom + +UUID_1 = '12345678-1234-1234-1234-000000000001' +UUID_2 = '12345678-1234-1234-1234-000000000002' +UUID_3 = '12345678-1234-1234-1234-000000000003' +UUID_4 = '12345678-1234-1234-1234-000000000004' + +UUID_S1 = '12345678-1234-1234-1234-100000000001' +UUID_S2 = '12345678-1234-1234-1234-100000000002' +UUID_S3 = '12345678-1234-1234-1234-100000000003' +UUID_S4 = '12345678-1234-1234-1234-100000000004' + +CONFIG_OK = { + 'san_hosts': ['192.168.0.2'], + 'san_api_port': 82, + 'driver_use_ssl': 'false', + 'jovian_rest_send_repeats': 3, + 'jovian_recovery_delay': 60, + 'jovian_user': 'admin', + 'jovian_password': 'password', + 'jovian_ignore_tpath': [], + 'target_port': 3260, + 'jovian_pool': 'Pool-0', + 'target_prefix': 'iqn.2020-04.com.open-e.cinder:', + 'chap_password_len': 12, + 'san_thin_provision': False, + 'jovian_block_size': '128K' +} + +CONFIG_BLOCK_SIZE = { + 'san_hosts': ['192.168.0.2'], + 'san_api_port': 82, + 'driver_use_ssl': 'false', + 'jovian_rest_send_repeats': 3, + 'jovian_recovery_delay': 60, + 'jovian_user': 'admin', + 'jovian_password': 'password', + 'jovian_ignore_tpath': [], + 'target_port': 3260, + 'jovian_pool': 'Pool-0', + 'target_prefix': 'iqn.2020-04.com.open-e.cinder:', + 'chap_password_len': 12, + 'san_thin_provision': False, + 'jovian_block_size': '64K' +} + +CONFIG_BAD_BLOCK_SIZE = { + 'san_hosts': ['192.168.0.2'], + 'san_api_port': 82, + 'driver_use_ssl': 'false', + 'jovian_rest_send_repeats': 3, + 'jovian_recovery_delay': 60, + 'jovian_user': 'admin', + 'jovian_password': 'password', + 'jovian_ignore_tpath': [], + 'target_port': 3260, + 'jovian_pool': 'Pool-0', + 'target_prefix': 'iqn.2020-04.com.open-e.cinder:', + 'chap_password_len': 12, + 'san_thin_provision': False, + 'jovian_block_size': '61K' +} + +CONFIG_BACKEND_NAME = { + 'san_hosts': ['192.168.0.2'], + 'san_api_port': 82, + 'driver_use_ssl': 'true', + 'jovian_rest_send_repeats': 3, + 'jovian_recovery_delay': 60, + 'jovian_user': 'admin', + 'jovian_password': 'password', + 'jovian_ignore_tpath': [], + 'target_port': 3260, + 'jovian_pool': 'Pool-0', + 'target_prefix': 'iqn.2020-04.com.open-e.cinder:', + 'chap_password_len': 12, + 'san_thin_provision': False, + 'volume_backend_name': 'JovianDSS', + 'reserved_percentage': 10, + 'jovian_block_size': '128K' +} + +CONFIG_MULTI_HOST = { + 'san_hosts': ['192.168.0.2', '192.168.0.3'], + 'san_api_port': 82, + 'driver_use_ssl': 'true', + 'jovian_rest_send_repeats': 3, + 'jovian_recovery_delay': 60, + 'jovian_user': 'admin', + 'jovian_password': 'password', + 'jovian_ignore_tpath': [], + 'target_port': 3260, + 'jovian_pool': 'Pool-0', + 'target_prefix': 'iqn.2020-04.com.open-e.cinder:', + 'chap_password_len': 12, + 'san_thin_provision': False, + 'volume_backend_name': 'JovianDSS', + 'reserved_percentage': 10, + 'jovian_block_size': '128K' +} + +VOLUME_GET_NO_SNAPSHOTS = { + "origin": None, + "relatime": None, + "acltype": None, + "vscan": None, + "full_name": f"Pool-0/{jcom.vname(UUID_1)}", + "userrefs": None, + "primarycache": "all", + "logbias": "latency", + "creation": "1695048563", + "sync": "always", + "is_clone": False, + "dedup": "off", + "sharenfs": None, + "receive_resume_token": None, + "volsize": "1073741824", + "referenced": "57344", + "sharesmb": None, + "createtxg": "19806101", + "reservation": "0", + "scontext": None, + "mountpoint": None, + "casesensitivity": None, + "guid": "13628065397986503663", + "usedbyrefreservation": "1079975936", + "dnodesize": None, + "written": "57344", + "logicalused": "28672", + "compressratio": "1.00", + "rootcontext": "none", + "default_scsi_id": "9e697f6e11336500", + "type": "volume", + "compression": "lz4", + "snapdir": None, + "overlay": None, + "encryption": "off", + "xattr": None, + "volmode": "default", + "copies": "1", + "snapshot_limit": "18446744073709551615", + "aclinherit": None, + "defcontext": "none", + "readonly": "off", + "version": None, + "recordsize": None, + "filesystem_limit": None, + "mounted": None, + "mlslabel": "none", + "secondarycache": "all", + "refreservation": "1080033280", + "available": "955831783424", + "san:volume_id": "9e697f6e11336500480c13e4467b7964bed4b02e", + "encryptionroot": None, + "exec": None, + "refquota": None, + "refcompressratio": "1.00", + "quota": None, + "utf8only": None, + "keylocation": "none", + "snapdev": "hidden", + "snapshot_count": "18446744073709551615", + "fscontext": "none", + "clones": None, + "canmount": None, + "keystatus": None, + "atime": None, + "usedbysnapshots": "0", + "normalization": None, + "usedbychildren": "0", + "volblocksize": "65536", + "usedbydataset": "57344", + "objsetid": "18142", + "name": "a1", + "defer_destroy": None, + "pbkdf2iters": "0", + "checksum": "on", + "redundant_metadata": "all", + "filesystem_count": None, + "devices": None, + "keyformat": "none", + "setuid": None, + "used": "1080033280", + "logicalreferenced": "28672", + "context": "none", + "zoned": None, + "nbmand": None, +} + +VOLUME_GET_THAT_IS_CLONE = { + "origin": f"Pool-0/{jcom.vname(UUID_1)}@{jcom.sname(UUID_S1, UUID_1)}", + "relatime": None, + "acltype": None, + "vscan": None, + "full_name": f"Pool-0/{jcom.vname(UUID_2)}", + "userrefs": None, + "primarycache": "all", + "logbias": "latency", + "creation": "1695078560", + "sync": "always", + "is_clone": True, + "dedup": "off", + "sharenfs": None, + "receive_resume_token": None, + "volsize": "1073741824", + "referenced": "57344", + "sharesmb": None, + "createtxg": "19812058", + "reservation": "0", + "scontext": None, + "mountpoint": None, + "casesensitivity": None, + "guid": "4947994863040470005", + "usedbyrefreservation": "0", + "dnodesize": None, + "written": "0", + "logicalused": "0", + "compressratio": "1.00", + "rootcontext": "none", + "default_scsi_id": "5c02d042ed8dbce2", + "type": "volume", + "compression": "lz4", + "snapdir": None, + "overlay": None, + "encryption": "off", + "xattr": None, + "volmode": "default", + "copies": "1", + "snapshot_limit": "18446744073709551615", + "aclinherit": None, + "defcontext": "none", + "readonly": "off", + "version": None, + "recordsize": None, + "filesystem_limit": None, + "mounted": None, + "mlslabel": "none", + "secondarycache": "all", + "refreservation": "0", + "available": "954751713280", + "san:volume_id": "5c02d042ed8dbce2570c8d5dc276dd6a2431e138", + "encryptionroot": None, + "exec": None, + "refquota": None, + "refcompressratio": "1.00", + "quota": None, + "utf8only": None, + "keylocation": "none", + "snapdev": "hidden", + "snapshot_count": "18446744073709551615", + "fscontext": "none", + "clones": None, + "canmount": None, + "keystatus": None, + "atime": None, + "usedbysnapshots": "0", + "normalization": None, + "usedbychildren": "0", + "volblocksize": "65536", + "usedbydataset": "0", + "objsetid": "19228", + "name": "a2", + "defer_destroy": None, + "pbkdf2iters": "0", + "checksum": "on", + "redundant_metadata": "all", + "filesystem_count": None, + "devices": None, + "keyformat": "none", + "setuid": None, + "used": "0", + "logicalreferenced": "28672", + "context": "none", + "zoned": None, + "nbmand": None, +} + +SNAPSHOTS_GET_NO_CLONES = [ + {'referenced': '57344', + 'userrefs': '0', + 'primarycache': 'all', + 'creation': '2023-06-28 16:49:33', + 'volsize': '1073741824', + 'createtxg': '18402390', + 'guid': '15554334551928551694', + 'compressratio': '1.00', + 'rootcontext': 'none', + 'encryption': 'off', + 'defcontext': 'none', + 'written': '0', + 'type': 'snapshot', + 'secondarycache': 'all', + 'used': '0', + 'refcompressratio': '1.00', + 'fscontext': 'none', + 'objsetid': '106843', + 'name': jcom.sname(UUID_S1, UUID_1), + 'defer_destroy': 'off', + 'san:volume_id': 'e82c7fcbd78df0ffe67d363412e5091421d313ca', + 'mlslabel': 'none', + 'logicalreferenced': '28672', + 'context': 'none'}, + {'referenced': '57344', + 'userrefs': '0', + 'primarycache': 'all', + 'creation': '2023-06-28 18:44:49', + 'volsize': '1073741824', + 'createtxg': '18403768', + 'guid': '18319280142829358721', + 'compressratio': '1.00', + 'rootcontext': 'none', + 'encryption': 'off', + 'defcontext': 'none', + 'written': '0', + 'type': 'snapshot', + 'secondarycache': 'all', + 'used': '0', + 'refcompressratio': '1.00', + 'fscontext': 'none', + 'objsetid': '107416', + 'name': jcom.sname(UUID_S2, UUID_1), + 'defer_destroy': 'off', + 'san:volume_id': 'e82c7fcbd78df0ffe67d363412e5091421d313ca', + 'mlslabel': 'none', + 'logicalreferenced': '28672', + 'context': 'none'}] + +SNAPSHOTS_GET_INTERMEDIATE_SNAP = [ + {'referenced': '57344', + 'userrefs': '0', + 'primarycache': 'all', + 'creation': '2023-06-28 16:49:33', + 'volsize': '1073741824', + 'createtxg': '18402390', + 'guid': '15554334551928551694', + 'compressratio': '1.00', + 'rootcontext': 'none', + 'encryption': 'off', + 'defcontext': 'none', + 'written': '0', + 'type': 'snapshot', + 'secondarycache': 'all', + 'used': '0', + 'refcompressratio': '1.00', + 'fscontext': 'none', + 'objsetid': '106843', + 'name': jcom.vname(UUID_S1), + 'defer_destroy': 'off', + 'san:volume_id': 'e82c7fcbd78df0ffe67d363412e5091421d313ca', + 'mlslabel': 'none', + 'logicalreferenced': '28672', + 'context': 'none'}] + +SNAPSHOTS_GET_ONE_CLONE = [ + {'referenced': '57344', + 'userrefs': '0', + 'primarycache': 'all', + 'creation': '2023-06-28 16:49:33', + 'volsize': '1073741824', + 'createtxg': '18402390', + 'guid': '15554334551928551694', + 'compressratio': '1.00', + 'rootcontext': 'none', + 'encryption': 'off', + 'defcontext': 'none', + 'written': '0', + 'type': 'snapshot', + 'secondarycache': 'all', + 'used': '0', + 'refcompressratio': '1.00', + 'fscontext': 'none', + 'objsetid': '106843', + 'name': jcom.sname(UUID_S1, UUID_1), + 'defer_destroy': 'off', + 'san:volume_id': 'e82c7fcbd78df0ffe67d363412e5091421d313ca', + 'mlslabel': 'none', + 'logicalreferenced': '28672', + 'context': 'none'}, + {'referenced': '57344', + 'userrefs': '0', + 'primarycache': 'all', + 'creation': '2023-06-28 18:44:49', + 'volsize': '1073741824', + 'createtxg': '18403768', + 'guid': '18319280142829358721', + 'compressratio': '1.00', + 'rootcontext': 'none', + 'encryption': 'off', + 'defcontext': 'none', + 'written': '0', + 'type': 'snapshot', + 'secondarycache': 'all', + 'used': '0', + 'refcompressratio': '1.00', + 'fscontext': 'none', + 'objsetid': '107416', + 'clones': 'Pool-0/' + jcom.vname(UUID_2), + 'name': jcom.sname(UUID_S2, UUID_1), + 'defer_destroy': 'off', + 'san:volume_id': 'e82c7fcbd78df0ffe67d363412e5091421d313ca', + 'mlslabel': 'none', + 'logicalreferenced': '28672', + 'context': 'none'}] + +SNAPSHOTS_GET_MULTIPLE_CLONES = [ + {'referenced': '57344', + 'userrefs': '0', + 'primarycache': 'all', + 'creation': '2023-06-28 16:49:33', + 'volsize': '1073741824', + 'createtxg': '18402390', + 'guid': '15554334551928551694', + 'compressratio': '1.00', + 'rootcontext': 'none', + 'encryption': 'off', + 'defcontext': 'none', + 'written': '0', + 'type': 'snapshot', + 'secondarycache': 'all', + 'used': '0', + 'refcompressratio': '1.00', + 'fscontext': 'none', + 'objsetid': '106843', + 'name': jcom.sname(UUID_S1, UUID_1), + 'defer_destroy': 'off', + 'san:volume_id': 'e82c7fcbd78df0ffe67d363412e5091421d313ca', + 'mlslabel': 'none', + 'logicalreferenced': '28672', + 'context': 'none'}, + {'referenced': '57344', + 'userrefs': '0', + 'primarycache': 'all', + 'creation': '2023-06-28 18:44:49', + 'volsize': '1073741824', + 'createtxg': '18403768', + 'guid': '18319280142829358721', + 'compressratio': '1.00', + 'rootcontext': 'none', + 'encryption': 'off', + 'defcontext': 'none', + 'written': '0', + 'type': 'snapshot', + 'secondarycache': 'all', + 'used': '0', + 'refcompressratio': '1.00', + 'fscontext': 'none', + 'objsetid': '107416', + 'clones': f'Pool-0/{jcom.vname(UUID_2)},Pool-0/{jcom.vname(UUID_3)}', + 'name': jcom.sname(UUID_S2, UUID_1), + 'defer_destroy': 'off', + 'san:volume_id': 'e82c7fcbd78df0ffe67d363412e5091421d313ca', + 'mlslabel': 'none', + 'logicalreferenced': '28672', + 'context': 'none'}] + +SNAPSHOT_GET_ONE_CLONE = { + "referenced": "57344", + "userrefs": "0", + "primarycache": "all", + "creation": "2023-09-19 01:08:25", + "volsize": "1073741824", + "createtxg": "19812047", + "guid": "7433980076067517643", + "compressratio": "1.00", + "rootcontext": "none", + "encryption": "off", + "defcontext": "none", + "written": "57344", + "type": "snapshot", + "secondarycache": "all", + "used": "0", + "refcompressratio": "1.00", + "fscontext": "none", + "clones": f"Pool-0/{jcom.vname(UUID_2)}", + "objsetid": "19220", + "defer_destroy": "off", + "san:volume_id": "9e697f6e11336500480c13e4467b7964bed4b02e", + "mlslabel": "none", + "logicalreferenced": "28672", + "context": "none" +} + +SNAPSHOTS_CASCADE_1 = [ + {"name": jcom.sname(UUID_S1, UUID_1), + "clones": "Pool-0/" + jcom.sname(UUID_S1, UUID_1)}, + {"name": jcom.sname(UUID_S1, UUID_2), + "clones": "Pool-0/" + jcom.sname(UUID_S1, UUID_2)}, + {"name": jcom.sname(UUID_S1, UUID_3), + "clones": "Pool-0/" + jcom.sname(UUID_S1, UUID_3)}] + +SNAPSHOTS_CASCADE_2 = [ + {"name": jcom.sname(UUID_S1, UUID_1), + "clones": "Pool-0/" + jcom.sname(UUID_S1, UUID_1)}, + {"name": jcom.vname(UUID_2), + "clones": "Pool-0/" + jcom.vname(UUID_2)}, + {"name": jcom.sname(UUID_S1, UUID_3), + "clones": "Pool-0/" + jcom.sname(UUID_S1, UUID_3)}] + +SNAPSHOTS_CASCADE_3 = [ + {"name": jcom.vname(UUID_4), + "clones": "Pool-0/" + jcom.vname(UUID_4)}] + +SNAPSHOTS_EMPTY = [] + +SNAPSHOTS_CLONE = [ + {"name": jcom.vname(UUID_1), + "clones": "Pool-0/" + jcom.vname(UUID_1)}] + +SNAPSHOTS_GARBAGE = [ + {"name": jcom.sname(UUID_S1, UUID_1), + "clones": "Pool-0/" + jcom.vname(UUID_2)}, + {"name": jcom.sname(UUID_S1, UUID_2), + "clones": ""}] + +SNAPSHOTS_RECURSIVE_1 = [ + {"name": jcom.sname(UUID_S1, UUID_1), + "clones": "Pool-0/" + jcom.sname(UUID_S1, UUID_1)}, + {"name": jcom.sname(UUID_S1, UUID_2), + "clones": "Pool-0/" + jcom.hidden(UUID_2)}] + +SNAPSHOTS_RECURSIVE_CHAIN_1 = [ + {"name": jcom.sname(UUID_S1, UUID_3), + "clones": "Pool-0/" + jcom.hidden(UUID_3)}] + +SNAPSHOTS_RECURSIVE_CHAIN_2 = [ + {"name": jcom.vname(UUID_2), + "clones": "Pool-0/" + jcom.hidden(UUID_2)}] + + +def get_jdss_exceptions(): + + out = [jexc.JDSSException(reason="Testing"), + jexc.JDSSRESTException(request="ra request", reason="Testing"), + jexc.JDSSRESTProxyException(host="test_host", reason="Testing"), + jexc.JDSSResourceNotFoundException(res="test_resource"), + jexc.JDSSVolumeNotFoundException(volume="test_volume"), + jexc.JDSSSnapshotNotFoundException(snapshot="test_snapshot"), + jexc.JDSSResourceExistsException(res="test_resource"), + jexc.JDSSSnapshotExistsException(snapshot="test_snapshot"), + jexc.JDSSVolumeExistsException(volume="test_volume"), + jexc.JDSSSnapshotIsBusyException(snapshot="test_snapshot")] + + return out + + +class TestOpenEJovianDSSDriver(test.TestCase): + + def get_jdss_driver(self, config): + ctx = context.get_admin_context() + + cfg = mock.Mock() + cfg.append_config_values.return_value = None + cfg.get = lambda val, default: config.get(val, default) + + jdssd = driver.JovianDSSDriver(cfg) + + lib_to_patch = ('cinder.volume.drivers.open_e.jovian_common.driver.' + 'rest.JovianRESTAPI') + with mock.patch(lib_to_patch) as ra: + ra.is_pool_exists.return_value = True + jdssd.ra = mock.Mock() + return jdssd, ctx + + def start_patches(self, patches): + for p in patches: + p.start() + + def stop_patches(self, patches): + for p in patches: + p.stop() + + def test_create_volume(self): + + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + vol = fake_volume.fake_volume_obj(ctx) + + vol.id = UUID_1 + vol.size = 1 + + jdssd.ra.create_lun.return_value = None + + jdssd.create_volume(vol.id, 1) + + create_vol_expected = [mock.call(jcom.vname(vol.id), + 1073741824, + sparse=False, + block_size=None)] + + jdssd.create_volume(vol.id, 1, sparse=True) + + create_vol_expected += [mock.call(jcom.vname(vol.id), + 1073741824, + sparse=True, + block_size=None)] + + jdssd.create_volume(vol.id, 1, sparse=True, block_size="64K") + + create_vol_expected += [mock.call(jcom.vname(vol.id), + 1073741824, + sparse=True, + block_size="64K")] + + jdssd.ra.create_lun.assert_has_calls(create_vol_expected) + + def test_promote_newest_delete_no_snapshots(self): + + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + vname = jcom.vname(UUID_1) + + # test provide empty snapshot list + snapshots = [] + jdssd.ra.get_snapshots.return_value = [] + resp = {'data': {"vscan": None, + "full_name": "Pool-0/v_" + UUID_1, + "userrefs": None, + "primarycache": "all", + "logbias": "latency", + "creation": "1591543140", + "sync": "always", + "is_clone": False, + "dedup": "off", + "sharenfs": None, + "receive_resume_token": None, + "volsize": "1073741824"}, + 'error': None, + 'code': 200} + jdssd.ra.get_lun.return_value = resp + jdssd.ra.delete_lun.return_value = None + jdssd._promote_newest_delete(vname, snapshots) + delete_vol_expected = [mock.call(vname, + force_umount=True, + recursively_children=True)] + jdssd.ra.delete_lun.assert_has_calls(delete_vol_expected) + + # test provide none as snapshot list + snapshots = None + jdssd.ra.get_snapshots.return_value = [] + resp = {'data': {"vscan": None, + "full_name": "Pool-0/v_" + UUID_1, + "userrefs": None, + "primarycache": "all", + "logbias": "latency", + "creation": "1591543140", + "sync": "always", + "is_clone": False, + "dedup": "off", + "sharenfs": None, + "receive_resume_token": None, + "volsize": "1073741824"}, + 'error': None, + 'code': 200} + jdssd.ra.get_lun.return_value = resp + jdssd.ra.delete_lun.return_value = None + jdssd._promote_newest_delete(vname, snapshots) + delete_vol_expected = [mock.call(vname, + force_umount=True, + recursively_children=True)] + jdssd.ra.delete_lun.assert_has_calls(delete_vol_expected) + + def test_promote_newest_delete_has_snapshots(self): + '''Test promote-remove on volume with snapshots + + We should sucessevely remove volume if it have snapshots + with no clones. + Also no promote should be called. + ''' + + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + vname = jcom.vname(UUID_1) + + snapshots = copy.deepcopy(SNAPSHOTS_GET_NO_CLONES) + jdssd.ra.get_snapshots.return_value = [] + resp = {"vscan": None, + "full_name": "Pool-0/v_" + UUID_1, + "userrefs": None, + "primarycache": "all", + "logbias": "latency", + "creation": "1591543140", + "sync": "always", + "is_clone": False, + "dedup": "off", + "sharenfs": None, + "receive_resume_token": None, + "volsize": "1073741824"}, + jdssd.ra.get_lun.return_value = resp + jdssd.ra.delete_lun.return_value = None + jdssd._promote_newest_delete(vname, snapshots) + delete_vol_expected = [mock.call(vname, + force_umount=True, + recursively_children=True)] + jdssd.ra.promote.assert_not_called() + jdssd.ra.delete_lun.assert_has_calls(delete_vol_expected) + + def test_promote_newest_delete_has_clone(self): + '''Test promote-remove on volume with clone + + We should sucessevely remove volume if it have snapshot + with no clone. + ''' + + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + vname = jcom.vname(UUID_1) + + snapshots = copy.deepcopy(SNAPSHOTS_GET_ONE_CLONE) + + self.assertEqual(f'Pool-0/{jcom.vname(UUID_2)}', + snapshots[1]['clones']) + jdssd.ra.get_snapshots.return_value = [] + resp = {'data': {"vscan": None, + "full_name": "Pool-0/v_" + UUID_1, + "userrefs": None, + "primarycache": "all", + "logbias": "latency", + "creation": "1591543140", + "sync": "always", + "is_clone": False, + "dedup": "off", + "sharenfs": None, + "receive_resume_token": None, + "volsize": "1073741824"}, + 'error': None, + 'code': 200} + jdssd.ra.get_lun.return_value = resp + jdssd.ra.delete_lun.return_value = None + + jdssd._promote_newest_delete(vname, snapshots) + delete_vol_expected = [mock.call(vname, + force_umount=True, + recursively_children=True)] + promote_vol_expected = [mock.call(vname, + jcom.sname(UUID_S2, UUID_1), + jcom.vname(UUID_2))] + jdssd.ra.promote.assert_has_calls(promote_vol_expected) + jdssd.ra.delete_lun.assert_has_calls(delete_vol_expected) + + def test_promote_newest_delete_has_multiple_clones(self): + '''Test promote-remove on volume with clone + + We should sucessevely remove volume if it have snapshot + with no clone. + ''' + + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + vname = jcom.vname(UUID_1) + + snapshots = copy.deepcopy(SNAPSHOTS_GET_MULTIPLE_CLONES) + jdssd.ra.get_snapshots.return_value = [] + resp = {'data': {"vscan": None, + "full_name": "Pool-0/s_" + UUID_S2, + "userrefs": None, + "primarycache": "all", + "logbias": "latency", + "creation": "1591543140", + "sync": "always", + "is_clone": False, + "dedup": "off", + "sharenfs": None, + "receive_resume_token": None, + "volsize": "1073741824"}, + 'error': None, + 'code': 200} + jdssd.ra.get_lun.return_value = resp + jdssd.ra.delete_lun.return_value = None + jdssd._promote_newest_delete(vname, snapshots) + delete_vol_expected = [mock.call(vname, + force_umount=True, + recursively_children=True)] + promote_vol_expected = [mock.call(vname, + jcom.sname(UUID_S2, UUID_1), + jcom.vname(UUID_3))] + jdssd.ra.promote.assert_has_calls(promote_vol_expected) + jdssd.ra.delete_lun.assert_has_calls(delete_vol_expected) + + def test_delete_vol_with_source_snap_no_snap(self): + '''Test _delete_vol_with_source_snap + + We should sucessevely remove volume with no snapshots. + ''' + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + vname = jcom.vname(UUID_1) + jdssd.ra.get_lun.return_value = copy.deepcopy(VOLUME_GET_NO_SNAPSHOTS) + jdssd.ra.delete_lun.return_value = None + + delete_vol_expected = [mock.call(vname, + force_umount=True, + recursively_children=False)] + + jdssd._delete_vol_with_source_snap(vname, recursive=False) + + jdssd.ra.delete_lun.assert_has_calls(delete_vol_expected) + jdssd.ra.delete_snapshot.assert_not_called() + + def test_delete_vol_with_source_snap(self): + '''Test _delete_vol_with_source_snap + + We should sucessevely remove volume that is clone. + We should not remove source snapshot + if that snapshot is not related to volume to remove + ''' + # Snapshot does belong to parent volume + + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + vname = jcom.vname(UUID_2) + jdssd.ra.get_lun.return_value = \ + copy.deepcopy(VOLUME_GET_THAT_IS_CLONE) + jdssd.ra.delete_lun.return_value = None + + delete_vol_expected = [mock.call(vname, + force_umount=True, + recursively_children=False)] + + jdssd._delete_vol_with_source_snap(vname, recursive=False) + + jdssd.ra.delete_lun.assert_has_calls(delete_vol_expected) + jdssd.ra.delete_snapshot.assert_not_called() + + def test_delete_vol_with_source_snap_snap_delete(self): + # Snapshot belongs to volume to delete + + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + vname = jcom.vname(UUID_2) + lun_info = copy.deepcopy(VOLUME_GET_THAT_IS_CLONE) + origin = f"Pool-0/{jcom.vname(UUID_1)}@{jcom.sname(UUID_S2, UUID_2)}" + lun_info['origin'] = origin + + jdssd.ra.get_lun.return_value = lun_info + + jdssd.ra.delete_lun.return_value = None + + delete_vol_expected = [mock.call(vname, + force_umount=True, + recursively_children=False)] + delete_snapshot_expected = [mock.call(jcom.vname(UUID_1), + jcom.sname(UUID_S2, UUID_2), + recursively_children=True, + force_umount=True)] + jdssd._delete_vol_with_source_snap(vname, recursive=False) + + jdssd.ra.delete_lun.assert_has_calls(delete_vol_expected) + jdssd.ra.delete_snapshot.assert_has_calls(delete_snapshot_expected) + + def test_clean_garbage_resources(self): + + # Make sure that we request list of snapshots if none is provide + # Make sure we remove intermediate volume like snapshot if it has + # no volumes associated with it + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + vname = jcom.vname(UUID_1) + snap_list = copy.deepcopy(SNAPSHOTS_GET_INTERMEDIATE_SNAP) + get_snapshots_expectes = [mock.call(jcom.vname(UUID_1))] + delete_snapshot_expected = [mock.call(jcom.vname(UUID_1), + jcom.vname(UUID_S1), + force_umount=True)] + + jdssd.ra.get_snapshots.side_effect = [snap_list, []] + + ret = jdssd._clean_garbage_resources(vname, snapshots=None) + + self.assertEqual([], ret) + jdssd.ra.get_snapshots.assert_has_calls(get_snapshots_expectes) + jdssd.ra.delete_snapshot.assert_has_calls(delete_snapshot_expected) + + def test_clean_garbage_resources_do_nothing(self): + + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + vname = jcom.vname(UUID_1) + snap_list = SNAPSHOTS_GET_ONE_CLONE.copy() + get_snapshots_expectes = [mock.call(jcom.vname(UUID_1))] + + jdssd.ra.get_snapshots.side_effect = [snap_list, snap_list] + + ret = jdssd._clean_garbage_resources(vname, snapshots=None) + + self.assertEqual(SNAPSHOTS_GET_ONE_CLONE, ret) + jdssd.ra.get_snapshots.assert_has_calls(get_snapshots_expectes) + jdssd.ra.delete_snapshot.assert_not_called() + + def test_clean_garbage_resources_clean_hidden(self): + + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + vname = jcom.vname(UUID_1) + snap_list = copy.deepcopy(SNAPSHOTS_GET_ONE_CLONE) + snap_list[1]['clones'] = f"Pool-0/{jcom.hidden(UUID_2)}" + snap_list[1]['name'] = jcom.sname(UUID_S2, UUID_1) + + get_snapshots_expectes = [mock.call(jcom.vname(UUID_1)), + mock.call(jcom.vname(UUID_1))] + + jdssd.ra.get_snapshots.side_effect = [snap_list, + SNAPSHOTS_GET_NO_CLONES] + with mock.patch.object(jdssd, '_promote_newest_delete') as pnd: + pnd.side_effect = [None] + ret = jdssd._clean_garbage_resources(vname, snapshots=None) + self.assertEqual(SNAPSHOTS_GET_NO_CLONES, ret) + pnd.assert_has_calls([mock.call(jcom.hidden(UUID_2))]) + + jdssd.ra.get_snapshots.assert_has_calls(get_snapshots_expectes) + jdssd.ra.delete_snapshot.assert_not_called() + + def test_clean_garbage_resources_clean_snapshot(self): + + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + vname = jcom.vname(UUID_1) + snap_list = copy.deepcopy(SNAPSHOTS_GET_ONE_CLONE) + snap_list[1]['clones'] = f"Pool-0/{jcom.sname(UUID_S2, UUID_1)}" + snap_list[1]['name'] = jcom.sname(UUID_S2, UUID_1) + + get_snapshots_expectes = [mock.call(jcom.vname(UUID_1))] + + jdssd.ra.get_snapshots.side_effect = [snap_list] + with mock.patch.object(jdssd, '_promote_newest_delete') as pnd: + pnd.side_effect = [None] + + ret = jdssd._clean_garbage_resources(vname, snapshots=None) + pnd.assert_not_called() + self.assertEqual(snap_list, ret) + + jdssd.ra.get_snapshots.assert_has_calls(get_snapshots_expectes) + jdssd.ra.delete_snapshot.assert_not_called() + + def test_list_busy_snapshots(self): + + # Check operation with regular clone + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + vname = jcom.vname(UUID_1) + + snap_list = copy.deepcopy(SNAPSHOTS_GET_ONE_CLONE) + + ret = jdssd._list_busy_snapshots(vname, snap_list) + self.assertEqual([SNAPSHOTS_GET_ONE_CLONE[1]], ret) + + # Check hidden clone + snap_list = copy.deepcopy(SNAPSHOTS_GET_ONE_CLONE) + + snap_list[0]['clones'] = f"Pool-0/{jcom.hidden(UUID_2)}" + snap_list[0]['name'] = jcom.sname(UUID_S2, UUID_1) + + ret = jdssd._list_busy_snapshots(vname, snap_list) + self.assertEqual(snap_list, ret) + + # Check exlude dedicated volume flag + snap_list = copy.deepcopy(SNAPSHOTS_GET_ONE_CLONE) + + ret = jdssd._list_busy_snapshots(vname, snap_list, + exclude_dedicated_volumes=True) + self.assertEqual([], ret) + + def _clean_volume_snapshots_mount_points(self): + + # Single attached snapshot case + vname = jcom.vname(UUID_1) + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + snap_list = copy.deepcopy(SNAPSHOTS_GET_ONE_CLONE) + cname = jcom.sname(UUID_S2, UUID_1) + snap_list[1]['clones'] = cname = f'Pool-0/{cname}' + + ret_list = copy.deepcopy(SNAPSHOTS_GET_ONE_CLONE) + ret_list[1].pop('clones') + + jdssd.ra.get_snapshots.return_value = ret_list + with mock.patch.object(jdssd, '_delete_volume'): + ret = jdssd._clean_volume_snapshots_mount_points(vname, snap_list) + jdssd._delete_volume.assert_called_once_with(cname, cascade=True) + + jdssd.ra.get_snapshots.assert_called_once_with(vname) + self.assertEqual(ret_list, ret) + + # Multiple attached snapshot case + vname = jcom.vname(UUID_1) + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + snap_list = copy.deepcopy(SNAPSHOTS_GET_ONE_CLONE) + cname0 = jcom.sname(UUID_S1, UUID_1) + cname1 = jcom.sname(UUID_S2, UUID_1) + snap_list[0]['clones'] = cname = f'Pool-0/{cname0}' + snap_list[1]['clones'] = cname = f'Pool-0/{cname1}' + + ret_list = copy.deepcopy(SNAPSHOTS_GET_ONE_CLONE) + ret_list[0].pop('clones') + ret_list[1].pop('clones') + + jdssd.ra.get_snapshots.return_value = ret_list + + del_vol_expected = [mock.call(jcom.vname(cname0), cascade=True), + mock.call(jcom.vname(cname1), cascade=True)] + + with mock.patch.object(jdssd, '_delete_volume'): + ret = jdssd._clean_volume_snapshots_mount_points(vname, snap_list) + jdssd._delete_volume.assert_has_calls(del_vol_expected) + + jdssd.ra.get_snapshots.assert_called_once_with(vname) + self.assertEqual(ret_list, ret) + + def test_delete_volume_no_snap(self): + + vname = jcom.vname(UUID_1) + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + jdssd.ra.delete_lun.return_value = None + + del_lun_exp = [mock.call( + jcom.vname(UUID_1), + force_umount=True, + recursively_children=False)] + + jdssd._delete_volume(vname) + + jdssd.ra.delete_lun.assert_has_calls(del_lun_exp) + jdssd.ra.get_snapshots.assert_not_called() + + def test_delete_volume_cascade_with_clones(self): + + vname = jcom.vname(UUID_1) + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + jdssd.ra.delete_lun.side_effect = [ + jexc.JDSSResourceIsBusyException(res=vname)] + + del_lun_exp = [mock.call(jcom.vname(UUID_1), + force_umount=True, + recursively_children=True)] + + snap_list = copy.deepcopy(SNAPSHOTS_GET_ONE_CLONE) + + get_snap_exp = [mock.call(jcom.vname(UUID_1))] + + jdssd.ra.get_snapshots.side_effect = [snap_list] + + pnd_exp = [mock.call(jcom.vname(UUID_1), snapshots=snap_list)] + with mock.patch.object(jdssd, '_promote_newest_delete'): + jdssd._delete_volume(vname, cascade=True) + jdssd._promote_newest_delete.assert_has_calls(pnd_exp) + + jdssd.ra.delete_lun.assert_has_calls(del_lun_exp) + jdssd.ra.get_snapshots.assert_has_calls(get_snap_exp) + + def test_delete_volume(self): + + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + vname = jcom.vname(UUID_1) + + with mock.patch.object(jdssd, '_delete_volume'): + jdssd.delete_volume(UUID_1) + jdssd._delete_volume.assert_called_once_with(vname, cascade=False) + + def test_get_provider_location(self): + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + host = CONFIG_OK["san_hosts"][0] + port = CONFIG_OK["target_port"] + target_name = CONFIG_OK["target_prefix"] + UUID_1 + patches = [mock.patch.object( + jdssd.ra, + "get_active_host", + return_value=host)] + out = '{host}:{port},1 {name} 0'.format( + host=host, + port=port, + name=target_name + ) + self.start_patches(patches) + self.assertEqual(out, jdssd.get_provider_location(UUID_1)) + self.stop_patches(patches) + + def test_get_target_name(self): + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + ret = jdssd._get_target_name(UUID_1) + self.assertEqual(ret, f'iqn.2020-04.com.open-e.cinder:{UUID_1}') + + def test_get_iscsi_properties(self): + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + provider_auth = 'chap user_name 123456789012' + + multipath = True + + target_name = CONFIG_OK['target_prefix'] + UUID_1 + ret = jdssd._get_iscsi_properties(UUID_1, + provider_auth, + multipath=multipath) + expected = {'auth_method': 'chap', + 'auth_password': '123456789012', + 'auth_username': 'user_name', + 'target_discovered': False, + 'target_iqns': [target_name], + 'target_lun': 0, + 'target_luns': [0], + 'target_portals': ['192.168.0.2:3260']} + self.assertEqual(expected, ret) + + def test_get_iscsi_properties_multipath(self): + jdssd, ctx = self.get_jdss_driver(CONFIG_MULTI_HOST) + + provider_auth = 'chap user_name 123456789012' + + target_name = CONFIG_OK['target_prefix'] + UUID_1 + ret = jdssd._get_iscsi_properties(UUID_1, + provider_auth, + multipath=True) + expected = {'auth_method': 'chap', + 'auth_password': '123456789012', + 'auth_username': 'user_name', + 'target_discovered': False, + 'target_iqns': [target_name, target_name], + 'target_lun': 0, + 'target_luns': [0, 0], + 'target_portals': ['192.168.0.2:3260', '192.168.0.3:3260']} + self.assertEqual(expected, ret) + + def test_remove_target_volume(self): + + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + target_name = CONFIG_OK['target_prefix'] + UUID_1 + + jdssd.ra.detach_target_vol.return_value = None + jdssd.ra.delete_target.return_value = None + + jdssd._remove_target_volume(UUID_1, jcom.vname(UUID_1)) + + jdssd.ra.detach_target_vol.assert_called_once_with(target_name, + jcom.vname(UUID_1)) + jdssd.ra.delete_target.assert_called_with(target_name) + + def test_remove_target_volume_no_target(self): + + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + target_name = CONFIG_OK['target_prefix'] + UUID_1 + vname = jcom.vname(UUID_1) + + jdssd.ra.detach_target_vol.return_value = None + jdssd.ra.detach_target_vol.side_effect = ( + jexc.JDSSResourceNotFoundException(res=target_name)) + jdssd.ra.delete_target.return_value = None + + jdssd._remove_target_volume(UUID_1, vname) + + jdssd.ra.detach_target_vol.assert_called_once_with(target_name, + jcom.vname(UUID_1)) + jdssd.ra.delete_target.assert_called_with(target_name) + + def test_remove_target_volume_fail_to_detach(self): + + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + target_name = CONFIG_OK['target_prefix'] + UUID_1 + + jdssd.ra.detach_target_vol.side_effect = ( + jexc.JDSSRESTException(reason='running test', request='test')) + jdssd.ra.delete_target.return_value = None + + self.assertRaises(jexc.JDSSException, + jdssd._remove_target_volume, + UUID_1, + jcom.vname(UUID_1)) + + jdssd.ra.detach_target_vol.assert_called_once_with( + target_name, jcom.vname(UUID_1)) + jdssd.ra.delete_target.assert_not_called() + + def test_remove_target_volume_fail_to_delete(self): + + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + target_name = CONFIG_OK['target_prefix'] + UUID_1 + + jdssd.ra.detach_target_vol.return_value = None + jdssd.ra.delete_target.side_effect = ( + jexc.JDSSRESTException(reason='running test', request='test')) + + self.assertRaises(jexc.JDSSException, + jdssd._remove_target_volume, + UUID_1, + jcom.vname(UUID_1)) + + jdssd.ra.detach_target_vol.assert_called_once_with(target_name, + jcom.vname(UUID_1)) + jdssd.ra.delete_target.assert_called_with(target_name) + + def test_ensure_export(self): + jdssd, ctx = self.get_jdss_driver(CONFIG_MULTI_HOST) + + provider_auth = 'chap user_name 123456789012' + + with mock.patch.object(jdssd, "_ensure_target_volume"): + jdssd.ensure_export(UUID_1, provider_auth) + jdssd._ensure_target_volume.assert_called_once_with( + UUID_1, + jcom.vname(UUID_1), + provider_auth) + + def test_initialize_connection(self): + + # Test Ok + jdssd, ctx = self.get_jdss_driver(CONFIG_MULTI_HOST) + + volume_id = UUID_1 + provider_auth = 'chap user_name 123456789012' + + multipath = True + + target_name = CONFIG_OK['target_prefix'] + UUID_1 + + properties = {'auth_method': 'chap', + 'auth_password': '123456789012', + 'auth_username': 'user_name', + 'target_discovered': False, + 'target_iqns': [target_name, target_name], + 'target_lun': 0, + 'target_luns': [0, 0], + 'target_portals': ['192.168.0.2:3260', + '192.168.0.3:3260']} + + con_info = { + 'driver_volume_type': 'iscsi', + 'data': properties, + } + + vname = jcom.vname(volume_id) + with mock.patch.object(jdssd, '_ensure_target_volume'): + ret = jdssd.initialize_connection(volume_id, provider_auth, + multipath=multipath) + jdssd._ensure_target_volume.assert_called_with(UUID_1, vname, + provider_auth) + + self.assertEqual(con_info, ret) + + # Test initialize for snapshot + jdssd, ctx = self.get_jdss_driver(CONFIG_MULTI_HOST) + + volume_id = UUID_1 + snapshot_id = UUID_S1 + + provider_auth = 'chap user_name 123456789012' + + multipath = True + + target_name = CONFIG_OK['target_prefix'] + UUID_S1 + + properties = {'auth_method': 'chap', + 'auth_password': '123456789012', + 'auth_username': 'user_name', + 'target_discovered': False, + 'target_iqns': [target_name, target_name], + 'target_lun': 0, + 'target_luns': [0, 0], + 'target_portals': ['192.168.0.2:3260', + '192.168.0.3:3260']} + + con_info = { + 'driver_volume_type': 'iscsi', + 'data': properties, + } + + sname = jcom.sname(snapshot_id, volume_id) + with mock.patch.object(jdssd, '_ensure_target_volume'): + ret = jdssd.initialize_connection(volume_id, provider_auth, + snapshot_id=snapshot_id, + multipath=multipath) + jdssd._ensure_target_volume.assert_called_with(UUID_S1, sname, + provider_auth, + mode='ro') + + self.assertEqual(con_info, ret) + + # Test no auth + jdssd, ctx = self.get_jdss_driver(CONFIG_MULTI_HOST) + + volume_id = UUID_1 + + provider_auth = None + + multipath = True + + target_name = CONFIG_OK['target_prefix'] + UUID_1 + + properties = {'auth_method': 'chap', + 'auth_password': '123456789012', + 'auth_username': 'user_name', + 'target_discovered': False, + 'target_iqns': [target_name, target_name], + 'target_lun': 0, + 'target_luns': [0, 0], + 'target_portals': ['192.168.0.2:3260', + '192.168.0.3:3260']} + + con_info = { + 'driver_volume_type': 'iscsi', + 'data': properties, + } + + sname = jcom.sname(snapshot_id, volume_id) + with mock.patch.object(jdssd, '_ensure_target_volume'): + + self.assertRaises(jexc.JDSSException, + jdssd.initialize_connection, + volume_id, + provider_auth, + multipath=multipath) + jdssd._ensure_target_volume.assert_not_called() + + def test_create_target_volume(self): + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + vid = jcom.vname(UUID_1) + target_name = CONFIG_OK['target_prefix'] + UUID_1 + provider_auth = 'chap user_name 123456789012' + + cred = {'name': 'user_name', 'password': '123456789012'} + + patches = [ + mock.patch.object(jdssd, "_attach_target_volume"), + mock.patch.object(jdssd, "_set_target_credentials")] + + self.start_patches(patches) + jdssd._create_target_volume(UUID_1, vid, provider_auth) + jdssd.ra.create_target.assert_called_once_with(target_name, + use_chap=True) + jdssd._attach_target_volume.assert_called_once_with( + target_name, jcom.vname(UUID_1)) + jdssd._set_target_credentials.assert_called_once_with( + target_name, cred) + self.stop_patches(patches) + + def test_create_target_volume_for_snapshot_attachment(self): + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + vid = jcom.sname(UUID_S1, UUID_1) + target_name = CONFIG_OK['target_prefix'] + UUID_S1 + provider_auth = 'chap user_name 123456789012' + + cred = {'name': 'user_name', 'password': '123456789012'} + + patches = [ + mock.patch.object(jdssd, "_attach_target_volume"), + mock.patch.object(jdssd, "_set_target_credentials")] + + self.start_patches(patches) + jdssd._create_target_volume(UUID_S1, vid, provider_auth) + jdssd.ra.create_target.assert_called_once_with(target_name, + use_chap=True) + jdssd._attach_target_volume.assert_called_once_with( + target_name, jcom.sname(UUID_S1, UUID_1)) + jdssd._set_target_credentials.assert_called_once_with( + target_name, cred) + self.stop_patches(patches) + + def test_attach_target_volume(self): + + jdssd, ctx = self.get_jdss_driver(CONFIG_BACKEND_NAME) + + target_name = CONFIG_OK['target_prefix'] + UUID_1 + vname = jcom.vname(UUID_1) + + jdssd.ra.attach_target_vol.return_value = None + jdssd.ra.delete_target.return_value = None + + jdssd._attach_target_volume(target_name, vname) + + jdssd.ra.attach_target_vol.assert_called_once_with( + target_name, vname) + jdssd.ra.delete_target.assert_not_called() + + ex = jexc.JDSSResourceExistsException(res=target_name) + jdssd.ra.attach_target_vol.side_effect = ex + + self.assertRaises(jexc.JDSSException, + jdssd._attach_target_volume, + target_name, + vname) + jdssd.ra.delete_target.assert_called_once_with(target_name) + + def test_set_target_credentials(self): + jdssd, ctx = self.get_jdss_driver(CONFIG_BACKEND_NAME) + + target_name = CONFIG_BACKEND_NAME['target_prefix'] + UUID_1 + cred = {'name': 'user_name', 'password': '123456789012'} + + jdssd.ra.create_target_user.return_value = None + jdssd.ra.delete_target.return_value = None + + jdssd._set_target_credentials(target_name, cred) + + jdssd.ra.create_target_user.assert_called_once_with( + target_name, cred) + jdssd.ra.delete_target.assert_not_called() + + ex = jexc.JDSSResourceExistsException(res=target_name) + jdssd.ra.create_target_user.side_effect = ex + + self.assertRaises(jexc.JDSSException, + jdssd._set_target_credentials, + target_name, + cred) + jdssd.ra.delete_target.assert_called_once_with(target_name) + + def test_clone_object(self): + + # test ok + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + ovname = jcom.vname(UUID_1) + sname = jcom.sname(UUID_S1, UUID_1) + cvname = jcom.vname(UUID_2) + jdssd._clone_object(cvname, sname, ovname, sparse=True) + + jdssd.ra.create_snapshot.assesrt_not_called() + jdssd.ra.create_volume_from_snapshot.assert_called_once_with( + cvname, sname, ovname, sparse=True) + + # test create snapshot + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + ovname = jcom.vname(UUID_1) + sname = jcom.sname(UUID_S1, UUID_1) + cvname = jcom.vname(UUID_2) + jdssd._clone_object(cvname, sname, ovname, sparse=True) + + jdssd.ra.create_snapshot.assesrt_not_called() + jdssd.ra.create_volume_from_snapshot.assert_called_once_with( + cvname, sname, ovname, sparse=True) + + # test create from snapshot failed + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + ovname = jcom.vname(UUID_1) + sname = jcom.sname(UUID_S1, UUID_1) + cvname = jcom.vname(UUID_2) + jdssd.ra.create_volume_from_snapshot.side_effect = [ + jexc.JDSSVolumeExistsException(volume=cvname)] + self.assertRaises(jexc.JDSSException, + jdssd._clone_object, + cvname, + sname, + ovname, + sparse=True) + + jdssd.ra.create_snapshot.assesrt_not_called() + jdssd.ra.create_volume_from_snapshot.assert_called_once_with( + cvname, sname, ovname, sparse=True) + jdssd.ra.delete_snapshot(ovname, sname, + force_umount=True, + recursively_children=True) + + def test_resize_volume(self): + + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + vname = jcom.vname(UUID_1) + jdssd.resize_volume(UUID_1, 2) + + jdssd.ra.extend_lun.assert_called_once_with(vname, o_units.Gi * 2) + + def test_create_cloned_volume(self): + + # test ok + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + cvname = jcom.vname(UUID_2) + vname = jcom.vname(UUID_1) + + jdssd.ra.get_lun.return_value = copy.deepcopy(VOLUME_GET_THAT_IS_CLONE) + with mock.patch.object(jdssd, '_clone_object'): + jdssd.create_cloned_volume(UUID_2, UUID_1, 1, sparse=False) + + jdssd._clone_object.assert_called_once_with( + cvname, cvname, vname, + sparse=False, + create_snapshot=True) + + # test clone from snapshot + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + cvname = jcom.vname(UUID_2) + vname = jcom.vname(UUID_1) + sname = jcom.sname(UUID_S1, UUID_1) + jdssd.ra.get_lun.return_value = copy.deepcopy(VOLUME_GET_THAT_IS_CLONE) + with mock.patch.object(jdssd, '_clone_object'): + jdssd.create_cloned_volume(UUID_2, UUID_1, 1, + snapshot_name=UUID_S1, + sparse=False) + + jdssd._clone_object.assert_called_once_with( + cvname, sname, vname, + sparse=False, + create_snapshot=False) + + # test extend + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + cvname = jcom.vname(UUID_2) + vname = jcom.vname(UUID_1) + + get_vol = copy.deepcopy(VOLUME_GET_THAT_IS_CLONE) + + get_vol['volsize'] = "1073145824" + jdssd.ra.get_lun.return_value = get_vol + with mock.patch.object(jdssd, '_clone_object'),\ + mock.patch.object(jdssd, "resize_volume"): + jdssd.create_cloned_volume(UUID_2, UUID_1, 1, sparse=False) + + jdssd._clone_object.assert_called_once_with( + cvname, cvname, vname, + sparse=False, + create_snapshot=True) + jdssd.resize_volume.assert_called_once_with(UUID_2, 1) + + def test_create_snapshot(self): + jdssd, ctx = self.get_jdss_driver(CONFIG_BACKEND_NAME) + + jdssd.create_snapshot(UUID_S1, UUID_1) + + vname = jcom.vname(UUID_1) + sname = jcom.sname(UUID_S1, UUID_1) + + jdssd.ra.create_snapshot.assert_called_once_with(vname, sname) + + def test_create_export_snapshot(self): + jdssd, ctx = self.get_jdss_driver(CONFIG_BACKEND_NAME) + provider_auth = 'chap user_name 123456789012' + + sname = jcom.sname(UUID_S1, UUID_1) + vname = jcom.vname(UUID_1) + + with mock.patch.object(jdssd, '_clone_object'), \ + mock.patch.object(jdssd, '_ensure_target_volume'): + jdssd.create_export_snapshot(UUID_S1, UUID_1, provider_auth) + + jdssd._clone_object.assert_called_once_with(sname, sname, vname, + sparse=True, + create_snapshot=False) + + jdssd._ensure_target_volume(UUID_S1, sname, provider_auth) + + def test_remove_export(self): + + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + vname = jcom.vname(UUID_1) + + patches = [ + mock.patch.object( + jdssd, + "_remove_target_volume", + return_value=None)] + + self.start_patches(patches) + + jdssd.remove_export(UUID_1) + jdssd._remove_target_volume.assert_called_once_with(UUID_1, vname) + + self.stop_patches(patches) + + def test_remove_export_snapshot(self): + + # remove ok + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + with mock.patch.object(jdssd, "_remove_target_volume"), \ + mock.patch.object(jdssd, "_delete_volume"): + + jdssd.remove_export_snapshot(UUID_S1, UUID_1) + jdssd._delete_volume.assert_called_once() + jdssd._remove_target_volume.assert_called_once() + + # remove export failed + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + vname = jcom.sname(UUID_S1, UUID_1) + with mock.patch.object(jdssd, "_remove_target_volume"), \ + mock.patch.object(jdssd, "_delete_volume"): + + jdssd._remove_target_volume.side_effect = [ + jexc.JDSSResourceIsBusyException(res=vname)] + + self.assertRaises(jexc.JDSSResourceIsBusyException, + jdssd.remove_export_snapshot, + UUID_S1, + UUID_1) + jdssd._delete_volume.assert_called_once() + jdssd._remove_target_volume.assert_called_once() + + def test_delete_snapshot(self): + + # Delete ok, letion of snapshot with no clones + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + vname = jcom.vname(UUID_1) + sname = jcom.sname(UUID_S1, UUID_1) + + jdssd.ra.delete_snapshot.side_effect = [None] + + jdssd._delete_snapshot(vname, sname) + jdssd.ra.delete_snapshot.assert_called_once_with(vname, sname, + force_umount=True) + + # Test deletion of snapshot with clones + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + vname = jcom.vname(UUID_1) + sname = jcom.sname(UUID_S1, UUID_1) + side_eff = [jexc.JDSSSnapshotIsBusyException(snapshot=sname), None] + jdssd.ra.delete_snapshot.side_effect = side_eff + + side_eff = [copy.deepcopy(SNAPSHOT_GET_ONE_CLONE)] + + jdssd.ra.get_snapshot.side_effect = side_eff + + with mock.patch.object(jdssd, '_promote_newest_delete'): + jdssd._delete_snapshot(vname, sname) + jdssd._promote_newest_delete.assert_not_called() + jdssd.ra.delete_snapshot.assert_called_once_with(vname, sname, + force_umount=True) + + # Test deletion of attached snapshot + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + vname = jcom.vname(UUID_1) + sname = jcom.sname(UUID_S1, UUID_1) + side_eff = [jexc.JDSSSnapshotIsBusyException(snapshot=sname), None] + jdssd.ra.delete_snapshot.side_effect = side_eff + + get_snap = copy.deepcopy(SNAPSHOT_GET_ONE_CLONE) + + get_snap['clones'] = f"Pool-0/{sname}" + side_eff = [get_snap] + + jdssd.ra.get_snapshot.side_effect = side_eff + + delete_snap_expected = [mock.call(vname, sname, force_umount=True), + mock.call(vname, sname, force_umount=True)] + + with mock.patch.object(jdssd, '_promote_newest_delete'): + + jdssd._delete_snapshot(vname, sname) + jdssd._promote_newest_delete.assert_called_once_with(sname) + + jdssd.ra.delete_snapshot.assert_has_calls(delete_snap_expected) + + def test_delete_snapshot_wrapper(self): + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + volume_name = UUID_1 + snapshot_name = UUID_S1 + + with mock.patch.object(jdssd, "_delete_snapshot"): + jdssd.delete_snapshot(volume_name, snapshot_name) + + jdssd._delete_snapshot.assert_called_once_with( + jcom.vname(UUID_1), + jcom.sname(UUID_S1, UUID_1)) + + def test_ensure_target_volume(self): + + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + id = UUID_1 + vid = jcom.vname(UUID_1) + + target_name = CONFIG_OK['target_prefix'] + UUID_1 + provider_auth = 'chap user_name 123456789012' + + cred = {'name': 'user_name'} + + patches = [ + mock.patch.object(jdssd, "_attach_target_volume"), + mock.patch.object(jdssd, "_set_target_credentials"), + mock.patch.object(jdssd, "_attach_target_volume")] + + jdssd.ra.is_target.return_value = True + jdssd.ra.is_target_lun.return_value = True + jdssd.ra.get_target_user.return_value = [cred] + + self.start_patches(patches) + + jdssd._ensure_target_volume(id, vid, provider_auth) + + jdssd.ra.is_target.assert_called_once_with(target_name) + + jdssd.ra.is_target_lun.assert_called_once_with(target_name, vid) + + jdssd.ra.get_target_user.assert_called_once_with(target_name) + + jdssd.ra.delete_target_user.assert_not_called() + jdssd._set_target_credentials.assert_not_called() + self.stop_patches(patches) + + def test_ensure_target_volume_not_attached(self): + + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + id = UUID_1 + vid = jcom.vname(UUID_1) + + target_name = CONFIG_OK['target_prefix'] + UUID_1 + provider_auth = 'chap user_name 123456789012' + + cred = {'name': 'user_name'} + + patches = [ + mock.patch.object(jdssd, "_attach_target_volume"), + mock.patch.object(jdssd, "_set_target_credentials"), + mock.patch.object(jdssd, "_attach_target_volume")] + + jdssd.ra.is_target.return_value = True + jdssd.ra.is_target_lun.return_value = False + jdssd.ra.get_target_user.return_value = [cred] + + self.start_patches(patches) + + jdssd._ensure_target_volume(id, vid, provider_auth) + + jdssd.ra.is_target.assert_called_once_with(target_name) + jdssd.ra.is_target_lun.assert_called_once_with(target_name, vid) + + jdssd._attach_target_volume.assert_called_once_with( + target_name, vid) + jdssd.ra.get_target_user.assert_called_once_with(target_name) + + jdssd.ra.delete_target_user.assert_not_called() + jdssd._set_target_credentials.assert_not_called() + self.stop_patches(patches) + + def test_ensure_target_volume_no_target(self): + + jdssd, ctx = self.get_jdss_driver(CONFIG_OK) + + id = UUID_1 + vid = jcom.vname(UUID_1) + + target_name = CONFIG_OK['target_prefix'] + UUID_1 + provider_auth = 'chap user_name 123456789012' + + cred = {'name': 'user_name'} + + patches = [ + mock.patch.object(jdssd, "_create_target_volume"), + mock.patch.object(jdssd, "_attach_target_volume"), + mock.patch.object(jdssd, "_set_target_credentials"), + mock.patch.object(jdssd, "_attach_target_volume")] + + jdssd.ra.is_target.return_value = False + jdssd.ra.get_target_user.return_value = cred['name'] + + self.start_patches(patches) + + jdssd._ensure_target_volume(id, vid, provider_auth) + + jdssd.ra.is_target.assert_called_once_with(target_name) + jdssd._create_target_volume.assert_called_once_with(id, + vid, + provider_auth) + + jdssd.ra.is_target_lun.assert_not_called() + self.stop_patches(patches) diff --git a/cinder/tests/unit/volume/drivers/open_e/test_iscsi.py b/cinder/tests/unit/volume/drivers/open_e/test_iscsi.py index 64ec0f4f87a..dbe245816d0 100644 --- a/cinder/tests/unit/volume/drivers/open_e/test_iscsi.py +++ b/cinder/tests/unit/volume/drivers/open_e/test_iscsi.py @@ -32,6 +32,11 @@ UUID_2 = '12345678-1234-1234-1234-000000000002' UUID_3 = '12345678-1234-1234-1234-000000000003' UUID_4 = '12345678-1234-1234-1234-000000000004' +UUID_S1 = '12345678-1234-1234-1234-100000000001' +UUID_S2 = '12345678-1234-1234-1234-100000000002' +UUID_S3 = '12345678-1234-1234-1234-100000000003' +UUID_S4 = '12345678-1234-1234-1234-100000000004' + CONFIG_OK = { 'san_hosts': ['192.168.0.2'], 'san_api_port': 82, @@ -122,20 +127,20 @@ CONFIG_MULTI_HOST = { } SNAPSHOTS_CASCADE_1 = [ - {"name": jcom.sname(UUID_1), - "clones": "Pool-0/" + jcom.sname(UUID_1)}, - {"name": jcom.sname(UUID_2), - "clones": "Pool-0/" + jcom.sname(UUID_2)}, - {"name": jcom.sname(UUID_3), - "clones": "Pool-0/" + jcom.sname(UUID_3)}] + {"name": jcom.sname(UUID_S1, UUID_1), + "clones": "Pool-0/" + jcom.sname(UUID_S1, UUID_1)}, + {"name": jcom.sname(UUID_S1, UUID_2), + "clones": "Pool-0/" + jcom.sname(UUID_S1, UUID_2)}, + {"name": jcom.sname(UUID_S1, UUID_3), + "clones": "Pool-0/" + jcom.sname(UUID_S1, UUID_3)}] SNAPSHOTS_CASCADE_2 = [ - {"name": jcom.sname(UUID_1), - "clones": "Pool-0/" + jcom.sname(UUID_1)}, + {"name": jcom.sname(UUID_S1, UUID_1), + "clones": "Pool-0/" + jcom.sname(UUID_S1, UUID_1)}, {"name": jcom.vname(UUID_2), "clones": "Pool-0/" + jcom.vname(UUID_2)}, - {"name": jcom.sname(UUID_3), - "clones": "Pool-0/" + jcom.sname(UUID_3)}] + {"name": jcom.sname(UUID_S1, UUID_3), + "clones": "Pool-0/" + jcom.sname(UUID_S1, UUID_3)}] SNAPSHOTS_CASCADE_3 = [ {"name": jcom.vname(UUID_4), @@ -148,19 +153,19 @@ SNAPSHOTS_CLONE = [ "clones": "Pool-0/" + jcom.vname(UUID_1)}] SNAPSHOTS_GARBAGE = [ - {"name": jcom.sname(UUID_1), + {"name": jcom.sname(UUID_S1, UUID_1), "clones": "Pool-0/" + jcom.vname(UUID_2)}, - {"name": jcom.sname(UUID_2), + {"name": jcom.sname(UUID_S1, UUID_2), "clones": ""}] SNAPSHOTS_RECURSIVE_1 = [ - {"name": jcom.sname(UUID_1), - "clones": "Pool-0/" + jcom.sname(UUID_1)}, - {"name": jcom.sname(UUID_2), + {"name": jcom.sname(UUID_S1, UUID_1), + "clones": "Pool-0/" + jcom.sname(UUID_S1, UUID_1)}, + {"name": jcom.sname(UUID_S1, UUID_2), "clones": "Pool-0/" + jcom.hidden(UUID_2)}] SNAPSHOTS_RECURSIVE_CHAIN_1 = [ - {"name": jcom.sname(UUID_3), + {"name": jcom.sname(UUID_S1, UUID_3), "clones": "Pool-0/" + jcom.hidden(UUID_3)}] SNAPSHOTS_RECURSIVE_CHAIN_2 = [ @@ -179,14 +184,16 @@ def get_jdss_exceptions(): jexc.JDSSResourceExistsException(res="test_resource"), jexc.JDSSSnapshotExistsException(snapshot="test_snapshot"), jexc.JDSSVolumeExistsException(volume="test_volume"), - jexc.JDSSSnapshotIsBusyException(snapshot="test_snapshot")] + jexc.JDSSResourceIsBusyException(res="test_resource"), + jexc.JDSSSnapshotIsBusyException(snapshot="test_snapshot"), + jexc.JDSSOSException(message="Some os error")] return out -class TestOpenEJovianDSSDriver(test.TestCase): +class TestOpenEJovianDSSISCSIDriver(test.TestCase): - def get_driver(self, config): + def get_iscsi_driver(self, config): ctx = context.get_admin_context() cfg = mock.Mock() @@ -202,6 +209,7 @@ class TestOpenEJovianDSSDriver(test.TestCase): ra.is_pool_exists.return_value = True jdssd.do_setup(ctx) jdssd.ra = mock.Mock() + jdssd.driver = mock.Mock() return jdssd, ctx def start_patches(self, patches): @@ -221,9 +229,10 @@ class TestOpenEJovianDSSDriver(test.TestCase): jdssd.configuration = cfg jdssd.ra = mock.Mock() + jdssd.driver = mock.Mock() # No IP - jdssd.ra.is_pool_exists.return_value = True + jdssd.driver.rest_config_is_ok.return_value = True jdssd.jovian_hosts = [] jdssd.block_size = ['64K'] @@ -231,41 +240,47 @@ class TestOpenEJovianDSSDriver(test.TestCase): jdssd.check_for_setup_error) # No pool detected - jdssd.ra.is_pool_exists.return_value = False + jdssd.driver.rest_config_is_ok.return_value = False jdssd.jovian_hosts = ['192.168.0.2'] jdssd.block_size = ['64K'] self.assertRaises(exception.VolumeDriverException, jdssd.check_for_setup_error) # Bad block size - jdssd.ra.is_pool_exists.return_value = True + jdssd.driver.rest_config_is_ok.return_value = True jdssd.jovian_hosts = ['192.168.0.2', '192.168.0.3'] jdssd.block_size = ['61K'] self.assertRaises(exception.InvalidConfigurationValue, jdssd.check_for_setup_error) - def test_get_provider_location(self): - jdssd, ctx = self.get_driver(CONFIG_OK) + def test_get_provider_info(self): + jdssd, ctx = self.get_iscsi_driver(CONFIG_OK) + host = CONFIG_OK["san_hosts"][0] port = CONFIG_OK["target_port"] target_name = CONFIG_OK["target_prefix"] + UUID_1 - patches = [mock.patch.object( - jdssd.ra, - "get_active_host", - return_value=host)] - out = '{host}:{port},1 {name} 0'.format( + location = '{host}:{port},1 {name} 0'.format( host=host, port=port, name=target_name ) - self.start_patches(patches) - self.assertEqual(out, jdssd._get_provider_location(UUID_1)) - self.stop_patches(patches) + + jdssd.driver.get_provider_location.return_value = location + + ret = jdssd._get_provider_info(UUID_1) + + jdssd.driver.get_provider_location.assert_called_once_with(UUID_1) + self.assertEqual(location, ret['provider_location']) + cred_format = (r"CHAP [0-9,a-z,A-Z]{{{name_len}}} " + "[0-9,a-z,A-Z]{{{pass_len}}}").format( + name_len=8, + pass_len=CONFIG_OK['chap_password_len']) + self.assertIsNotNone(re.match(cred_format, ret['provider_auth'])) def test_create_volume(self): - jdssd, ctx = self.get_driver(CONFIG_OK) + jdssd, ctx = self.get_iscsi_driver(CONFIG_OK) vol = fake_volume.fake_volume_obj(ctx) vol.id = UUID_1 vol.size = 1 @@ -273,27 +288,29 @@ class TestOpenEJovianDSSDriver(test.TestCase): port = CONFIG_OK["target_port"] target_name = CONFIG_OK["target_prefix"] + UUID_1 - - jdssd.ra.create_lun.return_value = None - jdssd.ra.get_active_host.return_value = host - - ret = jdssd.create_volume(vol) - location = '{host}:{port},1 {name} 0'.format( host=host, port=port, name=target_name ) + + jdssd.driver.get_provider_location.return_value = location + + ret = jdssd.create_volume(vol) + self.assertEqual(location, ret['provider_location']) cred_format = (r"CHAP [0-9,a-z,A-Z]{{{name_len}}} " "[0-9,a-z,A-Z]{{{pass_len}}}").format( name_len=8, pass_len=CONFIG_OK['chap_password_len']) + jdssd.driver.create_volume.assert_called_once_with( + vol.id, vol.size, sparse=False, block_size="128K") self.assertIsNotNone(re.match(cred_format, ret['provider_auth'])) + self.assertEqual(location, ret['provider_location']) def test_create_volume_small_block(self): - jdssd, ctx = self.get_driver(CONFIG_BLOCK_SIZE) + jdssd, ctx = self.get_iscsi_driver(CONFIG_BLOCK_SIZE) vol = fake_volume.fake_volume_obj(ctx) vol.id = UUID_1 vol.size = 1 @@ -301,15 +318,21 @@ class TestOpenEJovianDSSDriver(test.TestCase): port = CONFIG_OK["target_port"] target_name = CONFIG_OK["target_prefix"] + UUID_1 - vname = jcom.vname(UUID_1) + location = '{host}:{port},1 {name} 0'.format( + host=host, + port=port, + name=target_name + ) + + jdssd.driver.create_volume.return_value = None + jdssd.driver.get_provider_location.return_value = location - jdssd.ra.create_lun.return_value = None jdssd.ra.get_active_host.return_value = host ret = jdssd.create_volume(vol) - jdssd.ra.create_lun.assert_called_once_with( - vname, o_units.Gi, sparse=False, block_size="64K") + jdssd.driver.create_volume.assert_called_once_with( + vol.id, vol.size, sparse=False, block_size="64K") location = '{host}:{port},1 {name} 0'.format( host=host, @@ -323,381 +346,45 @@ class TestOpenEJovianDSSDriver(test.TestCase): pass_len=CONFIG_OK['chap_password_len']) self.assertIsNotNone(re.match(cred_format, ret['provider_auth'])) - def test_hide_object(self): - jdssd, ctx = self.get_driver(CONFIG_OK) - vname = jcom.vname(UUID_1) - - jdssd.ra.modify_lun.return_value = None - jdssd._hide_object(vname) - - hidden_volume = {"name": jcom.hidden(UUID_1)} - jdssd.ra.modify_lun.assert_called_once_with(vname, hidden_volume) - - def test_clean_garbage_snapshots(self): - jdssd, ctx = self.get_driver(CONFIG_OK) - o_vname = jcom.vname(UUID_1) - o_snaps = SNAPSHOTS_GARBAGE.copy() - - jdssd.ra.delete_snapshot.return_value = None - jdssd._clean_garbage_snapshots(o_vname, o_snaps) - jdssd.ra.delete_snapshot.assert_called_once_with( - o_vname, - SNAPSHOTS_GARBAGE[1]["name"]) - # Test exception handling - for exc in get_jdss_exceptions(): - o_snaps = SNAPSHOTS_GARBAGE.copy() - jdssd.ra.delete_snapshot.side_effect = exc - try: - jdssd._clean_garbage_snapshots(o_vname, o_snaps) - except Exception as err: - self.assertIsInstance(err, exception.VolumeBackendAPIException) - - def test_cascade_volume_delete_snapshots(self): - jdssd, ctx = self.get_driver(CONFIG_OK) - o_vname = jcom.vname(UUID_1) - - # Volume with 3 snapshots and no descendants - # We should delete snapshots and then cal for volume deletion - o_snaps = SNAPSHOTS_CASCADE_1.copy() - - jdssd.ra.modify_lun.return_value = None - jdssd.ra.delete_snapshot.return_value = None - jdssd.ra.get_snapshots.side_effect = [ - SNAPSHOTS_EMPTY, - SNAPSHOTS_EMPTY, - SNAPSHOTS_EMPTY] - - with mock.patch.object(jdssd, "_gc_delete", return_value=None) as gc: - jdssd._cascade_volume_delete(o_vname, o_snaps) - gc.assert_called_once_with(o_vname) - delete_snapshot_expected = [ - mock.call(o_vname, - SNAPSHOTS_CASCADE_1[0]["name"], - recursively_children=True, - recursively_dependents=True, - force_umount=True), - mock.call(o_vname, - SNAPSHOTS_CASCADE_1[1]["name"], - recursively_children=True, - recursively_dependents=True, - force_umount=True), - mock.call(o_vname, - SNAPSHOTS_CASCADE_1[2]["name"], - recursively_children=True, - recursively_dependents=True, - force_umount=True)] - jdssd.ra.delete_snapshot.assert_has_calls(delete_snapshot_expected) - - def test_cascade_volume_delete_with_clone(self): - # Volume with 2 snapshots and 1 clone - # We should delete snapshots and then cal for volume hiding - jdssd, ctx = self.get_driver(CONFIG_OK) - o_vname = jcom.vname(UUID_1) - o_snaps = SNAPSHOTS_CASCADE_2.copy() - - jdssd.ra.modify_lun.return_value = None - jdssd.ra.delete_snapshot.return_value = None - jdssd.ra.get_snapshots.side_effect = [ - SNAPSHOTS_EMPTY, - SNAPSHOTS_EMPTY] - - patches = [mock.patch.object(jdssd, "_gc_delete"), - mock.patch.object(jdssd, "_hide_object")] - - self.start_patches(patches) - - jdssd._cascade_volume_delete(o_vname, o_snaps) - - jdssd._hide_object.assert_called_once_with(o_vname) - jdssd._gc_delete.assert_not_called() - - self.stop_patches(patches) - - delete_snapshot_expected = [ - mock.call(o_vname, - SNAPSHOTS_CASCADE_2[0]["name"], - recursively_children=True, - recursively_dependents=True, - force_umount=True), - mock.call(o_vname, - SNAPSHOTS_CASCADE_2[2]["name"], - recursively_children=True, - recursively_dependents=True, - force_umount=True)] - jdssd.ra.delete_snapshot.assert_has_calls(delete_snapshot_expected) - - def test_cascade_volume_delete_snapshot_clone(self): + def test_delete_volume_cascade(self): # Volume with 3 snapshots and 1 clone of a snapshots # We should delete childless snapshots # and then cal for volume deletion - - jdssd, ctx = self.get_driver(CONFIG_OK) - o_vname = jcom.vname(UUID_1) - o_snaps = SNAPSHOTS_CASCADE_1.copy() - - jdssd.ra.modify_lun.return_value = None - jdssd.ra.delete_snapshot.return_value = None - jdssd.ra.get_snapshots.side_effect = [ - SNAPSHOTS_EMPTY, - SNAPSHOTS_CASCADE_3.copy(), - SNAPSHOTS_EMPTY] - get_snapshots = [ - mock.call(SNAPSHOTS_CASCADE_1[0]['name']), - mock.call(SNAPSHOTS_CASCADE_1[1]['name']), - mock.call(SNAPSHOTS_CASCADE_1[2]['name']) - ] - hide_object_expected = [ - mock.call(SNAPSHOTS_CASCADE_1[1]["name"]), - mock.call(o_vname)] - - patches = [mock.patch.object(jdssd, "_gc_delete"), - mock.patch.object(jdssd, "_hide_object")] - - self.start_patches(patches) - - jdssd._cascade_volume_delete(o_vname, o_snaps) - jdssd._hide_object.assert_has_calls(hide_object_expected) - jdssd._gc_delete.assert_not_called() - - self.stop_patches(patches) - - jdssd.ra.get_snapshots.assert_has_calls(get_snapshots) - - delete_snapshot_expected = [ - mock.call(o_vname, - SNAPSHOTS_CASCADE_2[0]["name"], - recursively_children=True, - recursively_dependents=True, - force_umount=True), - mock.call(o_vname, - SNAPSHOTS_CASCADE_2[2]["name"], - recursively_children=True, - recursively_dependents=True, - force_umount=True)] - jdssd.ra.delete_snapshot.assert_has_calls(delete_snapshot_expected) - - def test_delete_volume_with_snapshots(self): - jdssd, ctx = self.get_driver(CONFIG_OK) + jdssd, ctx = self.get_iscsi_driver(CONFIG_OK) vol = fake_volume.fake_volume_obj(ctx) vol.id = UUID_1 - vname = jcom.vname(UUID_1) - jdssd.ra.get_snapshots.side_effect = [SNAPSHOTS_CASCADE_1.copy()] - patches = [mock.patch.object(jdssd, "_cascade_volume_delete"), - mock.patch.object(jdssd, "_gc_delete"), - mock.patch.object(jdssd, "_hide_object")] + jdssd.driver.delete_volume.return_value = None - self.start_patches(patches) + jdssd.delete_volume(vol, cascade=True) - jdssd.delete_volume(vol, cascade=False) - jdssd._gc_delete.assert_not_called() - jdssd._cascade_volume_delete.assert_not_called() - jdssd._hide_object.assert_called_once_with(vname) - - self.stop_patches(patches) - - jdssd.ra.get_snapshots.assert_called_once_with(vname) - - def test_delete_volume_without_snapshots(self): - jdssd, ctx = self.get_driver(CONFIG_OK) - vol = fake_volume.fake_volume_obj(ctx) - vol.id = UUID_1 - vname = jcom.vname(UUID_1) - jdssd.ra.get_snapshots.side_effect = [SNAPSHOTS_EMPTY.copy()] - - patches = [mock.patch.object(jdssd, "_cascade_volume_delete"), - mock.patch.object(jdssd, "_gc_delete"), - mock.patch.object(jdssd, "_hide_object")] - - self.start_patches(patches) - - jdssd.delete_volume(vol, cascade=False) - jdssd._gc_delete.assert_called_once_with(vname) - jdssd._cascade_volume_delete.assert_not_called() - jdssd._hide_object.assert_not_called() - - self.stop_patches(patches) - - jdssd.ra.get_snapshots.assert_called_once_with(vname) + jdssd.driver.delete_volume.assert_called_once_with(UUID_1, + cascade=True) def test_delete_volume_exceptions(self): - jdssd, ctx = self.get_driver(CONFIG_OK) + jdssd, ctx = self.get_iscsi_driver(CONFIG_OK) vol = fake_volume.fake_volume_obj(ctx) vol.id = UUID_1 - patches = [mock.patch.object(jdssd, "_cascade_volume_delete"), - mock.patch.object(jdssd, "_gc_delete"), - mock.patch.object(jdssd, "_hide_object")] - - self.start_patches(patches) - for exc in get_jdss_exceptions(): - jdssd.ra.get_snapshots.side_effect = exc + jdssd.driver.delete_volume.side_effect = exc try: jdssd.delete_volume(vol, cascade=False) except Exception as err: self.assertIsInstance(err, exception.VolumeBackendAPIException) - jdssd._gc_delete.assert_not_called() - jdssd._cascade_volume_delete.assert_not_called() - jdssd._hide_object.assert_not_called() - - self.stop_patches(patches) - - def test_gc_delete_not_clone(self): - - jdssd, ctx = self.get_driver(CONFIG_OK) - vol = fake_volume.fake_volume_obj(ctx) - vol.id = UUID_1 - - get_lun_resp = {'vscan': None, - 'full_name': 'Pool-0/' + jcom.vname(UUID_1), - 'userrefs': None, - 'primarycache': 'all', - 'logbias': 'latency', - 'creation': '1591543140', - 'sync': 'always', - 'is_clone': False, - 'dedup': 'off', - 'sharenfs': None, - 'receive_resume_token': None, - 'volsize': '1073741824'} - - jdssd.ra.get_lun.return_value = get_lun_resp - jdssd.ra.delete_lun.return_value = None - - patches = [mock.patch.object(jdssd, "_delete_back_recursively")] - - self.start_patches(patches) - - jdssd._gc_delete(jcom.vname(UUID_1)) - - jdssd._delete_back_recursively.assert_not_called() - jdssd.ra.delete_lun.assert_called_once_with(jcom.vname(UUID_1), - force_umount=True) - - self.stop_patches(patches) - - def test_gc_delete_is_clone(self): - - jdssd, ctx = self.get_driver(CONFIG_OK) - vol = fake_volume.fake_volume_obj(ctx) - vol.id = UUID_1 - origin = "Pool-0/{vorig}@{sorig}".format(vorig=jcom.vname(UUID_1), - sorig=jcom.vname(UUID_2)) - - get_lun_resp = {'origin': origin, - 'vscan': None, - 'full_name': 'Pool-0/' + jcom.vname(UUID_2), - 'userrefs': None, - 'primarycache': 'all', - 'logbias': 'latency', - 'creation': '1591543140', - 'sync': 'always', - 'is_clone': True, - 'dedup': 'off', - 'sharenfs': None, - 'receive_resume_token': None, - 'volsize': '1073741824'} - - jdssd.ra.get_lun.return_value = get_lun_resp - jdssd.ra.delete_lun.return_value = None - - patches = [mock.patch.object(jdssd, "_delete_back_recursively")] - - self.start_patches(patches) - - jdssd._gc_delete(jcom.vname(UUID_1)) - - jdssd._delete_back_recursively.assert_called_once_with( - jcom.vname(UUID_1), jcom.vname(UUID_2)) - jdssd.ra.delete_lun.assert_not_called() - - self.stop_patches(patches) - - def test_delete_back_recursively_res_active(self): - - jdssd, ctx = self.get_driver(CONFIG_OK) - opvname = jcom.vname(UUID_1) - opsname = jcom.sname(UUID_2) - - jdssd._delete_back_recursively(opvname, opsname) - - jdssd.ra.delete_snapshot.assert_called_once_with( - opvname, - opsname, - recursively_children=True, - recursively_dependents=True, - force_umount=True) - - def test_delete_back_recursively_hidden_have_snapshots(self): - - jdssd, ctx = self.get_driver(CONFIG_OK) - opvname = jcom.hidden(UUID_1) - opsname = jcom.sname(UUID_2) - - jdssd.ra.get_snapshots.return_value = SNAPSHOTS_RECURSIVE_1.copy() - - jdssd._delete_back_recursively(opvname, opsname) - - jdssd.ra.delete_snapshot.assert_called_once_with( - opvname, - opsname, - recursively_children=True, - recursively_dependents=True, - force_umount=True) - - def test_delete_back_recursively_single_snapshot(self): - - jdssd, ctx = self.get_driver(CONFIG_OK) - opvname = jcom.hidden(UUID_2) - opsname = jcom.sname(UUID_3) - - jdssd.ra.get_snapshots.side_effect = [ - SNAPSHOTS_RECURSIVE_CHAIN_1.copy(), - SNAPSHOTS_RECURSIVE_CHAIN_2.copy()] - - origin = "Pool-0/{vorig}@{sorig}".format(vorig=jcom.vname(UUID_1), - sorig=jcom.vname(UUID_2)) - get_lun_resp = {'origin': origin, - 'vscan': None, - 'full_name': 'Pool-0/' + jcom.hidden(UUID_2), - 'userrefs': None, - 'primarycache': 'all', - 'logbias': 'latency', - 'creation': '1591543140', - 'sync': 'always', - 'is_clone': True, - 'dedup': 'off', - 'sharenfs': None, - 'receive_resume_token': None, - 'volsize': '1073741824'} - jdssd.ra.get_lun.return_value = get_lun_resp - jdssd._delete_back_recursively(opvname, opsname) - - jdssd.ra.delete_snapshot.assert_called_once_with( - jcom.vname(UUID_1), - jcom.vname(UUID_2), - recursively_children=True, - recursively_dependents=True, - force_umount=True) - - get_snapshots_expected = [mock.call(opvname)] - jdssd.ra.get_snapshots.assert_has_calls(get_snapshots_expected) - def test_extend_volume(self): - jdssd, ctx = self.get_driver(CONFIG_OK) + jdssd, ctx = self.get_iscsi_driver(CONFIG_OK) vol = fake_volume.fake_volume_obj(ctx) vol.id = UUID_1 - jdssd.ra.extend_lun.return_value = None + jdssd.driver.resize_volume.return_value = None jdssd.extend_volume(vol, 2) - jdssd.ra.extend_lun.assert_called_once_with( - jcom.vname(UUID_1), - 2147483648) + jdssd.driver.resize_volume.assert_called_once_with( + UUID_1, 2) def test_extend_volume_exceptions(self): - jdssd, ctx = self.get_driver(CONFIG_OK) + jdssd, ctx = self.get_iscsi_driver(CONFIG_OK) vol = fake_volume.fake_volume_obj(ctx) vol.id = UUID_1 @@ -707,211 +394,10 @@ class TestOpenEJovianDSSDriver(test.TestCase): except Exception as err: self.assertIsInstance(err, exception.VolumeBackendAPIException) - def test_revert_to_snapshot(self): - - jdssd, ctx = self.get_driver(CONFIG_OK) - vol = fake_volume.fake_volume_obj(ctx) - vol.id = UUID_1 - snap = fake_snapshot.fake_snapshot_obj(ctx) - snap.id = UUID_2 - - vname = jcom.vname(UUID_1) - sname = jcom.sname(UUID_2) - - get_lun_resp_1 = {'vscan': None, - 'full_name': 'Pool-0/' + UUID_1, - 'userrefs': None, - 'primarycache': 'all', - 'logbias': 'latency', - 'creation': '1591543140', - 'sync': 'always', - 'is_clone': False, - 'dedup': 'off', - 'sharenfs': None, - 'receive_resume_token': None, - 'volsize': '2147483648'} - - get_lun_resp_2 = {'vscan': None, - 'full_name': 'Pool-0/' + UUID_1, - 'userrefs': None, - 'primarycache': 'all', - 'logbias': 'latency', - 'creation': '1591543140', - 'sync': 'always', - 'is_clone': False, - 'dedup': 'off', - 'sharenfs': None, - 'receive_resume_token': None, - 'volsize': '1073741824'} - - jdssd.ra.get_lun.side_effect = [get_lun_resp_1, get_lun_resp_2] - - get_lun_expected = [mock.call(vname), mock.call(vname)] - - jdssd.revert_to_snapshot(ctx, vol, snap) - - jdssd.ra.get_lun.assert_has_calls(get_lun_expected) - - jdssd.ra.rollback_volume_to_snapshot.assert_called_once_with(vname, - sname) - jdssd.ra.extend_lun(vname, '2147483648') - - def test_revert_to_snapshot_exception(self): - - jdssd, ctx = self.get_driver(CONFIG_OK) - vol = fake_volume.fake_volume_obj(ctx) - vol.id = UUID_1 - snap = fake_snapshot.fake_snapshot_obj(ctx) - snap.id = UUID_2 - - vname = jcom.vname(UUID_1) - - get_lun_resp_no_size = {'vscan': None, - 'full_name': 'Pool-0/' + vname, - 'userrefs': None, - 'primarycache': 'all', - 'logbias': 'latency', - 'creation': '1591543140', - 'sync': 'always', - 'is_clone': False, - 'dedup': 'off', - 'sharenfs': None, - 'receive_resume_token': None, - 'volsize': None} - - get_lun_resp_1 = {'vscan': None, - 'full_name': 'Pool-0/' + vname, - 'userrefs': None, - 'primarycache': 'all', - 'logbias': 'latency', - 'creation': '1591543140', - 'sync': 'always', - 'is_clone': False, - 'dedup': 'off', - 'sharenfs': None, - 'receive_resume_token': None, - 'volsize': '2147483648'} - - get_lun_resp_2 = {'vscan': None, - 'full_name': 'Pool-0/' + vname, - 'userrefs': None, - 'primarycache': 'all', - 'logbias': 'latency', - 'creation': '1591543140', - 'sync': 'always', - 'is_clone': False, - 'dedup': 'off', - 'sharenfs': None, - 'receive_resume_token': None, - 'volsize': '1073741824'} - - jdssd.ra.get_lun.side_effect = [get_lun_resp_no_size, get_lun_resp_2] - - self.assertRaises(exception.VolumeDriverException, - jdssd.revert_to_snapshot, - ctx, - vol, - snap) - - jdssd.ra.get_lun.side_effect = [get_lun_resp_1, get_lun_resp_2] - - jdssd.ra.rollback_volume_to_snapshot.side_effect = [ - jexc.JDSSResourceNotFoundException(res=vname)] - self.assertRaises(exception.VolumeBackendAPIException, - jdssd.revert_to_snapshot, - ctx, - vol, - snap) - - jdssd.ra.get_lun.side_effect = [get_lun_resp_1, - jexc.JDSSException("some_error")] - - jdssd.ra.rollback_volume_to_snapshot.side_effect = [ - jexc.JDSSResourceNotFoundException(res=vname)] - self.assertRaises(exception.VolumeBackendAPIException, - jdssd.revert_to_snapshot, - ctx, - vol, - snap) - - def test_clone_object(self): - jdssd, ctx = self.get_driver(CONFIG_OK) - origin = jcom.vname(UUID_1) - clone = jcom.vname(UUID_2) - - jdssd.ra.create_snapshot.return_value = None - jdssd.ra.create_volume_from_snapshot.return_value = None - - jdssd._clone_object(origin, clone) - jdssd.ra.create_snapshot.assert_called_once_with(origin, clone) - jdssd.ra.create_volume_from_snapshot.assert_called_once_with( - clone, - clone, - origin, - sparse=False) - - def test_clone_object_dne(self): - jdssd, ctx = self.get_driver(CONFIG_OK) - calls = [] - origin = jcom.vname(UUID_1) - clone = jcom.vname(UUID_2) - calls.append(mock.call(origin, clone)) - - jdssd.ra.create_snapshot.side_effect = ( - jexc.JDSSResourceNotFoundException(res=origin)) - - self.assertRaises(exception.VolumeNotFound, - jdssd._clone_object, origin, clone) - - origin = jcom.sname(UUID_1) - calls.append(mock.call(origin, clone)) - - self.assertRaises(exception.SnapshotNotFound, - jdssd._clone_object, origin, clone) - jdssd.ra.create_snapshot.assert_has_calls(calls) - - def test_clone_object_exists(self): - jdssd, ctx = self.get_driver(CONFIG_OK) - - origin = jcom.vname(UUID_1) - clone = jcom.vname(UUID_2) - - jdssd.ra.create_snapshot.side_effect = ( - jexc.JDSSSnapshotExistsException(snapshot=clone)) - - jdssd.ra.delete_snapshot.side_effect = ( - jexc.JDSSSnapshotIsBusyException(snapshot=clone)) - - self.assertRaises(exception.Duplicate, - jdssd._clone_object, origin, clone) - jdssd.ra.delete_snapshot.assert_called_once_with(origin, clone) - jdssd.ra.create_snapshot.assert_called_once_with(origin, clone) - - def test_clone_object_volume_exists(self): - jdssd, ctx = self.get_driver(CONFIG_OK) - - origin = jcom.vname(UUID_1) - clone = jcom.vname(UUID_2) - - jdssd.ra.create_snapshot.return_value = None - jdssd.ra.create_volume_from_snapshot.side_effect = ( - jexc.JDSSVolumeExistsException(volume=clone)) - - self.assertRaises(exception.Duplicate, - jdssd._clone_object, origin, clone) - jdssd.ra.create_snapshot.assert_called_once_with(origin, clone) - jdssd.ra.create_volume_from_snapshot.assert_called_once_with( - clone, - clone, - origin, - sparse=CONFIG_OK['san_thin_provision']) - def test_create_cloned_volume(self): - jdssd, ctx = self.get_driver(CONFIG_OK) - - origin_vname = jcom.vname(UUID_1) - clone_vname = jcom.vname(UUID_2) + jdssd, ctx = self.get_iscsi_driver(CONFIG_OK) + sparse = CONFIG_OK['san_thin_provision'] orig_vol = fake_volume.fake_volume_obj(ctx) orig_vol.id = UUID_1 orig_vol.size = 1 @@ -935,120 +421,27 @@ class TestOpenEJovianDSSDriver(test.TestCase): name_len=8, pass_len=CONFIG_OK['chap_password_len']) - patches = [ - mock.patch.object(jdssd, "_clone_object", return_value=None), - mock.patch.object(jdssd, "extend_volume", return_value=None), - mock.patch.object( - jdssd, - "_get_provider_location", - return_value=location), - mock.patch.object( - jdssd, - "_get_provider_auth", - return_value=cred_format)] - - jdssd.ra.get_lun.return_value = { - 'vscan': None, - 'full_name': 'Pool-0/' + UUID_2, - 'userrefs': None, - 'primarycache': 'all', - 'logbias': 'latency', - 'creation': '1591543140', - 'sync': 'always', - 'is_clone': False, - 'dedup': 'off', - 'sharenfs': None, - 'receive_resume_token': None, - 'volsize': '1073741824'} - - self.start_patches(patches) + jdssd.driver.get_provider_location.return_value = location + jdssd.driver.create_cloned_volume.return_value = None ret = jdssd.create_cloned_volume(clone_vol, orig_vol) - jdssd.extend_volume.assert_not_called() - jdssd._clone_object.assert_called_once_with(origin_vname, clone_vname) - self.stop_patches(patches) + jdssd.driver.create_cloned_volume.assert_called_once_with( + clone_vol.id, + orig_vol.id, + clone_vol.size, + sparse=sparse) - jdssd.ra.get_lun.assert_called_once_with(jcom.vname(clone_vol.id)) self.assertEqual(location, ret['provider_location']) - self.assertEqual(cred_format, ret['provider_auth']) + self.assertIsNotNone(re.match(cred_format, ret['provider_auth'])) + self.assertEqual(location, ret['provider_location']) def test_create_volume_from_snapshot(self): - jdssd, ctx = self.get_driver(CONFIG_OK) - - origin_sname = jcom.sname(UUID_1) - - clone_vname = jcom.vname(UUID_2) + jdssd, ctx = self.get_iscsi_driver(CONFIG_OK) orig_snap = fake_snapshot.fake_snapshot_obj(ctx) - orig_snap.id = UUID_1 - - clone_vol = fake_volume.fake_volume_obj(ctx) - clone_vol.id = UUID_2 - clone_vol.size = 1 - - host = CONFIG_OK["san_hosts"][0] - port = CONFIG_OK["target_port"] - target_name = CONFIG_OK["target_prefix"] + UUID_2 - - location = '{host}:{port},1 {name} 0'.format( - host=host, - port=port, - name=target_name - ) - - cred_format = (r"CHAP [0-9,a-z,A-Z]{{{name_len}}} " - "[0-9,a-z,A-Z]{{{pass_len}}}").format( - name_len=8, - pass_len=CONFIG_OK['chap_password_len']) - - patches = [ - mock.patch.object(jdssd, "_clone_object", return_value=None), - mock.patch.object(jdssd, "extend_volume", return_value=None), - mock.patch.object( - jdssd, - "_get_provider_location", - return_value=location), - mock.patch.object( - jdssd, - "_get_provider_auth", - return_value=cred_format)] - - jdssd.ra.get_lun.return_value = { - 'vscan': None, - 'full_name': 'Pool-0/' + UUID_2, - 'userrefs': None, - 'primarycache': 'all', - 'logbias': 'latency', - 'creation': '1591543140', - 'sync': 'always', - 'is_clone': False, - 'dedup': 'off', - 'sharenfs': None, - 'receive_resume_token': None, - 'volsize': '1073741824'} - - self.start_patches(patches) - - ret = jdssd.create_volume_from_snapshot(clone_vol, orig_snap) - - jdssd.extend_volume.assert_not_called() - jdssd._clone_object.assert_called_once_with(origin_sname, clone_vname) - self.stop_patches(patches) - - jdssd.ra.get_lun.assert_called_once_with(jcom.vname(clone_vol.id)) - self.assertEqual(location, ret['provider_location']) - self.assertEqual(cred_format, ret['provider_auth']) - - def test_create_volume_from_snapshot_extend(self): - jdssd, ctx = self.get_driver(CONFIG_OK) - - origin_sname = jcom.sname(UUID_1) - - clone_vname = jcom.vname(UUID_2) - - orig_snap = fake_snapshot.fake_snapshot_obj(ctx) - orig_snap.id = UUID_1 + orig_snap.id = UUID_S1 + orig_snap.volume_id = UUID_1 clone_vol = fake_volume.fake_volume_obj(ctx) clone_vol.id = UUID_2 @@ -1070,130 +463,72 @@ class TestOpenEJovianDSSDriver(test.TestCase): pass_len=CONFIG_OK['chap_password_len']) patches = [ - mock.patch.object(jdssd, "_clone_object", return_value=None), - mock.patch.object(jdssd, "extend_volume", return_value=None), - mock.patch.object( - jdssd, - "_get_provider_location", - return_value=location), mock.patch.object( jdssd, "_get_provider_auth", return_value=cred_format)] - jdssd.ra.get_lun.return_value = { - 'vscan': None, - 'full_name': 'Pool-0/' + UUID_2, - 'userrefs': None, - 'primarycache': 'all', - 'logbias': 'latency', - 'creation': '1591543140', - 'sync': 'always', - 'is_clone': False, - 'dedup': 'off', - 'sharenfs': None, - 'receive_resume_token': None, - 'volsize': '1073741824'} + jdssd.driver.get_provider_location.return_value = location + jdssd.driver.create_cloned_volume.return_value = None self.start_patches(patches) ret = jdssd.create_volume_from_snapshot(clone_vol, orig_snap) - jdssd.extend_volume.assert_called_once_with(clone_vol, clone_vol.size) - jdssd._clone_object.assert_called_once_with(origin_sname, clone_vname) + jdssd.driver.create_cloned_volume.assert_called_once_with( + clone_vol.id, + orig_snap.volume_id, + clone_vol.size, + snapshot_name=orig_snap.id) self.stop_patches(patches) - jdssd.ra.get_lun.assert_called_once_with(jcom.vname(clone_vol.id)) self.assertEqual(location, ret['provider_location']) self.assertEqual(cred_format, ret['provider_auth']) def test_create_snapshot(self): - jdssd, ctx = self.get_driver(CONFIG_OK) + jdssd, ctx = self.get_iscsi_driver(CONFIG_OK) - vname = jcom.vname(UUID_1) - - sname = jcom.sname(UUID_2) - - snap = fake_snapshot.fake_snapshot_obj(ctx, id=UUID_2) + snap = fake_snapshot.fake_snapshot_obj(ctx, id=UUID_S1) snap.volume_id = UUID_1 - patches = [ - mock.patch.object(jdssd, "_clone_object", return_value=None)] - - self.start_patches(patches) - + jdssd.driver.create_snapshot.return_value = None jdssd.create_snapshot(snap) - jdssd._clone_object.assert_called_once_with(vname, sname) - self.stop_patches(patches) + jdssd.driver.create_snapshot.assert_called_once_with(UUID_S1, UUID_1) - def test_delete_snapshot_no_child(self): - jdssd, ctx = self.get_driver(CONFIG_OK) + def test_delete_snapshot(self): + jdssd, ctx = self.get_iscsi_driver(CONFIG_OK) - sname = jcom.sname(UUID_2) - - snap = fake_snapshot.fake_snapshot_obj(ctx, id=UUID_2) - - jdssd.ra.get_snapshots.return_value = SNAPSHOTS_EMPTY - patches = [ - mock.patch.object( - jdssd, - "_clean_garbage_snapshots", - return_value=SNAPSHOTS_EMPTY), - mock.patch.object(jdssd, "_clone_object", return_value=None), - mock.patch.object(jdssd, "_hide_object", return_value=None), - mock.patch.object(jdssd, "_gc_delete", return_value=None)] - - self.start_patches(patches) - - jdssd.create_snapshot(snap) + snap = fake_snapshot.fake_snapshot_obj(ctx, + id=UUID_S1, + volume_id=UUID_1) + jdssd.driver.delete_snapshot.return_value = None jdssd.delete_snapshot(snap) - jdssd._gc_delete.assert_called_once_with(sname) - jdssd._hide_object.assert_not_called() - jdssd._clean_garbage_snapshots.assert_called_once_with( - sname, - SNAPSHOTS_EMPTY) - self.stop_patches(patches) + jdssd.driver.delete_snapshot.assert_called_once_with(UUID_1, UUID_S1) - def test_delete_snapshot_has_clone(self): - jdssd, ctx = self.get_driver(CONFIG_OK) + def test_delete_snapshot_exceptions(self): + jdssd, ctx = self.get_iscsi_driver(CONFIG_OK) + snap = fake_snapshot.fake_snapshot_obj(ctx, id=UUID_1) - sname = jcom.sname(UUID_2) - - snap = fake_snapshot.fake_snapshot_obj(ctx, id=UUID_2) - - jdssd.ra.get_snapshots.return_value = SNAPSHOTS_EMPTY - patches = [ - mock.patch.object( - jdssd, - "_clean_garbage_snapshots", - return_value=SNAPSHOTS_CLONE), - mock.patch.object(jdssd, "_clone_object", return_value=None), - mock.patch.object(jdssd, "_hide_object", return_value=None), - mock.patch.object(jdssd, "_gc_delete", return_value=None)] - - self.start_patches(patches) - - jdssd.create_snapshot(snap) - - jdssd.delete_snapshot(snap) - jdssd._gc_delete.assert_not_called() - jdssd._hide_object.assert_called_once_with(sname) - jdssd._clean_garbage_snapshots.assert_called_once_with( - sname, - SNAPSHOTS_EMPTY) - self.stop_patches(patches) + for exc in get_jdss_exceptions(): + jdssd.driver.delete_snapshot.side_effect = exc + try: + ret = jdssd.delete_snapshot(snap) + if isinstance(exc, jexc.JDSSVolumeNotFoundException): + self.assertTrue(ret is None) + except Exception as err: + self.assertIsInstance(err, exception.VolumeBackendAPIException) def test_local_path(self): - jdssd, ctx = self.get_driver(CONFIG_OK) + jdssd, ctx = self.get_iscsi_driver(CONFIG_OK) vol = fake_snapshot.fake_snapshot_obj(ctx, id=UUID_1) self.assertRaises(NotImplementedError, jdssd.local_path, vol) def test_get_provider_auth(self): - jdssd, ctx = self.get_driver(CONFIG_OK) + jdssd, ctx = self.get_iscsi_driver(CONFIG_OK) auth = jdssd._get_provider_auth() cred_format = (r"CHAP [0-9,a-z,A-Z]{{{name_len}}} " @@ -1205,7 +540,7 @@ class TestOpenEJovianDSSDriver(test.TestCase): def test_get_provider_auth_long(self): long_pass_config = CONFIG_OK.copy() long_pass_config['chap_password_len'] = 16 - jdssd, ctx = self.get_driver(long_pass_config) + jdssd, ctx = self.get_iscsi_driver(long_pass_config) auth = jdssd._get_provider_auth() cred_format = (r"CHAP [0-9,a-z,A-Z]{{{name_len}}} " @@ -1216,73 +551,67 @@ class TestOpenEJovianDSSDriver(test.TestCase): def test_create_export(self): - jdssd, ctx = self.get_driver(CONFIG_OK) - vol = fake_volume.fake_volume_obj(ctx) - vol.id = UUID_1 + jdssd, ctx = self.get_iscsi_driver(CONFIG_OK) + vol = fake_volume.fake_volume_obj(ctx, id=UUID_1) - patches = [ - mock.patch.object( - jdssd, - "_ensure_target_volume", - return_value=None), - mock.patch.object( - jdssd, - "_get_provider_location", - return_value='provider_location')] + host = CONFIG_OK["san_hosts"][0] + port = CONFIG_OK["target_port"] + target_name = CONFIG_OK["target_prefix"] + UUID_1 + location = '{host}:{port},1 {name} 0'.format( + host=host, + port=port, + name=target_name + ) - self.start_patches(patches) + jdssd.driver.get_provider_location.return_value = location + + jdssd.driver.get_provider_location.return_value = location ret = jdssd.create_export(ctx, vol, "connector") - jdssd._ensure_target_volume.assert_called_once_with(vol) + jdssd.driver.ensure_export.assert_called_once_with(vol.id, + mock.ANY) - self.stop_patches(patches) - - self.assertEqual('provider_location', ret["provider_location"]) + self.assertEqual(location, ret["provider_location"]) def test_ensure_export(self): - jdssd, ctx = self.get_driver(CONFIG_OK) + jdssd, ctx = self.get_iscsi_driver(CONFIG_OK) vol = fake_volume.fake_volume_obj(ctx) vol.id = UUID_1 - patches = [ - mock.patch.object( - jdssd, - "_ensure_target_volume", - return_value=None)] + host = CONFIG_OK["san_hosts"][0] + port = CONFIG_OK["target_port"] + target_name = CONFIG_OK["target_prefix"] + UUID_1 + location = '{host}:{port},1 {name} 0'.format( + host=host, + port=port, + name=target_name + ) + auth = 'chap user_name 123456789012' + jdssd.driver.get_provider_location.return_value = location + with mock.patch.object(jdssd, '_get_provider_auth'): - self.start_patches(patches) - - jdssd.ensure_export(ctx, vol) - jdssd._ensure_target_volume.assert_called_once_with(vol) - - self.stop_patches(patches) + jdssd._get_provider_auth.return_value = auth + ret = jdssd.ensure_export(ctx, vol) + jdssd._get_provider_auth.assert_called_once() + jdssd.driver.ensure_export.assert_called_once_with(vol.id, auth) + self.assertEqual(location, ret["provider_location"]) def test_remove_export(self): - jdssd, ctx = self.get_driver(CONFIG_OK) + jdssd, ctx = self.get_iscsi_driver(CONFIG_OK) vol = fake_volume.fake_volume_obj(ctx, id=UUID_1) - patches = [ - mock.patch.object( - jdssd, - "_remove_target_volume", - return_value=None)] - - self.start_patches(patches) - jdssd.remove_export(ctx, vol) - jdssd._remove_target_volume.assert_called_once_with(vol) - - self.stop_patches(patches) + jdssd.driver.remove_export.assert_called_once_with(UUID_1) def test_update_volume_stats(self): - jdssd, ctx = self.get_driver(CONFIG_BACKEND_NAME) + jdssd, ctx = self.get_iscsi_driver(CONFIG_BACKEND_NAME) location_info = 'JovianISCSIDriver:192.168.0.2:Pool-0' correct_out = { 'vendor_name': 'Open-E', - 'driver_version': "1.0.2", + 'driver_version': "1.0.3", 'storage_protocol': 'iSCSI', 'total_capacity_gb': 100, 'free_capacity_gb': 50, @@ -1295,313 +624,39 @@ class TestOpenEJovianDSSDriver(test.TestCase): jdssd.ra.get_pool_stats.return_value = { 'size': 100 * o_units.Gi, 'available': 50 * o_units.Gi} - jdssd.ra.get_active_host.return_value = CONFIG_OK['san_hosts'] + jdssd.ra.get_active_host.return_value = CONFIG_OK['san_hosts'][0] jdssd._update_volume_stats() self.assertEqual(correct_out, jdssd._stats) - def test_create_target(self): - - jdssd, ctx = self.get_driver(CONFIG_BACKEND_NAME) - - target_name = CONFIG_OK['target_prefix'] + UUID_1 - jdssd.ra.create_target.return_value = None - jdssd._create_target(target_name, use_chap=True) - - jdssd.ra.create_target.assert_called_once_with( - target_name, use_chap=True) - - jdssd.ra.create_target.side_effect = jexc.JDSSResourceExistsException( - res=target_name) - - self.assertRaises(exception.Duplicate, - jdssd._create_target, - target_name, - use_chap=True) - - def test_attach_target_volume(self): - - jdssd, ctx = self.get_driver(CONFIG_BACKEND_NAME) - - target_name = CONFIG_OK['target_prefix'] + UUID_1 - vname = jcom.vname(UUID_1) - - jdssd.ra.attach_target_vol.return_value = None - jdssd.ra.delete_target.return_value = None - - jdssd._attach_target_volume(target_name, vname) - - jdssd.ra.attach_target_vol.assert_called_once_with( - target_name, vname) - jdssd.ra.delete_target.assert_not_called() - - ex = jexc.JDSSResourceExistsException(res=target_name) - jdssd.ra.attach_target_vol.side_effect = ex - - self.assertRaises(exception.VolumeBackendAPIException, - jdssd._attach_target_volume, - target_name, - vname) - jdssd.ra.delete_target.assert_called_once_with(target_name) - - def test_set_target_credentials(self): - jdssd, ctx = self.get_driver(CONFIG_BACKEND_NAME) - - target_name = CONFIG_BACKEND_NAME['target_prefix'] + UUID_1 - cred = {'name': 'user_name', 'password': '123456789012'} - - jdssd.ra.create_target_user.return_value = None - jdssd.ra.delete_target.return_value = None - - jdssd._set_target_credentials(target_name, cred) - - jdssd.ra.create_target_user.assert_called_once_with( - target_name, cred) - jdssd.ra.delete_target.assert_not_called() - - ex = jexc.JDSSResourceExistsException(res=target_name) - jdssd.ra.create_target_user.side_effect = ex - - self.assertRaises(exception.VolumeBackendAPIException, - jdssd._set_target_credentials, - target_name, - cred) - jdssd.ra.delete_target.assert_called_once_with(target_name) - - def test_create_target_volume(self): - jdssd, ctx = self.get_driver(CONFIG_OK) - - vol = fake_volume.fake_volume_obj(ctx) - vol.id = UUID_1 - target_name = CONFIG_OK['target_prefix'] + UUID_1 - vol.provider_auth = 'chap user_name 123456789012' - - cred = {'name': 'user_name', 'password': '123456789012'} - - patches = [ - mock.patch.object(jdssd, "_create_target"), - mock.patch.object(jdssd, "_attach_target_volume"), - mock.patch.object(jdssd, "_set_target_credentials")] - - self.start_patches(patches) - jdssd._create_target_volume(vol) - jdssd._create_target.assert_called_once_with(target_name, True) - jdssd._attach_target_volume.assert_called_once_with( - target_name, jcom.vname(UUID_1)) - jdssd._set_target_credentials.assert_called_once_with( - target_name, cred) - self.stop_patches(patches) - - def test_ensure_target_volume(self): - - jdssd, ctx = self.get_driver(CONFIG_OK) - - vol = fake_volume.fake_volume_obj(ctx) - vol.id = UUID_1 - vname = jcom.vname(UUID_1) - - target_name = CONFIG_OK['target_prefix'] + UUID_1 - vol.provider_auth = 'chap user_name 123456789012' - - cred = {'name': 'user_name'} - - patches = [ - mock.patch.object(jdssd, "_create_target"), - mock.patch.object(jdssd, "_attach_target_volume"), - mock.patch.object(jdssd, "_set_target_credentials"), - mock.patch.object(jdssd, "_attach_target_volume")] - - jdssd.ra.is_target.return_value = True - jdssd.ra.is_target_lun.return_value = True - jdssd.ra.get_target_user.return_value = [cred] - - self.start_patches(patches) - - jdssd._ensure_target_volume(vol) - - jdssd.ra.is_target.assert_called_once_with(target_name) - - jdssd.ra.is_target_lun.assert_called_once_with(target_name, vname) - - jdssd.ra.get_target_user.assert_called_once_with(target_name) - - jdssd.ra.delete_target_user.assert_not_called() - jdssd._set_target_credentials.assert_not_called() - self.stop_patches(patches) - - def test_ensure_target_volume_not_attached(self): - - jdssd, ctx = self.get_driver(CONFIG_OK) - - vol = fake_volume.fake_volume_obj(ctx) - vol.id = UUID_1 - vname = jcom.vname(UUID_1) - target_name = CONFIG_OK['target_prefix'] + UUID_1 - vol.provider_auth = 'chap user_name 123456789012' - - cred = {'name': 'user_name'} - - patches = [ - mock.patch.object(jdssd, "_create_target"), - mock.patch.object(jdssd, "_attach_target_volume"), - mock.patch.object(jdssd, "_set_target_credentials"), - mock.patch.object(jdssd, "_attach_target_volume")] - - jdssd.ra.is_target.return_value = True - jdssd.ra.is_target_lun.return_value = False - jdssd.ra.get_target_user.return_value = [cred] - - self.start_patches(patches) - - jdssd._ensure_target_volume(vol) - - jdssd.ra.is_target.assert_called_once_with(target_name) - jdssd.ra.is_target_lun.assert_called_once_with(target_name, vname) - - jdssd._attach_target_volume.assert_called_once_with( - target_name, vname) - jdssd.ra.get_target_user.assert_called_once_with(target_name) - - jdssd.ra.delete_target_user.assert_not_called() - jdssd._set_target_credentials.assert_not_called() - self.stop_patches(patches) - - def test_ensure_target_volume_no_target(self): - - jdssd, ctx = self.get_driver(CONFIG_OK) - - vol = fake_volume.fake_volume_obj(ctx) - vol.id = UUID_1 - target_name = CONFIG_OK['target_prefix'] + UUID_1 - vol.provider_auth = 'chap user_name 123456789012' - - cred = {'name': 'user_name'} - - patches = [ - mock.patch.object(jdssd, "_create_target_volume"), - mock.patch.object(jdssd, "_attach_target_volume"), - mock.patch.object(jdssd, "_set_target_credentials"), - mock.patch.object(jdssd, "_attach_target_volume")] - - jdssd.ra.is_target.return_value = False - jdssd.ra.get_target_user.return_value = cred['name'] - - self.start_patches(patches) - - jdssd._ensure_target_volume(vol) - - jdssd.ra.is_target.assert_called_once_with(target_name) - jdssd._create_target_volume.assert_called_once_with(vol) - - jdssd.ra.is_target_lun.assert_not_called() - self.stop_patches(patches) - - def test_remove_target_volume(self): - - jdssd, ctx = self.get_driver(CONFIG_OK) - - vol = fake_volume.fake_volume_obj(ctx) - vol.id = UUID_1 - target_name = CONFIG_OK['target_prefix'] + UUID_1 - - jdssd.ra.detach_target_vol.return_value = None - jdssd.ra.delete_target.return_value = None - - jdssd._remove_target_volume(vol) - - jdssd.ra.detach_target_vol.assert_called_once_with(target_name, - jcom.vname(UUID_1)) - jdssd.ra.delete_target.assert_called_with(target_name) - - def test_remove_target_volume_no_target(self): - - jdssd, ctx = self.get_driver(CONFIG_OK) - - vol = fake_volume.fake_volume_obj(ctx) - vol.id = UUID_1 - target_name = CONFIG_OK['target_prefix'] + UUID_1 - - jdssd.ra.detach_target_vol.return_value = None - jdssd.ra.detach_target_vol.side_effect = ( - jexc.JDSSResourceNotFoundException(res=target_name)) - jdssd.ra.delete_target.return_value = None - - jdssd._remove_target_volume(vol) - - jdssd.ra.detach_target_vol.assert_called_once_with(target_name, - jcom.vname(UUID_1)) - jdssd.ra.delete_target.assert_called_with(target_name) - - def test_remove_target_volume_fail_to_detach(self): - - jdssd, ctx = self.get_driver(CONFIG_OK) - - vol = fake_volume.fake_volume_obj(ctx) - vol.id = UUID_1 - target_name = CONFIG_OK['target_prefix'] + UUID_1 - - jdssd.ra.detach_target_vol.side_effect = ( - jexc.JDSSRESTException(reason='running test', request='test')) - jdssd.ra.delete_target.return_value = None - - self.assertRaises(exception.VolumeBackendAPIException, - jdssd._remove_target_volume, vol) - - jdssd.ra.detach_target_vol.assert_called_once_with( - target_name, jcom.vname(UUID_1)) - jdssd.ra.delete_target.assert_not_called() - - def test_remove_target_volume_fail_to_delete(self): - - jdssd, ctx = self.get_driver(CONFIG_OK) - - vol = fake_volume.fake_volume_obj(ctx) - vol.id = UUID_1 - target_name = CONFIG_OK['target_prefix'] + UUID_1 - - jdssd.ra.detach_target_vol.return_value = None - jdssd.ra.delete_target.side_effect = ( - jexc.JDSSRESTException(reason='running test', request='test')) - - self.assertRaises(exception.VolumeBackendAPIException, - jdssd._remove_target_volume, vol) - - jdssd.ra.detach_target_vol.assert_called_once_with(target_name, - jcom.vname(UUID_1)) - jdssd.ra.delete_target.assert_called_with(target_name) - def test_get_iscsi_properties(self): - jdssd, ctx = self.get_driver(CONFIG_OK) + jdssd, ctx = self.get_iscsi_driver(CONFIG_OK) - vol = fake_volume.fake_volume_obj(ctx) - vol.id = UUID_1 - vol.provider_auth = 'chap user_name 123456789012' - - connector = {'multipath': True} + provider_auth = 'chap user_name 123456789012' target_name = CONFIG_OK['target_prefix'] + UUID_1 - ret = jdssd._get_iscsi_properties(vol, connector) + with mock.patch.object(jdssd.ra, "get_active_host"): + jdssd.ra.get_active_host.return_value = CONFIG_OK['san_hosts'][0] + ret = jdssd._get_iscsi_properties(UUID_1, provider_auth, + multipath=False) expected = {'auth_method': 'chap', 'auth_password': '123456789012', 'auth_username': 'user_name', 'target_discovered': False, - 'target_iqns': [target_name], + 'target_iqn': target_name, 'target_lun': 0, - 'target_luns': [0], - 'target_portals': ['192.168.0.2:3260']} + 'target_portal': '192.168.0.2:3260'} self.assertEqual(expected, ret) def test_get_iscsi_properties_multipath(self): - jdssd, ctx = self.get_driver(CONFIG_MULTI_HOST) + jdssd, ctx = self.get_iscsi_driver(CONFIG_MULTI_HOST) - vol = fake_volume.fake_volume_obj(ctx) - vol.id = UUID_1 - vol.provider_auth = 'chap user_name 123456789012' - - connector = {'multipath': True} + provider_auth = 'chap user_name 123456789012' target_name = CONFIG_OK['target_prefix'] + UUID_1 - ret = jdssd._get_iscsi_properties(vol, connector) + ret = jdssd._get_iscsi_properties(UUID_1, + provider_auth, + multipath=True) expected = {'auth_method': 'chap', 'auth_password': '123456789012', 'auth_username': 'user_name', @@ -1614,12 +669,45 @@ class TestOpenEJovianDSSDriver(test.TestCase): def test_initialize_connection(self): - jdssd, ctx = self.get_driver(CONFIG_MULTI_HOST) + jdssd, ctx = self.get_iscsi_driver(CONFIG_MULTI_HOST) - vol = fake_volume.fake_volume_obj(ctx) - vol.id = UUID_1 + vol = fake_volume.fake_volume_obj(ctx, id=UUID_1) vol.provider_auth = 'chap user_name 123456789012' + connector = {'multipath': False, 'ip': '172.16.0.2'} + + target_name = CONFIG_OK['target_prefix'] + UUID_1 + + properties = {'auth_method': 'chap', + 'auth_password': '123456789012', + 'auth_username': 'user_name', + 'target_discovered': False, + 'target_iqn': target_name, + 'target_lun': 0, + 'target_portal': '192.168.0.2:3260'} + + con_info = { + 'driver_volume_type': 'iscsi', + 'data': properties, + } + jdssd.driver.initialize_connection.return_value = con_info + init_con_exp = [mock.call(vol.id, + vol.provider_auth, + multipath=False)] + with mock.patch.object(jdssd.ra, "get_active_host"): + jdssd.ra.get_active_host.return_value = CONFIG_OK["san_hosts"][0] + + ret = jdssd.initialize_connection(vol, connector) + jdssd.driver.initialize_connection.assert_has_calls(init_con_exp) + self.assertEqual(con_info, ret) + + def test_initialize_connection_snapshot(self): + + jdssd, ctx = self.get_iscsi_driver(CONFIG_MULTI_HOST) + + snap = fake_snapshot.fake_snapshot_obj(ctx, id=UUID_1) + snap.provider_auth = 'chap user_name 123456789012' + connector = {'multipath': True, 'ip': '172.16.0.2'} target_name = CONFIG_OK['target_prefix'] + UUID_1 @@ -1638,7 +726,13 @@ class TestOpenEJovianDSSDriver(test.TestCase): 'driver_volume_type': 'iscsi', 'data': properties, } + init_con_exp = [mock.call(snap.volume_id, + snap.provider_auth, + snapshot_id=snap.id, + multipath=True)] - ret = jdssd.initialize_connection(vol, connector) + jdssd.driver.initialize_connection.return_value = con_info + ret = jdssd.initialize_connection_snapshot(snap, connector) + jdssd.driver.initialize_connection.assert_has_calls(init_con_exp) self.assertEqual(con_info, ret) diff --git a/cinder/tests/unit/volume/drivers/open_e/test_rest.py b/cinder/tests/unit/volume/drivers/open_e/test_rest.py index 87bbc933a58..b1388575a1d 100644 --- a/cinder/tests/unit/volume/drivers/open_e/test_rest.py +++ b/cinder/tests/unit/volume/drivers/open_e/test_rest.py @@ -18,7 +18,6 @@ from unittest import mock from oslo_utils import units as o_units from cinder import context -from cinder import exception from cinder.tests.unit import test from cinder.volume.drivers.open_e.jovian_common import exception as jexc from cinder.volume.drivers.open_e.jovian_common import jdss_common as jcom @@ -28,6 +27,10 @@ UUID_1 = '12345678-1234-1234-1234-000000000001' UUID_2 = '12345678-1234-1234-1234-000000000002' UUID_3 = '12345678-1234-1234-1234-000000000003' +UUID_S1 = '12345678-1234-1234-1234-100000000001' +UUID_S2 = '12345678-1234-1234-1234-100000000002' +UUID_S3 = '12345678-1234-1234-1234-100000000003' + CONFIG_OK = { 'san_hosts': ['192.168.0.2'], 'san_api_port': 82, @@ -39,7 +42,7 @@ CONFIG_OK = { 'jovian_ignore_tpath': [], 'target_port': 3260, 'jovian_pool': 'Pool-0', - 'iscsi_target_prefix': 'iqn.2020-04.com.open-e.cinder:', + 'target_prefix': 'iqn.2020-04.com.open-e.cinder:', 'chap_password_len': 12, 'san_thin_provision': False, 'jovian_block_size': '128K' @@ -408,7 +411,7 @@ class TestOpenEJovianRESTAPI(test.TestCase): delete_lun_expected += [mock.call('DELETE', addr)] jrest.rproxy.pool_request.return_value = resp self.assertRaises( - exception.VolumeIsBusy, + jexc.JDSSResourceIsBusyException, jrest.delete_lun, 'v_' + UUID_1) @@ -424,7 +427,6 @@ class TestOpenEJovianRESTAPI(test.TestCase): 'error': None, 'code': 204} req = {'recursively_children': True, - 'recursively_dependents': True, 'force_umount': True} delete_lun_expected = [mock.call('DELETE', addr, json_data=req)] @@ -432,7 +434,6 @@ class TestOpenEJovianRESTAPI(test.TestCase): self.assertIsNone( jrest.delete_lun('v_' + UUID_1, recursively_children=True, - recursively_dependents=True, force_umount=True)) jrest.rproxy.pool_request.assert_has_calls(delete_lun_expected) @@ -441,7 +442,7 @@ class TestOpenEJovianRESTAPI(test.TestCase): jrest, ctx = self.get_rest(CONFIG_OK) - tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1 + tname = CONFIG_OK['target_prefix'] + UUID_1 addr = '/san/iscsi/targets/{}'.format(tname) data = {'incoming_users_active': True, 'name': tname, @@ -483,7 +484,7 @@ class TestOpenEJovianRESTAPI(test.TestCase): jrest, ctx = self.get_rest(CONFIG_OK) # Create OK - tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1 + tname = CONFIG_OK['target_prefix'] + UUID_1 addr = '/san/iscsi/targets' data = {'incoming_users_active': True, 'name': tname, @@ -506,7 +507,7 @@ class TestOpenEJovianRESTAPI(test.TestCase): self.assertIsNone(jrest.create_target(tname)) # Target exists - tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1 + tname = CONFIG_OK['target_prefix'] + UUID_1 addr = '/san/iscsi/targets' data = {'incoming_users_active': True, 'name': tname, @@ -541,7 +542,7 @@ class TestOpenEJovianRESTAPI(test.TestCase): jrest.create_target, tname) # Unknown error - tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1 + tname = CONFIG_OK['target_prefix'] + UUID_1 addr = "/san/iscsi/targets" resp = {'data': data, @@ -576,7 +577,7 @@ class TestOpenEJovianRESTAPI(test.TestCase): jrest, ctx = self.get_rest(CONFIG_OK) # Delete OK - tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1 + tname = CONFIG_OK['target_prefix'] + UUID_1 addr = '/san/iscsi/targets/{}'.format(tname) resp = {'data': None, @@ -628,7 +629,7 @@ class TestOpenEJovianRESTAPI(test.TestCase): jrest, ctx = self.get_rest(CONFIG_OK) # Modify OK - tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1 + tname = CONFIG_OK['target_prefix'] + UUID_1 addr = '/san/iscsi/targets/{}/incoming-users'.format(tname) chap_cred = {"name": "chapuser", @@ -682,7 +683,7 @@ class TestOpenEJovianRESTAPI(test.TestCase): jrest, ctx = self.get_rest(CONFIG_OK) # Get OK - tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1 + tname = CONFIG_OK['target_prefix'] + UUID_1 addr = '/san/iscsi/targets/{}/incoming-users'.format(tname) chap_users = {"name": "chapuser"} @@ -736,7 +737,7 @@ class TestOpenEJovianRESTAPI(test.TestCase): jrest, ctx = self.get_rest(CONFIG_OK) # Delete OK - tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1 + tname = CONFIG_OK['target_prefix'] + UUID_1 user = "chapuser" addr = '/san/iscsi/targets/{}/incoming-users/chapuser'.format(tname) @@ -791,7 +792,7 @@ class TestOpenEJovianRESTAPI(test.TestCase): jrest, ctx = self.get_rest(CONFIG_OK) # lun present - tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1 + tname = CONFIG_OK['target_prefix'] + UUID_1 vname = jcom.vname(UUID_1) addr = '/san/iscsi/targets/{target}/luns/{lun}'.format( target=tname, lun=vname) @@ -852,7 +853,7 @@ class TestOpenEJovianRESTAPI(test.TestCase): jrest, ctx = self.get_rest(CONFIG_OK) # attach ok - tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1 + tname = CONFIG_OK['target_prefix'] + UUID_1 vname = jcom.vname(UUID_1) addr = '/san/iscsi/targets/{}/luns'.format(tname) @@ -875,7 +876,38 @@ class TestOpenEJovianRESTAPI(test.TestCase): mock.call('POST', addr, json_data=jbody)] self.assertIsNone(jrest.attach_target_vol(tname, vname)) + # attach with mode and lun + jrest, ctx = self.get_rest(CONFIG_OK) + + tname = CONFIG_OK['target_prefix'] + UUID_1 + vname = jcom.vname(UUID_1) + + addr = '/san/iscsi/targets/{}/luns'.format(tname) + jbody = {"name": vname, "lun": 1, "mode": 'ro'} + + data = {"block_size": 512, + "device_handler": "vdisk_fileio", + "lun": 0, + "mode": "ro", + "name": vname, + "prod_id": "Storage", + "scsi_id": "99e2c883331edf87"} + + resp = {'data': data, + 'error': None, + 'code': 201} + + jrest.rproxy.pool_request.return_value = resp + attach_target_vol_expected = [ + mock.call('POST', addr, json_data=jbody)] + self.assertIsNone(jrest.attach_target_vol(tname, vname, + lun_id=1, mode='ro')) + jrest.rproxy.pool_request.assert_has_calls(attach_target_vol_expected) + # lun attached already + jrest, ctx = self.get_rest(CONFIG_OK) + jbody = {"name": vname, "lun": 0} + url = 'http://85.14.118.246:11582/api/v3/pools/Pool-0/{}'.format(addr) msg = 'Volume /dev/Pool-0/{} is already used.'.format(vname) err = {"class": "opene.exceptions.ItemConflictError", @@ -893,6 +925,9 @@ class TestOpenEJovianRESTAPI(test.TestCase): jrest.attach_target_vol, tname, vname) # no such target + jrest, ctx = self.get_rest(CONFIG_OK) + jbody = {"name": vname, "lun": 0} + url = 'http://85.14.118.246:11582/api/v3/pools/Pool-0/{}'.format(addr) msg = 'Target {} not exists.'.format(vname) err = {"class": "opene.exceptions.ItemNotFoundError", @@ -904,12 +939,14 @@ class TestOpenEJovianRESTAPI(test.TestCase): 'code': 404} jrest.rproxy.pool_request.return_value = resp - attach_target_vol_expected += [ + attach_target_vol_expected = [ mock.call('POST', addr, json_data=jbody)] self.assertRaises(jexc.JDSSResourceNotFoundException, jrest.attach_target_vol, tname, vname) # error unknown + jrest, ctx = self.get_rest(CONFIG_OK) + jbody = {"name": vname, "lun": 0} url = 'http://85.14.118.246:11582/api/v3/pools/Pool-0/{}'.format(addr) msg = 'Target {} not exists.'.format(vname) @@ -923,17 +960,28 @@ class TestOpenEJovianRESTAPI(test.TestCase): 'code': 500} jrest.rproxy.pool_request.return_value = resp - attach_target_vol_expected += [ + attach_target_vol_expected = [ mock.call('POST', addr, json_data=jbody)] self.assertRaises(jexc.JDSSException, jrest.attach_target_vol, tname, vname) jrest.rproxy.pool_request.assert_has_calls(attach_target_vol_expected) + # error incorrect mode + jrest, ctx = self.get_rest(CONFIG_OK) + jbody = {"name": vname, "lun": 0} + url = 'http://85.14.118.246:11582/api/v3/pools/Pool-0/{}'.format(addr) + + attach_target_vol_expected = [ + mock.call('POST', addr, json_data=jbody)] + self.assertRaises(jexc.JDSSException, + jrest.attach_target_vol, tname, vname, mode='bad') + jrest.rproxy.pool_request.assert_not_called() + def test_detach_target_vol(self): jrest, ctx = self.get_rest(CONFIG_OK) # detach target vol ok - tname = CONFIG_OK['iscsi_target_prefix'] + UUID_1 + tname = CONFIG_OK['target_prefix'] + UUID_1 vname = jcom.vname(UUID_1) addr = '/san/iscsi/targets/{tar}/luns/{vol}'.format( @@ -989,9 +1037,9 @@ class TestOpenEJovianRESTAPI(test.TestCase): jrest, ctx = self.get_rest(CONFIG_OK) vname = jcom.vname(UUID_1) - sname = jcom.sname(UUID_2) + sname = jcom.sname(UUID_S1, UUID_1) - data = {'name': jcom.sname(UUID_2)} + data = {'name': jcom.sname(UUID_S2, UUID_1)} resp = {'data': data, 'error': None, 'code': 201} @@ -1003,7 +1051,7 @@ class TestOpenEJovianRESTAPI(test.TestCase): jrest, ctx = self.get_rest(CONFIG_OK) vname = jcom.vname(UUID_1) - sname = jcom.sname(UUID_2) + sname = jcom.sname(UUID_S1, UUID_1) addr = '/volumes/{vol}/snapshots'.format(vol=vname) req = {'snapshot_name': sname} @@ -1066,7 +1114,7 @@ class TestOpenEJovianRESTAPI(test.TestCase): jrest, ctx = self.get_rest(CONFIG_OK) vname = jcom.vname(UUID_1) - sname = jcom.sname(UUID_2) + sname = jcom.sname(UUID_S1, UUID_1) cname = jcom.vname(UUID_3) addr = '/volumes/{vol}/clone'.format(vol=vname) @@ -1101,7 +1149,7 @@ class TestOpenEJovianRESTAPI(test.TestCase): jrest, ctx = self.get_rest(CONFIG_OK) vname = jcom.vname(UUID_1) - sname = jcom.sname(UUID_2) + sname = jcom.sname(UUID_S2, UUID_2) cname = jcom.vname(UUID_3) addr = '/volumes/{vol}/clone'.format(vol=vname) @@ -1177,7 +1225,7 @@ class TestOpenEJovianRESTAPI(test.TestCase): jrest, ctx = self.get_rest(CONFIG_OK) vname = jcom.vname(UUID_1) - sname = jcom.sname(UUID_2) + sname = jcom.sname(UUID_S2, UUID_2) req = ('/volumes/{vol}/snapshots/' '{snap}/rollback').format(vol=vname, snap=sname) @@ -1198,7 +1246,7 @@ class TestOpenEJovianRESTAPI(test.TestCase): jrest, ctx = self.get_rest(CONFIG_OK) vname = jcom.vname(UUID_1) - sname = jcom.sname(UUID_2) + sname = jcom.sname(UUID_S2, UUID_2) req = ('/volumes/{vol}/snapshots/' '{snap}/rollback').format(vol=vname, @@ -1253,13 +1301,12 @@ class TestOpenEJovianRESTAPI(test.TestCase): jrest, ctx = self.get_rest(CONFIG_OK) vname = jcom.vname(UUID_1) - sname = jcom.sname(UUID_2) + sname = jcom.sname(UUID_S2, UUID_2) addr = '/volumes/{vol}/snapshots/{snap}'.format(vol=vname, snap=sname) jbody = { 'recursively_children': True, - 'recursively_dependents': True, 'force_umount': True } @@ -1268,7 +1315,7 @@ class TestOpenEJovianRESTAPI(test.TestCase): 'code': 204} jrest.rproxy.pool_request.return_value = resp - delete_snapshot_expected = [mock.call('DELETE', addr)] + delete_snapshot_expected = [mock.call('DELETE', addr, json_data={})] self.assertIsNone(jrest.delete_snapshot(vname, sname)) delete_snapshot_expected += [ @@ -1276,7 +1323,6 @@ class TestOpenEJovianRESTAPI(test.TestCase): self.assertIsNone(jrest.delete_snapshot(vname, sname, recursively_children=True, - recursively_dependents=True, force_umount=True)) jrest.rproxy.pool_request.assert_has_calls(delete_snapshot_expected) @@ -1285,8 +1331,8 @@ class TestOpenEJovianRESTAPI(test.TestCase): jrest, ctx = self.get_rest(CONFIG_OK) vname = jcom.vname(UUID_1) - sname = jcom.sname(UUID_2) - cname = jcom.sname(UUID_3) + sname = jcom.sname(UUID_S2, UUID_1) + cname = jcom.sname(UUID_S3, UUID_1) addr = '/volumes/{vol}/snapshots/{snap}'.format(vol=vname, snap=sname) @@ -1307,7 +1353,7 @@ class TestOpenEJovianRESTAPI(test.TestCase): jrest.rproxy.pool_request.return_value = resp delete_snapshot_expected = [ - mock.call('DELETE', addr)] + mock.call('DELETE', addr, json_data={})] self.assertRaises(jexc.JDSSSnapshotIsBusyException, jrest.delete_snapshot, @@ -1325,7 +1371,7 @@ class TestOpenEJovianRESTAPI(test.TestCase): 'code': 500} jrest.rproxy.pool_request.return_value = resp - delete_snapshot_expected += [mock.call('DELETE', addr)] + delete_snapshot_expected += [mock.call('DELETE', addr, json_data={})] self.assertRaises(jexc.JDSSException, jrest.delete_snapshot, vname, sname) @@ -1340,7 +1386,7 @@ class TestOpenEJovianRESTAPI(test.TestCase): data = {"results": 2, "entries": {"referenced": "65536", - "name": jcom.sname(UUID_2), + "name": jcom.sname(UUID_S1, UUID_1), "defer_destroy": "off", "userrefs": "0", "primarycache": "all", diff --git a/cinder/tests/unit/volume/drivers/open_e/test_rest_proxy.py b/cinder/tests/unit/volume/drivers/open_e/test_rest_proxy.py index 49d63d31a0b..04a71ca32c9 100644 --- a/cinder/tests/unit/volume/drivers/open_e/test_rest_proxy.py +++ b/cinder/tests/unit/volume/drivers/open_e/test_rest_proxy.py @@ -41,7 +41,7 @@ CONFIG_OK = { 'jovian_ignore_tpath': [], 'target_port': 3260, 'jovian_pool': 'Pool-0', - 'iscsi_target_prefix': 'iqn.2020-04.com.open-e.cinder:', + 'target_prefix': 'iqn.2020-04.com.open-e.cinder:', 'chap_password_len': 12, 'san_thin_provision': False, 'jovian_block_size': '128K' @@ -61,7 +61,7 @@ CONFIG_BAD_IP = { 'jovian_ignore_tpath': [], 'target_port': 3260, 'jovian_pool': 'Pool-0', - 'iscsi_target_prefix': 'iqn.2020-04.com.open-e.cinder:', + 'target_prefix': 'iqn.2020-04.com.open-e.cinder:', 'chap_password_len': 12, 'san_thin_provision': False, 'jovian_block_size': '128K' @@ -81,7 +81,7 @@ CONFIG_MULTIHOST = { 'jovian_ignore_tpath': [], 'target_port': 3260, 'jovian_pool': 'Pool-0', - 'iscsi_target_prefix': 'iqn.2020-04.com.open-e.cinder:', + 'target_prefix': 'iqn.2020-04.com.open-e.cinder:', 'chap_password_len': 12, 'san_thin_provision': False, 'jovian_block_size': '128K' @@ -102,12 +102,12 @@ class TestOpenEJovianRESTProxy(test.TestCase): def test_init(self): self.assertRaises(exception.InvalidConfigurationValue, - rest_proxy.JovianRESTProxy, + rest_proxy.JovianDSSRESTProxy, CONFIG_BAD_IP) def test_get_base_url(self): - proxy = rest_proxy.JovianRESTProxy(CONFIG_OK) + proxy = rest_proxy.JovianDSSRESTProxy(CONFIG_OK) url = proxy._get_base_url() @@ -119,7 +119,7 @@ class TestOpenEJovianRESTProxy(test.TestCase): def test_next_host(self): - proxy = rest_proxy.JovianRESTProxy(CONFIG_MULTIHOST) + proxy = rest_proxy.JovianDSSRESTProxy(CONFIG_MULTIHOST) self.assertEqual(0, proxy.active_host) proxy._next_host() @@ -134,7 +134,7 @@ class TestOpenEJovianRESTProxy(test.TestCase): def test_request(self): - proxy = rest_proxy.JovianRESTProxy(CONFIG_MULTIHOST) + proxy = rest_proxy.JovianDSSRESTProxy(CONFIG_MULTIHOST) patches = [ mock.patch.object(requests, "Request", return_value="request"), @@ -153,7 +153,7 @@ class TestOpenEJovianRESTProxy(test.TestCase): def test_request_host_failure(self): - proxy = rest_proxy.JovianRESTProxy(CONFIG_MULTIHOST) + proxy = rest_proxy.JovianDSSRESTProxy(CONFIG_MULTIHOST) patches = [ mock.patch.object(requests, "Request", return_value="request"), @@ -185,7 +185,7 @@ class TestOpenEJovianRESTProxy(test.TestCase): def test_pool_request(self): - proxy = rest_proxy.JovianRESTProxy(CONFIG_OK) + proxy = rest_proxy.JovianDSSRESTProxy(CONFIG_OK) patches = [mock.patch.object(proxy, "request")] @@ -199,7 +199,7 @@ class TestOpenEJovianRESTProxy(test.TestCase): def test_send(self): - proxy = rest_proxy.JovianRESTProxy(CONFIG_MULTIHOST) + proxy = rest_proxy.JovianDSSRESTProxy(CONFIG_MULTIHOST) json_data = {"data": [{"available": "949998694400", "status": 26, @@ -236,70 +236,52 @@ class TestOpenEJovianRESTProxy(test.TestCase): self.assertEqual(json_data['error'], ret['error']) self.stop_patches(patches) - def test_send_connection_error(self): - - proxy = rest_proxy.JovianRESTProxy(CONFIG_MULTIHOST) - - json_data = {"data": None, - "error": None} - - session_ret = mock.Mock() - session_ret.text = json.dumps(json_data) - session_ret.status_code = 200 - patches = [mock.patch.object(proxy.session, "send")] - - pr = 'prepared_request' - + def test_request_host_change(self): + proxy = rest_proxy.JovianDSSRESTProxy(CONFIG_MULTIHOST) + patches = [ + mock.patch.object(requests, "Request", return_value="request"), + mock.patch.object(proxy.session, + "prepare_request", + return_value="out_data"), + mock.patch.object(proxy, "_send", return_value="out_data")] + request_expected = [ + mock.call('GET', + 'https://192.168.0.2:82/api/v3/pools/Pool-0'), + mock.call('GET', + 'https://192.168.0.3:82/api/v3/pools/Pool-0'), + mock.call('GET', + 'https://192.168.0.4:82/api/v3/pools/Pool-0'), + mock.call('GET', + 'https://192.168.0.2:82/api/v3/pools/Pool-0')] self.start_patches(patches) - - side_effect = [requests.exceptions.ConnectionError()] * 4 - side_effect += [session_ret] - - proxy.session.send.side_effect = side_effect - - send_expected = [mock.call(pr)] * 4 - - ret = proxy._send(pr) - - proxy.session.send.assert_has_calls(send_expected) - + proxy._send.side_effect = [ + requests.exceptions.ConnectionError(), + requests.exceptions.ConnectionError(), + requests.exceptions.ConnectionError(), + "out_data"] + proxy.request('GET', '/pools/Pool-0') self.assertEqual(0, proxy.active_host) - - self.assertEqual(200, ret['code']) - self.assertEqual(json_data['data'], ret['data']) - self.assertEqual(json_data['error'], ret['error']) + requests.Request.assert_has_calls(request_expected) self.stop_patches(patches) - def test_send_mixed_error(self): - - proxy = rest_proxy.JovianRESTProxy(CONFIG_MULTIHOST) - - json_data = {"data": None, - "error": None} + def test_send_jsondecode_error(self): + proxy = rest_proxy.JovianDSSRESTProxy(CONFIG_MULTIHOST) session_ret = mock.Mock() - session_ret.text = json.dumps(json_data) + session_ret.text = "{ some-bad-json" session_ret.status_code = 200 - patches = [mock.patch.object(proxy.session, "send")] + patches = [mock.patch.object(proxy.session, "send")] pr = 'prepared_request' self.start_patches(patches) - - side_effect = [requests.exceptions.ConnectionError()] * 4 - side_effect += [jexc.JDSSOSException()] * 4 - side_effect += [session_ret] - + side_effect = [session_ret] * 3 proxy.session.send.side_effect = side_effect + send_expected = [mock.call(pr)] * 3 - send_expected = [mock.call(pr)] * 7 - - self.assertRaises(jexc.JDSSOSException, proxy._send, pr) - + self.assertRaises(json.JSONDecodeError, proxy._send, pr) proxy.session.send.assert_has_calls(send_expected) - self.assertEqual(0, proxy.active_host) - def test_handle_500(self): error = {"class": "exceptions.OSError", @@ -314,7 +296,7 @@ class TestOpenEJovianRESTProxy(test.TestCase): session_ret.status_code = 500 self.assertRaises(jexc.JDSSOSException, - rest_proxy.JovianRESTProxy._handle_500, + rest_proxy.JovianDSSRESTProxy._handle_500, session_ret) session_ret.status_code = 200 @@ -322,4 +304,5 @@ class TestOpenEJovianRESTProxy(test.TestCase): "error": None} session_ret.text = json.dumps(json_data) - self.assertIsNone(rest_proxy.JovianRESTProxy._handle_500(session_ret)) + self.assertIsNone( + rest_proxy.JovianDSSRESTProxy._handle_500(session_ret)) diff --git a/cinder/volume/drivers/open_e/iscsi.py b/cinder/volume/drivers/open_e/iscsi.py index 3117b6342d2..d92537a0876 100644 --- a/cinder/volume/drivers/open_e/iscsi.py +++ b/cinder/volume/drivers/open_e/iscsi.py @@ -25,6 +25,7 @@ from cinder import exception from cinder.i18n import _ from cinder import interface from cinder.volume import driver +from cinder.volume.drivers.open_e.jovian_common import driver as jdriver from cinder.volume.drivers.open_e.jovian_common import exception as jexc from cinder.volume.drivers.open_e.jovian_common import jdss_common as jcom from cinder.volume.drivers.open_e.jovian_common import rest @@ -32,6 +33,7 @@ from cinder.volume.drivers.open_e import options from cinder.volume.drivers.san import san from cinder.volume import volume_utils + LOG = logging.getLogger(__name__) @@ -48,11 +50,14 @@ class JovianISCSIDriver(driver.ISCSIDriver): Added revert to snapshot support 1.0.2 - Added multi-attach support Added 16K block support + 1.0.3 - Driver rework and optimisation + Abandon recursive volume deletion + Removed revert to snapshot support """ - # ThirdPartySystems wiki page + # Third-party Systems wiki page CI_WIKI_NAME = "Open-E_JovianDSS_CI" - VERSION = "1.0.2" + VERSION = "1.0.3" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -66,6 +71,7 @@ class JovianISCSIDriver(driver.ISCSIDriver): self.jovian_hosts = None self._pool = 'Pool-0' self.ra = None + self.driver = None @property def backend_name(self): @@ -88,10 +94,6 @@ class JovianISCSIDriver(driver.ISCSIDriver): options.jdss_volume_opts) self.configuration.append_config_values(san.san_opts) - self._pool = self.configuration.get('jovian_pool', 'Pool-0') - self.jovian_iscsi_target_portal_port = self.configuration.get( - 'target_port', 3260) - self.jovian_target_prefix = self.configuration.get( 'target_prefix', 'iqn.2020-04.com.open-e.cinder:') @@ -107,8 +109,7 @@ class JovianISCSIDriver(driver.ISCSIDriver): 'san_hosts', []) self.ra = rest.JovianRESTAPI(self.configuration) - - self.check_for_setup_error() + self.driver = jdriver.JovianDSSDriver(self.configuration) def check_for_setup_error(self): """Check for setup error.""" @@ -116,7 +117,7 @@ class JovianISCSIDriver(driver.ISCSIDriver): msg = _("No hosts provided in configuration") raise exception.VolumeDriverException(msg) - if not self.ra.is_pool_exists(): + if not self.driver.rest_config_is_ok(): msg = (_("Unable to identify pool %s") % self._pool) raise exception.VolumeDriverException(msg) @@ -128,10 +129,10 @@ class JovianISCSIDriver(driver.ISCSIDriver): def _get_target_name(self, volume_name): """Return iSCSI target name to access volume.""" - return '%s%s' % (self.jovian_target_prefix, volume_name) + return f'{self.jovian_target_prefix}{volume_name}' def _get_active_ifaces(self): - """Return list of ip addreses for iSCSI connection""" + """Return list of ip addresses for iSCSI connection""" return self.jovian_hosts @@ -141,119 +142,21 @@ class JovianISCSIDriver(driver.ISCSIDriver): :param volume: volume reference :return: model update dict for volume reference """ - vname = jcom.vname(volume.id) - LOG.debug('creating volume %s.', vname) - - provider_location = self._get_provider_location(volume.id) - provider_auth = self._get_provider_auth() + LOG.debug('creating volume %s.', volume.id) try: - self.ra.create_lun(vname, - volume.size * o_units.Gi, - sparse=self.jovian_sparse, - block_size=self.block_size) + self.driver.create_volume(volume.id, + volume.size, + sparse=self.jovian_sparse, + block_size=self.block_size) except jexc.JDSSException as jerr: LOG.error("Create volume error. Because %(err)s", {"err": jerr}) raise exception.VolumeBackendAPIException( _('Failed to create volume %s.') % volume.id) from jerr - ret = {} - if provider_auth is not None: - ret['provider_auth'] = provider_auth - ret['provider_location'] = provider_location - - return ret - - def _hide_object(self, vname): - """Mark volume/snapshot as hidden - - :param vname: physical volume name - """ - rename = {'name': jcom.hidden(vname)} - try: - self.ra.modify_lun(vname, rename) - except jexc.JDSSException as jerr: - emsg = _('Failure in hiding %(object)s, err: %(error)s,' - ' object has to be removed manually') % {'object': vname, - 'error': jerr} - LOG.warning(emsg) - raise exception.VolumeBackendAPIException(emsg) - - def _clean_garbage_snapshots(self, vname, snapshots): - """Delete physical snapshots that have no descendents""" - garbage = [] - for snap in snapshots: - if snap['clones'] == '': - try: - self.ra.delete_snapshot(vname, snap['name']) - except jexc.JDSSException as jerr: - args = {'obj': jcom.idname(vname), 'err': jerr} - msg = (_("Unable to clean garbage for " - "%(obj)s: %(err)s") % args) - raise exception.VolumeBackendAPIException(msg) - garbage.append(snap) - for snap in garbage: - snapshots.remove(snap) - - return snapshots - - def _cascade_volume_delete(self, o_vname, o_snaps): - """Delete or hides volume(if it is busy) - - Go over snapshots and deletes them if possible - Calls for recursive volume deletion if volume do not have children - """ - vsnaps = [] - deletable = True - - for snap in o_snaps: - if jcom.is_snapshot(snap['name']): - vsnaps += [(snap['name'], - jcom.full_name_volume(snap['clones']))] - - active_vsnaps = [vs for vs in vsnaps if jcom.is_hidden(vs[1]) is False] - - # If volume have clones or hidden snapshots it should be hidden - if len(active_vsnaps) < len(o_snaps): - deletable = False - - for vsnap in active_vsnaps: - psnap = [] - try: - psnap = self.ra.get_snapshots(vsnap[1]) - except jexc.JDSSException as jerr: - msg = (_('Failure in acquiring snapshot for %s.') % vsnap[1]) - raise exception.VolumeBackendAPIException(msg) from jerr - - try: - psnap = self._clean_garbage_snapshots(vsnap[1], psnap) - except exception.VolumeBackendAPIException as err: - msg = (_('Failure in cleaning garbage snapshots %s' - ' for volume %s, %s') % psnap, vsnap[1], err) - raise exception.VolumeBackendAPIException(msg) from err - if len(psnap) > 0: - deletable = False - self._hide_object(vsnap[1]) - else: - try: - self.ra.delete_snapshot(o_vname, - vsnap[0], - recursively_children=True, - recursively_dependents=True, - force_umount=True) - except jexc.JDSSException as jerr: - LOG.warning('Failure during deletion of physical ' - 'snapshot %s, err: %s', vsnap[0], jerr) - msg = (_('Failure during deletion of virtual snapshot ' - '%s') % vsnap[1]) - raise exception.VolumeBackendAPIException(msg) - - if deletable: - self._gc_delete(o_vname) - else: - self._hide_object(o_vname) + return self._get_provider_info(volume.id) def delete_volume(self, volume, cascade=False): """Delete volume @@ -261,252 +164,25 @@ class JovianISCSIDriver(driver.ISCSIDriver): :param volume: volume reference :param cascade: remove snapshots of a volume as well """ - vname = jcom.vname(volume.id) - LOG.debug('deleating volume %s', vname) - - snapshots = None try: - snapshots = self.ra.get_snapshots(vname) - except jexc.JDSSResourceNotFoundException: - LOG.debug('volume %s dne, it was already ' - 'deleted', vname) - return + self.driver.delete_volume(volume.id, cascade=cascade) except jexc.JDSSException as jerr: raise exception.VolumeBackendAPIException(jerr) - snapshots = self._clean_garbage_snapshots(vname, snapshots) - - if cascade: - self._cascade_volume_delete(vname, snapshots) - else: - if len(snapshots) > 0: - self._hide_object(vname) - else: - self._gc_delete(vname) - - def _gc_delete(self, vname): - """Delete volume and its hidden parents - - Deletes volume by going recursively to the first active - parent and cals recursive deletion on storage side - """ - vol = None - try: - vol = self.ra.get_lun(vname) - except jexc.JDSSResourceNotFoundException: - LOG.debug('volume %s does not exist, it was already ' - 'deleted.', vname) - return - except jexc.JDSSException as jerr: - raise exception.VolumeBackendAPIException(jerr) - - if vol['is_clone']: - self._delete_back_recursively(jcom.origin_volume(vol['origin']), - jcom.origin_snapshot(vol['origin'])) - else: - try: - self.ra.delete_lun(vname, force_umount=True) - except jexc.JDSSRESTException as jerr: - LOG.debug( - "Unable to delete physical volume %(volume)s " - "with error %(err)s.", { - "volume": vname, - "err": jerr}) - raise exception.VolumeIsBusy(jerr) - - def _delete_back_recursively(self, opvname, opsname): - """Deletes snapshot by removing its oldest removable parent - - Checks if source volume for this snapshot is hidden: - If it is hidden and have no other descenents, it calls itself on its - source snapshot if such exists, or deletes it - If it is not hidden, trigers delete for snapshot - - :param opvname: origin physical volume name - :param opsname: origin physical snapshot name - """ - - if jcom.is_hidden(opvname): - # Resource is hidden - snaps = [] - try: - snaps = self.ra.get_snapshots(opvname) - except jexc.JDSSResourceNotFoundException: - LOG.debug('Unable to get physical snapshots related to' - ' physical volume %s, volume do not exist', - opvname) - return - except jexc.JDSSException as jerr: - raise exception.VolumeBackendAPIException(jerr) - - snaps = self._clean_garbage_snapshots(opvname, snaps) - - if len(snaps) > 1: - # opvname has active snapshots and cant be deleted - # that is why we delete branch related to opsname - try: - self.ra.delete_snapshot(opvname, - opsname, - recursively_children=True, - recursively_dependents=True, - force_umount=True) - except jexc.JDSSException as jerr: - raise exception.VolumeBackendAPIException(jerr) - else: - vol = None - try: - vol = self.ra.get_lun(opvname) - - except jexc.JDSSResourceNotFoundException: - LOG.debug('volume %s does not exist, it was already ' - 'deleted.', opvname) - return - except jexc.JDSSException as jerr: - raise exception.VolumeBackendAPIException(jerr) - - if vol['is_clone']: - self._delete_back_recursively( - jcom.origin_volume(vol['origin']), - jcom.origin_snapshot(vol['origin'])) - else: - try: - self.ra.delete_lun(opvname, - recursively_children=True, - recursively_dependents=True, - force_umount=True) - except jexc.JDSSResourceNotFoundException: - LOG.debug('volume %s does not exist, it was already ' - 'deleted.', opvname) - return - except jexc.JDSSException as jerr: - raise exception.VolumeBackendAPIException(jerr) - else: - # Resource is active - try: - self.ra.delete_snapshot(opvname, - opsname, - recursively_children=True, - recursively_dependents=True, - force_umount=True) - except jexc.JDSSException as jerr: - raise exception.VolumeBackendAPIException(jerr) from jerr - def extend_volume(self, volume, new_size): """Extend an existing volume. :param volume: volume reference :param new_size: volume new size in GB """ - LOG.debug("Extend volume %s", volume.id) try: - self.ra.extend_lun(jcom.vname(volume.id), - new_size * o_units.Gi) + self.driver.resize_volume(volume.id, new_size) except jexc.JDSSException as jerr: + msg = _('Failed to extend volume %s.') raise exception.VolumeBackendAPIException( - (_('Failed to extend volume %s.'), volume.id)) from jerr - - def revert_to_snapshot(self, context, volume, snapshot): - """Revert volume to snapshot. - - Note: the revert process should not change the volume's - current size, that means if the driver shrank - the volume during the process, it should extend the - volume internally. - """ - vname = jcom.vname(volume.id) - sname = jcom.sname(snapshot.id) - LOG.debug('reverting %(vname)s to %(sname)s', { - "vname": vname, - "sname": sname}) - - vsize = None - try: - vsize = self.ra.get_lun(vname).get('volsize') - except jexc.JDSSResourceNotFoundException as jerr: - raise exception.VolumeNotFound(volume_id=volume.id) from jerr - except jexc.JDSSException as jerr: - raise exception.VolumeBackendAPIException(jerr) from jerr - - if vsize is None: - raise exception.VolumeDriverException( - _("unable to identify volume size")) - - try: - self.ra.rollback_volume_to_snapshot(vname, sname) - except jexc.JDSSException as jerr: - raise exception.VolumeBackendAPIException(jerr.message) from jerr - - try: - rvsize = self.ra.get_lun(vname).get('volsize') - if rvsize != vsize: - self.ra.extend_lun(vname, vsize) - except jexc.JDSSResourceNotFoundException as jerr: - raise exception.VolumeNotFound(volume_id=volume.id) from jerr - except jexc.JDSSException as jerr: - raise exception.VolumeBackendAPIException(jerr) from jerr - - def _clone_object(self, oname, coname): - """Creates a clone of specified object - - :param: oname: name of an object to clone - :param: coname: name of a new clone - """ - LOG.debug('cloning %(oname)s to %(coname)s', { - "oname": oname, - "coname": coname}) - - try: - self.ra.create_snapshot(oname, coname) - except jexc.JDSSSnapshotExistsException: - try: - self.ra.delete_snapshot(oname, coname) - except jexc.JDSSSnapshotIsBusyException as jerrisbusy: - raise exception.Duplicate() from jerrisbusy - except jexc.JDSSException as jerr: - raise exception.VolumeBackendAPIException( - (_("Unable to create volume %s.") % coname)) from jerr - except jexc.JDSSResourceNotFoundException as jerrnotfound: - if jcom.is_volume(oname): - raise exception.VolumeNotFound( - volume_id=jcom.idname(oname)) from jerrnotfound - raise exception.SnapshotNotFound( - snapshot_id=jcom.idname(oname)) from jerrnotfound - - except jexc.JDSSException as jerr: - args = {'snapshot': coname, - 'object': oname, - 'err': jerr} - msg = (_('Failed to create tmp snapshot %(snapshot)s ' - 'for object %(object)s: %(err)s') % args) - raise exception.VolumeBackendAPIException(msg) - - try: - self.ra.create_volume_from_snapshot( - coname, - coname, - oname, - sparse=self.jovian_sparse) - except jexc.JDSSResourceExistsException as jerr: - raise exception.Duplicate() from jerr - except jexc.JDSSException as jerr: - try: - self.ra.delete_snapshot(oname, - coname, - recursively_children=True, - recursively_dependents=True, - force_umount=True) - except jexc.JDSSException as jerrd: - LOG.warning("Because of %s physical snapshot %s of volume" - " %s have to be removed manually", - jerrd, - coname, - oname) - - raise exception.VolumeBackendAPIException( - _("Unable to create volume %(vol)s because of %(err)s.") % { - 'vol': coname, 'err': jerr}) from jerr + data=msg % volume.id) from jerr def create_cloned_volume(self, volume, src_vref): """Create a clone of the specified volume. @@ -514,156 +190,104 @@ class JovianISCSIDriver(driver.ISCSIDriver): :param volume: new volume reference :param src_vref: source volume reference """ - cvname = jcom.vname(volume.id) - - vname = jcom.vname(src_vref.id) - - LOG.debug('cloned volume %(id)s to %(id_clone)s', { - "id": src_vref.id, - "id_clone": volume.id}) - - self._clone_object(vname, cvname) - - clone_size = 0 try: - clone_size = int(self.ra.get_lun(cvname)['volsize']) + self.driver.create_cloned_volume(volume.id, + src_vref.id, + volume.size, + sparse=self.jovian_sparse) except jexc.JDSSException as jerr: + msg = _("Fail to clone volume %(vol)s to %(clone)s because of " + "error %(err)s.") % { + 'vol': src_vref.id, + 'clone': volume.id, + 'err': jerr} + raise exception.VolumeBackendAPIException(msg) from jerr - self._delete_back_recursively(vname, cvname) - raise exception.VolumeBackendAPIException( - _("Fail in cloning volume %(vol)s to %(clone)s.") % { - 'vol': src_vref.id, 'clone': volume.id}) from jerr - - try: - if int(clone_size) < o_units.Gi * int(volume.size): - self.extend_volume(volume, int(volume.size)) - - except exception.VolumeBackendAPIException: - # If volume can't be set to a proper size make sure to clean it - # before failing - try: - self._delete_back_recursively(cvname, cvname) - except exception.VolumeBackendAPIException as err: - LOG.warning("Because of %s physical snapshot %s of volume" - " %s have to be removed manually", - err, - cvname, - vname) - raise - - provider_location = self._get_provider_location(volume.id) - provider_auth = self._get_provider_auth() - - ret = {} - if provider_auth: - ret['provider_auth'] = provider_auth - - ret['provider_location'] = provider_location - - return ret + return self._get_provider_info(volume.id) def create_volume_from_snapshot(self, volume, snapshot): """Create a volume from a snapshot. - If volume_type extra specs includes 'replication: True' - the driver needs to create a volume replica (secondary), - and setup replication between the newly created volume and - the secondary volume. """ LOG.debug('create volume %(vol)s from snapshot %(snap)s', { 'vol': volume.id, 'snap': snapshot.id}) - cvname = jcom.vname(volume.id) - sname = jcom.sname(snapshot.id) - - self._clone_object(sname, cvname) - - clone_size = 0 - try: - clone_size = int(self.ra.get_lun(cvname)['volsize']) + self.driver.create_cloned_volume(volume.id, + snapshot.volume_id, + volume.size, + snapshot_name=snapshot.id) + except jexc.JDSSResourceExistsException as jerr: + raise exception.Duplicate() from jerr except jexc.JDSSException as jerr: - - self._delete_back_recursively(sname, cvname) raise exception.VolumeBackendAPIException( - _("Fail in cloning snapshot %(snap)s to %(clone)s.") % { - 'snap': snapshot.id, 'clone': volume.id}) from jerr + _("Failed to create clone %(clone)s from snapshot %(snap)s " + "of volume %(vol)s because of error %(err)s.") % { + 'vol': snapshot.volume_id, + 'clone': volume.id, + 'snap': snapshot.id, + 'err': jerr}) from jerr - try: - if clone_size < o_units.Gi * int(volume.size): - self.extend_volume(volume, int(volume.size)) - except exception.VolumeBackendAPIException: - # If volume can't be set to a proper size make sure to clean it - # before failing - try: - self._delete_back_recursively(cvname, cvname) - except exception.VolumeBackendAPIException as err: - msg = ("Hidden snapshot %s of volume %s " - "have to be removed manualy, " - "as automatic removal failed: %s") - LOG.warning(msg, cvname, sname, err) - raise - - provider_location = self._get_provider_location(volume.id) - provider_auth = self._get_provider_auth() - - ret = {} - if provider_auth is not None: - ret['provider_auth'] = provider_auth - - ret['provider_location'] = provider_location - - return ret + return self._get_provider_info(volume.id) def create_snapshot(self, snapshot): """Create snapshot of existing volume. - :param snapshot: snapshot reference + :param snapshot: snapshot object """ LOG.debug('create snapshot %(snap)s for volume %(vol)s', { 'snap': snapshot.id, 'vol': snapshot.volume_id}) - vname = jcom.vname(snapshot.volume_id) - sname = jcom.sname(snapshot.id) - - self._clone_object(vname, sname) - try: - self.ra.make_readonly_lun(sname) - except jexc.JDSSException as err: - # Name of snapshot should be the same as a name of volume - # that is going to be created from it - self._delete_back_recursively(vname, sname) - raise exception.VolumeBackendAPIException(err) + self.driver.create_snapshot(snapshot.id, snapshot.volume_id) + except jexc.JDSSSnapshotExistsException as jexistserr: + raise exception.Duplicate() from jexistserr + except jexc.JDSSResourceNotFoundException as jerrnotfound: + raise exception.VolumeNotFound( + volume_id=jcom.idname(snapshot.volume_id)) from jerrnotfound + except jexc.JDSSException as jerr: + args = {'snapshot': snapshot.id, + 'object': snapshot.volume_id, + 'err': jerr} + msg = (_('Failed to create tmp snapshot %(snapshot)s ' + 'for object %(object)s: %(err)s') % args) + raise exception.VolumeBackendAPIException(msg) from jerr + + return self._get_provider_info(snapshot.id) def delete_snapshot(self, snapshot): """Delete snapshot of existing volume. :param snapshot: snapshot reference """ - sname = jcom.sname(snapshot.id) - LOG.debug('deleating snapshot %s.', sname) - - snapshots = None try: - snapshots = self.ra.get_snapshots(sname) + self.driver.delete_snapshot(snapshot.volume_id, snapshot.id) except jexc.JDSSResourceNotFoundException: - LOG.debug('physical volume %s dne, it was already ' - 'deleted.', sname) return except jexc.JDSSException as jerr: raise exception.VolumeBackendAPIException(jerr) - snapshots = self._clean_garbage_snapshots(sname, snapshots) + def _get_provider_info(self, vid): + '''returns provider info dict - if len(snapshots) > 0: - self._hide_object(sname) - else: - self._gc_delete(sname) + :param vid: volume id + ''' + + info = {} + try: + info['provider_location'] = self.driver.get_provider_location(vid) + except jexc.JDSSException as jerr: + msg = _("Fail to identify critical properties of " + "new volume %s.") % vid + raise exception.VolumeBackendAPIException(data=msg) from jerr + + info['provider_auth'] = self._get_provider_auth() + + return info def _get_provider_auth(self): """Get provider authentication for the volume. @@ -683,13 +307,6 @@ class JovianISCSIDriver(driver.ISCSIDriver): return 'CHAP %(user)s %(passwd)s' % { 'user': chap_user, 'passwd': chap_password} - def _get_provider_location(self, volume_name): - """Return volume iscsiadm-formatted provider location string.""" - return '%(host)s:%(port)s,1 %(name)s 0' % { - 'host': self.ra.get_active_host(), - 'port': self.jovian_iscsi_target_portal_port, - 'name': self._get_target_name(volume_name)} - def create_export(self, _ctx, volume, connector): """Create new export for zvol. @@ -698,9 +315,21 @@ class JovianISCSIDriver(driver.ISCSIDriver): """ LOG.debug("create export for volume: %s.", volume.id) - self._ensure_target_volume(volume) + provider_auth = volume.provider_auth + ret = dict() - return {'provider_location': self._get_provider_location(volume.id)} + if provider_auth is None: + provider_auth = self._get_provider_auth() + ret['provider_auth'] = provider_auth + + try: + self.driver.ensure_export(volume.id, provider_auth) + location = self.driver.get_provider_location(volume.id) + ret['provider_location'] = location + except jexc.JDSSException as jerr: + raise exception.VolumeDriverException from jerr + + return ret def ensure_export(self, _ctx, volume): """Recreate parts of export if necessary. @@ -708,16 +337,55 @@ class JovianISCSIDriver(driver.ISCSIDriver): :param volume: reference of volume to be exported """ LOG.debug("ensure export for volume: %s.", volume.id) - self._ensure_target_volume(volume) + provider_auth = volume.provider_auth + ret = dict() + + if provider_auth is None: + provider_auth = self._get_provider_auth() + ret['provider_auth'] = provider_auth + try: + self.driver.ensure_export(volume.id, provider_auth) + location = self.driver.get_provider_location(volume.id) + ret['provider_location'] = location + except jexc.JDSSException as jerr: + raise exception.VolumeDriverException from jerr + return ret + + def create_export_snapshot(self, context, snapshot, connector): + provider_auth = snapshot.provider_auth + ret = dict() + + if provider_auth is None: + provider_auth = self._get_provider_auth() + ret['provider_auth'] = provider_auth + try: + ret = self.driver.create_export_snapshot(snapshot.id, + snapshot.volume_id, + provider_auth) + except jexc.JDSSResourceExistsException as jres_err: + raise exception.Duplicate() from jres_err + except jexc.JDSSException as jerr: + raise exception.VolumeDriverException from jerr + return ret def remove_export(self, _ctx, volume): """Destroy all resources created to export zvol. - :param volume: reference of volume to be unexported + :param volume: reference of volume to be unexposed """ LOG.debug("remove_export for volume: %s.", volume.id) - self._remove_target_volume(volume) + try: + self.driver.remove_export(volume.id) + except jexc.JDSSException as jerr: + raise exception.VolumeDriverException from jerr + + def remove_export_snapshot(self, context, snapshot): + try: + self.driver.remove_export_snapshot(snapshot.id, + snapshot.volume_id) + except jexc.JDSSException as jerr: + raise exception.VolumeDriverException from jerr def _update_volume_stats(self): """Retrieve stats info.""" @@ -737,7 +405,7 @@ class JovianISCSIDriver(driver.ISCSIDriver): location_info = '%(driver)s:%(host)s:%(volume)s' % { 'driver': self.__class__.__name__, - 'host': self.ra.get_active_host()[0], + 'host': self.ra.get_active_host(), 'volume': self._pool } @@ -759,200 +427,18 @@ class JovianISCSIDriver(driver.ISCSIDriver): self._stats['total_capacity_gb'], self._stats['free_capacity_gb']) - def _create_target(self, target_name, use_chap=True): - """Creates target and handles exceptions - - Tryes to create target. - :param target_name: name of target - :param use_chap: flag for using chap - """ - try: - self.ra.create_target(target_name, - use_chap=use_chap) - - except jexc.JDSSResourceExistsException as jerrex: - raise exception.Duplicate() from jerrex - except jexc.JDSSException as jerr: - - msg = (_('Unable to create target %(target)s ' - 'because of %(error)s.') % {'target': target_name, - 'error': jerr}) - raise exception.VolumeBackendAPIException(msg) - - def _attach_target_volume(self, target_name, vname): - """Attach target to volume and handles exceptions - - Tryes to set attach volume to specific target. - In case of failure will remve target. - :param target_name: name of target - :param use_chap: flag for using chap - """ - try: - self.ra.attach_target_vol(target_name, vname) - except jexc.JDSSException as jerr: - msg = ('Unable to attach volume to target {target} ' - 'because of {error}.') - emsg = msg.format(target=target_name, error=jerr) - LOG.debug(msg, {"target": target_name, "error": jerr}) - try: - self.ra.delete_target(target_name) - except jexc.JDSSException: - pass - raise exception.VolumeBackendAPIException(_(emsg)) - - def _set_target_credentials(self, target_name, cred): - """Set CHAP configuration for target and handle exceptions - - Tryes to set CHAP credentials for specific target. - In case of failure will remve target. - :param target_name: name of target - :param cred: CHAP user name and password - """ - try: - self.ra.create_target_user(target_name, cred) - - except jexc.JDSSException as jerr: - try: - self.ra.delete_target(target_name) - except jexc.JDSSException: - pass - - err_msg = (('Unable to create user %(user)s ' - 'for target %(target)s ' - 'because of %(error)s.') % { - 'target': target_name, - 'user': cred['name'], - 'error': jerr}) - - LOG.debug(err_msg) - - raise exception.VolumeBackendAPIException(_(err_msg)) - - def _create_target_volume(self, volume): - """Creates target and attach volume to it - - :param volume: volume id - :return: - """ - LOG.debug("create target and attach volume %s to it", volume.id) - - target_name = self.jovian_target_prefix + volume.id - vname = jcom.vname(volume.id) - - auth = volume.provider_auth - - if not auth: - msg = _("Volume %s is missing provider_auth") % volume.id - raise exception.VolumeDriverException(msg) - - (__, auth_username, auth_secret) = auth.split() - chap_cred = {"name": auth_username, - "password": auth_secret} - - # Create target - self._create_target(target_name, True) - - # Attach volume - self._attach_target_volume(target_name, vname) - - # Set credentials - self._set_target_credentials(target_name, chap_cred) - - def _ensure_target_volume(self, volume): - """Checks if target configured properly and volume is attached to it - - param: volume: volume structure - """ - LOG.debug("ensure volume %s assigned to a proper target", volume.id) - - target_name = self.jovian_target_prefix + volume.id - - auth = volume.provider_auth - - if not auth: - msg = _("volume %s is missing provider_auth") % volume.id - raise exception.VolumeDriverException(msg) - - (__, auth_username, auth_secret) = auth.split() - chap_cred = {"name": auth_username, - "password": auth_secret} - - if not self.ra.is_target(target_name): - self._create_target_volume(volume) - return - - vname = jcom.vname(volume.id) - if not self.ra.is_target_lun(target_name, vname): - self._attach_target_volume(target_name, vname) - - try: - users = self.ra.get_target_user(target_name) - if len(users) == 1: - if users[0]['name'] == chap_cred['name']: - return - self.ra.delete_target_user( - target_name, - users[0]['name']) - for user in users: - self.ra.delete_target_user( - target_name, - user['name']) - self._set_target_credentials(target_name, chap_cred) - - except jexc.JDSSException as jerr: - self.ra.delete_target(target_name) - raise exception.VolumeBackendAPIException(jerr) - - def _remove_target_volume(self, volume): - """_remove_target_volume - - Ensure that volume is not attached to target and target do not exists. - """ - target_name = self.jovian_target_prefix + volume.id - LOG.debug("remove export") - LOG.debug("detach volume:%(vol)s from target:%(targ)s.", { - 'vol': volume, - 'targ': target_name}) - - try: - self.ra.detach_target_vol(target_name, jcom.vname(volume.id)) - except jexc.JDSSResourceNotFoundException as jerrrnf: - LOG.debug('failed to remove resource %(t)s because of %(err)s', { - 't': target_name, - 'err': jerrrnf.args[0]}) - except jexc.JDSSException as jerr: - LOG.debug('failed to Terminate_connection for target %(targ)s ' - 'because of: %(err)s', { - 'targ': target_name, - 'err': jerr.args[0]}) - raise exception.VolumeBackendAPIException(jerr) - - LOG.debug("delete target: %s", target_name) - - try: - self.ra.delete_target(target_name) - except jexc.JDSSResourceNotFoundException as jerrrnf: - LOG.debug('failed to remove resource %(target)s because ' - 'of %(err)s', {'target': target_name, - 'err': jerrrnf.args[0]}) - - except jexc.JDSSException as jerr: - LOG.debug('Failed to Terminate_connection for target %(targ)s ' - 'because of: %(err)s ', { - 'targ': target_name, - 'err': jerr.args[0]}) - - raise exception.VolumeBackendAPIException(jerr) - - def _get_iscsi_properties(self, volume, connector): + def _get_iscsi_properties(self, volume_id, provider_auth, + multipath=False): """Return dict according to cinder/driver.py implementation. - :param volume: + :param volume_id: openstack volume UUID + :param str provider_auth: space-separated triple + ' ' + :param bool multipath: use multipath flag :return: """ - tname = self.jovian_target_prefix + volume.id + tname = self.jovian_target_prefix + volume_id iface_info = [] - multipath = connector.get('multipath', False) if multipath: iface_info = self._get_active_ifaces() if not iface_info: @@ -960,7 +446,7 @@ class JovianISCSIDriver(driver.ISCSIDriver): _('No available interfaces ' 'or config excludes them')) - iscsi_properties = dict() + iscsi_properties = {} if multipath: iscsi_properties['target_iqns'] = [] @@ -970,7 +456,7 @@ class JovianISCSIDriver(driver.ISCSIDriver): for iface in iface_info: iscsi_properties['target_iqns'].append( self.jovian_target_prefix + - volume.id) + volume_id) iscsi_properties['target_portals'].append( iface + ":" + @@ -985,13 +471,14 @@ class JovianISCSIDriver(driver.ISCSIDriver): iscsi_properties['target_discovered'] = False - auth = volume.provider_auth - if auth: - (auth_method, auth_username, auth_secret) = auth.split() + if provider_auth is None: + provider_auth = self._get_provider_auth() - iscsi_properties['auth_method'] = auth_method - iscsi_properties['auth_username'] = auth_username - iscsi_properties['auth_password'] = auth_secret + (auth_method, auth_username, auth_secret) = provider_auth.split() + + iscsi_properties['auth_method'] = auth_method + iscsi_properties['auth_username'] = auth_username + iscsi_properties['auth_password'] = auth_secret iscsi_properties['target_lun'] = 0 return iscsi_properties @@ -1000,7 +487,7 @@ class JovianISCSIDriver(driver.ISCSIDriver): """Initialize the connection and returns connection info. The iscsi driver returns a driver_volume_type of 'iscsi'. - the format of the driver data is defined in smis_get_iscsi_properties. + Format of the driver data is defined in _get_iscsi_properties. Example return value: .. code-block:: json { @@ -1013,17 +500,26 @@ class JovianISCSIDriver(driver.ISCSIDriver): } } """ - iscsi_properties = self._get_iscsi_properties(volume, connector) + multipath = connector.get("multipath", False) - LOG.debug("initialize_connection for %(volume)s %(ip)s.", - {'volume': volume.id, - 'ip': connector['ip']}) + provider_auth = volume.provider_auth - return { + ret = { 'driver_volume_type': 'iscsi', - 'data': iscsi_properties, + 'data': None, } + try: + self.driver.initialize_connection(volume.id, + provider_auth, + multipath=multipath) + ret['data'] = self._get_iscsi_properties(volume.id, + provider_auth, + multipath=multipath) + except jexc.JDSSException as jerr: + raise exception.VolumeDriverException from jerr + return ret + def terminate_connection(self, volume, connector, force=False, **kwargs): """terminate_connection @@ -1031,3 +527,29 @@ class JovianISCSIDriver(driver.ISCSIDriver): LOG.debug("terminate connection for %(volume)s ", {'volume': volume.id}) + + def initialize_connection_snapshot(self, snapshot, connector, **kwargs): + multipath = connector.get("multipath", False) + + provider_auth = snapshot.provider_auth + + ret = { + 'driver_volume_type': 'iscsi', + 'data': None, + } + + try: + self.driver.initialize_connection(snapshot.volume_id, + provider_auth, + snapshot_id=snapshot.id, + multipath=multipath) + ret['data'] = self._get_iscsi_properties(snapshot.id, + provider_auth, + multipath=multipath) + except jexc.JDSSException as jerr: + raise exception.VolumeDriverException from jerr + + return ret + + def terminate_connection_snapshot(self, snapshot, connector, **kwargs): + pass diff --git a/cinder/volume/drivers/open_e/jovian_common/driver.py b/cinder/volume/drivers/open_e/jovian_common/driver.py new file mode 100644 index 00000000000..1ed2c10e6d5 --- /dev/null +++ b/cinder/volume/drivers/open_e/jovian_common/driver.py @@ -0,0 +1,814 @@ +# Copyright (c) 2023 Open-E, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_log import log as logging +from oslo_utils import units as o_units + +from cinder import exception +from cinder.i18n import _ +from cinder.volume.drivers.open_e.jovian_common import exception as jexc +from cinder.volume.drivers.open_e.jovian_common import jdss_common as jcom +from cinder.volume.drivers.open_e.jovian_common import rest + +LOG = logging.getLogger(__name__) + + +class JovianDSSDriver(object): + + def __init__(self, config): + + self.configuration = config + self._pool = self.configuration.get('jovian_pool', 'Pool-0') + self.jovian_iscsi_target_portal_port = self.configuration.get( + 'target_port', 3260) + + self.jovian_target_prefix = self.configuration.get( + 'target_prefix', + 'iqn.2020-04.com.open-e.cinder:') + self.jovian_chap_pass_len = self.configuration.get( + 'chap_password_len', 12) + self.block_size = ( + self.configuration.get('jovian_block_size', '64K')) + self.jovian_sparse = ( + self.configuration.get('san_thin_provision', True)) + self.jovian_ignore_tpath = self.configuration.get( + 'jovian_ignore_tpath', None) + self.jovian_hosts = self.configuration.get( + 'san_hosts', []) + + self.ra = rest.JovianRESTAPI(config) + + def rest_config_is_ok(self): + """Check config correctness by checking pool availability""" + + return self.ra.is_pool_exists() + + def get_active_ifaces(self): + """Return list of ip addresses for iSCSI connection""" + + return self.jovian_hosts + + def get_provider_location(self, volume_name): + """Return volume iscsiadm-formatted provider location string.""" + return '%(host)s:%(port)s,1 %(name)s 0' % { + 'host': self.ra.get_active_host(), + 'port': self.jovian_iscsi_target_portal_port, + 'name': self._get_target_name(volume_name)} + + def create_volume(self, volume_id, volume_size, sparse=False, + block_size=None): + """Create a volume. + + :param str volume_id: volume id + :param int volume_size: size in Gi + :param bool sparse: thin or thick volume flag (default thin) + :param int block_size: size of block (default None) + + :return: None + """ + vname = jcom.vname(volume_id) + LOG.debug("Create volume:%(name)s with size:%(size)s", + {'name': volume_id, 'size': volume_size}) + + self.ra.create_lun(vname, + volume_size * o_units.Gi, + sparse=sparse, + block_size=block_size) + return + + def _promote_newest_delete(self, vname, snapshots=None): + '''Promotes and delete volume + + This function deletes volume. + It will promote volume if needed before deletion. + + :param str vname: physical volume id + :param list snapshots: snapshot data list (default None) + + :return: None + ''' + + if snapshots is None: + try: + snapshots = self.ra.get_snapshots(vname) + except jexc.JDSSResourceNotFoundException: + LOG.debug('volume %s do not exists, it was already ' + 'deleted', vname) + return + + bsnaps = self._list_busy_snapshots(vname, snapshots) + + if len(bsnaps) != 0: + + promote_target = None + + sname = jcom.get_newest_snapshot_name(bsnaps) + + for snap in bsnaps: + if snap['name'] == sname: + cvnames = jcom.snapshot_clones(snap) + for cvname in cvnames: + if jcom.is_volume(cvname): + promote_target = cvname + if jcom.is_snapshot(cvname): + self._promote_newest_delete(cvname) + if jcom.is_hidden(cvname): + self._promote_newest_delete(cvname) + break + + if promote_target is None: + self._promote_newest_delete(vname) + return + + self.ra.promote(vname, sname, promote_target) + + self._delete_vol_with_source_snap(vname, recursive=True) + + def _delete_vol_with_source_snap(self, vname, recursive=False): + '''Delete volume and its source snapshot if required + + This function deletes volume. + If volume is a clone it will check its source snapshot if + one is originates from volume to delete. + + :param str vname: physical volume id + :param bool recursive: recursive flag (default False) + + :return: None + ''' + vol = None + + try: + vol = self.ra.get_lun(vname) + except jexc.JDSSResourceNotFoundException: + LOG.debug('unable to get volume %s info, ' + 'assume it was already deleted', vname) + return + try: + self.ra.delete_lun(vname, + force_umount=True, + recursively_children=recursive) + except jexc.JDSSResourceNotFoundException: + LOG.debug('volume %s do not exists, it was already ' + 'deleted', vname) + return + + if vol is not None and \ + 'origin' in vol and \ + vol['origin'] is not None: + if jcom.is_volume(jcom.origin_snapshot(vol)) or \ + jcom.is_hidden(jcom.origin_snapshot(vol)) or \ + (jcom.vid_from_sname(jcom.origin_snapshot(vol)) == + jcom.idname(vname)): + self.ra.delete_snapshot(jcom.origin_volume(vol), + jcom.origin_snapshot(vol), + recursively_children=True, + force_umount=True) + + def _clean_garbage_resources(self, vname, snapshots=None): + '''Removes resources that is not related to volume + + Goes through volume snapshots and it clones to identify one + that is clearly not related to vname volume and therefore + have to be deleted. + + :param str vname: physical volume id + :param list snapshots: list of snapshot info dictionaries + + :return: updated list of snapshots + ''' + + if snapshots is None: + try: + snapshots = self.ra.get_snapshots(vname) + except jexc.JDSSResourceNotFoundException: + LOG.debug('volume %s do not exists, it was already ' + 'deleted', vname) + return + update = False + for snap in snapshots: + if jcom.is_volume(jcom.sname_from_snap(snap)): + cvnames = jcom.snapshot_clones(snap) + if len(cvnames) == 0: + self._delete_snapshot(vname, jcom.sname_from_snap(snap)) + update = True + if jcom.is_snapshot(jcom.sname_from_snap(snap)): + cvnames = jcom.snapshot_clones(snap) + for cvname in cvnames: + if jcom.is_hidden(cvname): + self._promote_newest_delete(cvname) + update = True + if jcom.is_snapshot(cvname): + if jcom.idname(vname) != jcom.vid_from_sname(cvname): + self._promote_newest_delete(cvname) + update = True + if update: + snapshots = self.ra.get_snapshots(vname) + return snapshots + + def _list_busy_snapshots(self, vname, snapshots, + exclude_dedicated_volumes=False) -> list: + """List all volume snapshots with clones + + Goes through provided list of snapshots. + If additional parameters are given, will filter list of snapshots + accordingly. + + Keyword arguments: + :param str vname: zvol id + :param list snapshots: list of snapshots data dicts + :param bool exclude_dedicated_volumes: list snapshots that has clones + (default False) + + :return: filtered list of snapshot data dicts + :rtype: list + """ + + out = [] + for snap in snapshots: + clones = jcom.snapshot_clones(snap) + add = False + for cvname in clones: + if exclude_dedicated_volumes and jcom.is_volume(cvname): + continue + add = True + if add: + out.append(snap) + + return out + + def _clean_volume_snapshots_mount_points(self, vname, snapshots): + update = False + for snap in snapshots: + clones = jcom.snapshot_clones(snap) + for cname in [c for c in clones if jcom.is_snapshot(c)]: + update = True + self._delete_volume(cname, cascade=True) + if update: + snapshots = self.ra.get_snapshots(vname) + return snapshots + + def _delete_volume(self, vname, cascade=False): + """_delete_volume delete routine containing delete logic + + :param str vname: physical volume id + :param bool cascade: flag for cascade volume deletion + with its snapshots + + :return: None + """ + try: + self.ra.delete_lun(vname, + force_umount=True, + recursively_children=cascade) + except jexc.JDSSResourceIsBusyException: + LOG.debug('unable to conduct direct volume %s deletion', vname) + except jexc.JDSSResourceNotFoundException: + LOG.debug('volume %s do not exists, it was already ' + 'deleted', vname) + return + except jexc.JDSSRESTException as jerr: + LOG.debug( + "Unable to delete physical volume %(volume)s " + "with error %(err)s.", { + "volume": vname, + "err": jerr}) + else: + LOG.debug('in place deletion suceeded') + return + + snapshots = None + try: + snapshots = self.ra.get_snapshots(vname) + except jexc.JDSSResourceNotFoundException: + LOG.debug('volume %s do not exists, it was already ' + 'deleted', vname) + return + + if cascade is False: + bsnaps = self._list_busy_snapshots(vname, + snapshots, + exclude_dedicated_volumes=True) + if len(bsnaps) > 0: + raise exception.VolumeIsBusy('Volume has snapshots') + + snaps = self._clean_garbage_resources(vname, snapshots) + snaps = self._clean_volume_snapshots_mount_points(vname, snapshots) + + self._promote_newest_delete(vname, snapshots=snaps) + + def delete_volume(self, volume_name, cascade=False): + """Delete volume + + :param volume: volume reference + :param cascade: remove snapshots of a volume as well + """ + vname = jcom.vname(volume_name) + + LOG.debug('deleting volume %s', vname) + + self._delete_volume(vname, cascade=cascade) + + def _clone_object(self, cvname, sname, ovname, + sparse=None, + create_snapshot=False): + """Creates a clone of specified object + + Will create snapshot if it is not provided + + :param str cvname: clone volume name + :param str sname: snapshot name + :param str ovname: original volume name + :param bool sparse: sparse property of new volume + :param bool create_snapshot: + """ + LOG.debug('cloning %(ovname)s to %(coname)s', { + "ovname": ovname, + "coname": cvname}) + + if create_snapshot: + self.ra.create_snapshot(ovname, sname) + try: + self.ra.create_volume_from_snapshot( + cvname, + sname, + ovname, + sparse=sparse) + except jexc.JDSSException as jerr: + # This is a garbage collecting section responsible for cleaning + # all the mess of request failed + if create_snapshot: + try: + self.ra.delete_snapshot(ovname, + cvname, + recursively_children=True, + force_umount=True) + except jexc.JDSSException as jerrd: + LOG.warning("Because of %s physical snapshot %s of volume" + " %s have to be removed manually", + jerrd, + sname, + ovname) + + raise jerr + + def resize_volume(self, volume_name, new_size): + """Extend an existing volume. + + :param str volume_name: volume id + :param int new_size: volume new size in Gi + """ + LOG.debug("Extend volume:%(name)s to size:%(size)s", + {'name': volume_name, 'size': new_size}) + + self.ra.extend_lun(jcom.vname(volume_name), + int(new_size) * o_units.Gi) + + def create_cloned_volume(self, + clone_name, + volume_name, + size, + snapshot_name=None, + sparse=False): + """Create a clone of the specified volume. + + :param str clone_name: new volume id + :param volume_name: original volume id + :param int size: size in Gi + :param str snapshot_name: openstack snapshot id to use for cloning + :param bool sparse: sparse flag + """ + cvname = jcom.vname(clone_name) + + ovname = jcom.vname(volume_name) + + LOG.debug('clone volume %(id)s to %(id_clone)s', { + "id": volume_name, + "id_clone": clone_name}) + + if snapshot_name: + sname = jcom.sname(snapshot_name, volume_name) + self._clone_object(cvname, sname, ovname, + create_snapshot=False, + sparse=sparse) + else: + sname = jcom.vname(clone_name) + self._clone_object(cvname, sname, ovname, + create_snapshot=True, + sparse=sparse) + + clone_size = 0 + + try: + clone_size = int(self.ra.get_lun(cvname)['volsize']) + except jexc.JDSSException as jerr: + + self.delete_volume(clone_name, cascade=False) + raise exception.VolumeBackendAPIException( + _("Fail in cloning volume %(vol)s to %(clone)s.") % { + 'vol': volume_name, 'clone': clone_name}) from jerr + + try: + if int(clone_size) < o_units.Gi * int(size): + self.resize_volume(clone_name, int(size)) + + except jexc.JDSSException as jerr: + # If volume can't be set to a proper size make sure to clean it + # before failing + try: + self.delete_volume(clone_name, cascade=False) + except jexc.JDSSException as jerrex: + LOG.warning("Error %s during cleaning failed volume %s", + jerrex, volume_name) + raise jerr from jerrex + + def create_snapshot(self, snapshot_name, volume_name): + """Create snapshot of existing volume. + + :param str snapshot_name: new snapshot id + :param str volume_name: original volume id + """ + LOG.debug('create snapshot %(snap)s for volume %(vol)s', { + 'snap': snapshot_name, + 'vol': volume_name}) + + vname = jcom.vname(volume_name) + sname = jcom.sname(snapshot_name, volume_name) + + self.ra.create_snapshot(vname, sname) + + def create_export_snapshot(self, snapshot_name, volume_name, + provider_auth): + """Creates iscsi resources needed to start using snapshot + + :param str snapshot_name: openstack snapshot id + :param str volume_name: openstack volume id + :param str provider_auth: space-separated triple + ' ' + """ + + sname = jcom.sname(snapshot_name, volume_name) + ovname = jcom.vname(volume_name) + self._clone_object(sname, sname, ovname, + sparse=True, + create_snapshot=False) + try: + self._ensure_target_volume(snapshot_name, sname, provider_auth, + ro=True) + except jexc.JDSSException as jerr: + self._delete_volume(sname, cascade=True) + raise jerr + + def remove_export(self, volume_name): + """Remove iscsi target created to make volume attachable + + :param str volume_name: openstack volume id + """ + vname = jcom.vname(volume_name) + try: + self._remove_target_volume(volume_name, vname) + except jexc.JDSSException as jerr: + LOG.warning(jerr) + + def remove_export_snapshot(self, snapshot_name, volume_name): + """Remove tmp vol and iscsi target created to make snap attachable + + :param str snapshot_name: openstack snapshot id + :param str volume_name: openstack volume id + """ + + sname = jcom.sname(snapshot_name, volume_name) + + try: + self._remove_target_volume(snapshot_name, sname) + except jexc.JDSSException as jerr: + self._delete_volume(sname, cascade=True) + raise jerr + + self._delete_volume(sname, cascade=True) + + def _delete_snapshot(self, vname, sname): + """Delete snapshot + + This method will delete snapshot mount point and snapshot if possible + + :param str vname: zvol name + :param dict snap: snapshot info dictionary + + :return: None + """ + + try: + self.ra.delete_snapshot(vname, sname, force_umount=True) + except jexc.JDSSResourceIsBusyException: + LOG.debug('Direct deletion of snapshot %s failed', vname) + else: + return + + snap = self.ra.get_snapshot(vname, sname) + + clones = jcom.snapshot_clones(snap) + busy = False + for cvname in clones: + if jcom.is_snapshot(cvname): + self._promote_newest_delete(cvname) + if jcom.is_volume(cvname): + LOG.debug('Will not delete snap %(snap)s,' + 'becasue it is used by %(vol)s', + {'snap': sname, + 'vol': cvname}) + busy = True + if busy: + return + try: + self.ra.delete_snapshot(vname, sname, force_umount=True) + except jexc.JDSSResourceIsBusyException: + LOG.debug('Unable to delete snap %(snap)s because it is busy', + {'snap': jcom.sname_from_snap(snap)}) + + def delete_snapshot(self, volume_name, snapshot_name): + """Delete snapshot of existing volume. + + :param str volume_name: volume id + :param str snapshot_name: snapshot id + """ + vname = jcom.vname(volume_name) + sname = jcom.sname(snapshot_name, volume_name) + + self._delete_snapshot(vname, sname) + + def _ensure_target_volume(self, id, vid, provider_auth, ro=False): + """Checks if target configured properly and volume is attached to it + + :param str id: id that would be used for target naming + :param str vname: physical volume id + :param str provider_auth: space-separated triple + ' ' + """ + LOG.debug("ensure volume %s assigned to a proper target", id) + + target_name = self._get_target_name(id) + + if not provider_auth: + msg = _("volume %s is missing provider_auth") % jcom.idname(id) + raise jexc.JDSSException(msg) + + if not self.ra.is_target(target_name): + + return self._create_target_volume(id, vid, provider_auth) + + if not self.ra.is_target_lun(target_name, vid): + self._attach_target_volume(target_name, vid) + + (__, auth_username, auth_secret) = provider_auth.split() + chap_cred = {"name": auth_username, + "password": auth_secret} + + try: + users = self.ra.get_target_user(target_name) + if len(users) == 1: + if users[0]['name'] == chap_cred['name']: + return + self.ra.delete_target_user( + target_name, + users[0]['name']) + for user in users: + self.ra.delete_target_user( + target_name, + user['name']) + self._set_target_credentials(target_name, chap_cred) + + except jexc.JDSSException as jerr: + self.ra.delete_target(target_name) + raise exception.VolumeBackendAPIException(jerr) + + def _get_target_name(self, volume_id): + """Return iSCSI target name to access volume.""" + return f'{self.jovian_target_prefix}{volume_id}' + + def _get_iscsi_properties(self, volume_id, provider_auth, multipath=False): + """Return dict according to cinder/driver.py implementation. + + :param volume_id: UUID of volume, might take snapshot UUID + :param str provider_auth: space-separated triple + ' ' + :return: + """ + tname = self._get_target_name(volume_id) + iface_info = [] + if multipath: + iface_info = self.get_active_ifaces() + if not iface_info: + raise exception.InvalidConfigurationValue( + _('No available interfaces ' + 'or config excludes them')) + + iscsi_properties = {} + + if multipath: + iscsi_properties['target_iqns'] = [] + iscsi_properties['target_portals'] = [] + iscsi_properties['target_luns'] = [] + LOG.debug('tpaths %s.', iface_info) + for iface in iface_info: + iscsi_properties['target_iqns'].append( + self._get_target_name(volume_id)) + iscsi_properties['target_portals'].append( + iface + + ":" + + str(self.jovian_iscsi_target_portal_port)) + iscsi_properties['target_luns'].append(0) + else: + iscsi_properties['target_iqn'] = tname + iscsi_properties['target_portal'] = ( + self.ra.get_active_host() + + ":" + + str(self.jovian_iscsi_target_portal_port)) + + iscsi_properties['target_discovered'] = False + + if provider_auth: + (auth_method, auth_username, auth_secret) = provider_auth.split() + + iscsi_properties['auth_method'] = auth_method + iscsi_properties['auth_username'] = auth_username + iscsi_properties['auth_password'] = auth_secret + + iscsi_properties['target_lun'] = 0 + return iscsi_properties + + def _remove_target_volume(self, id, vid): + """_remove_target_volume + + Ensure that volume is not attached to target and target do not exists. + """ + + target_name = self._get_target_name(id) + LOG.debug("remove export") + LOG.debug("detach volume:%(vol)s from target:%(targ)s.", { + 'vol': id, + 'targ': target_name}) + + try: + self.ra.detach_target_vol(target_name, vid) + except jexc.JDSSResourceNotFoundException as jerrrnf: + LOG.debug('failed to remove resource %(t)s because of %(err)s', { + 't': target_name, + 'err': jerrrnf.args[0]}) + except jexc.JDSSException as jerr: + LOG.warning('failed to Terminate_connection for target %(targ)s ' + 'because of: %(err)s', {'targ': target_name, + 'err': jerr.args[0]}) + raise jerr + + LOG.debug("delete target: %s", target_name) + + try: + self.ra.delete_target(target_name) + except jexc.JDSSResourceNotFoundException as jerrrnf: + LOG.debug('failed to remove resource %(target)s because ' + 'of %(err)s', + {'target': target_name, 'err': jerrrnf.args[0]}) + + except jexc.JDSSException as jerr: + LOG.warning('Failed to Terminate_connection for target %(targ)s ' + 'because of: %(err)s ', + {'targ': target_name, 'err': jerr.args[0]}) + + raise jerr + + def ensure_export(self, volume_id, provider_auth): + + vname = jcom.vname(volume_id) + + self._ensure_target_volume(volume_id, vname, provider_auth) + + def initialize_connection(self, volume_id, provider_auth, + snapshot_id=None, + multipath=False): + """Ensures volume is ready for connection and return connection data + + Ensures that particular volume is ready to be used over iscsi + with credentials provided in provider_auth + If snapshot name is provided method will ensure that connection + leads to read only volume object associated with particular snapshot + + :param str volume_id: Volume id string + :param str provider_auth: space-separated triple + ' ' + :param str snapshot_id: id of snapshot that should be connected + :param bool multipath: specifies if multipath should be used + """ + + id_of_disk_to_attach = volume_id + vid = jcom.vname(volume_id) + if provider_auth is None: + raise jexc.JDSSException(_("CHAP credentials missing")) + if snapshot_id: + id_of_disk_to_attach = snapshot_id + vid = jcom.sname(snapshot_id, volume_id) + iscsi_properties = self._get_iscsi_properties(id_of_disk_to_attach, + provider_auth, + multipath=multipath) + if snapshot_id: + self._ensure_target_volume(id_of_disk_to_attach, + vid, + provider_auth, + mode='ro') + else: + self._ensure_target_volume(id_of_disk_to_attach, + vid, + provider_auth) + + LOG.debug( + "initialize_connection for physical disk %(vid)s with %(id)s", + {'vid': vid, 'id': id_of_disk_to_attach}) + + return { + 'driver_volume_type': 'iscsi', + 'data': iscsi_properties, + } + + def _create_target_volume(self, id, vid, provider_auth): + """Creates target and attach volume to it + + :param id: uuid of particular resource + :param vid: physical volume id, might identify snapshot mount + :param str provider_auth: space-separated triple + ' ' + :return: + """ + LOG.debug("create target and attach volume %s to it", vid) + + target_name = self._get_target_name(id) + + (__, auth_username, auth_secret) = provider_auth.split() + chap_cred = {"name": auth_username, + "password": auth_secret} + + # Create target + self.ra.create_target(target_name, use_chap=True) + + # Attach volume + self._attach_target_volume(target_name, vid) + + # Set credentials + self._set_target_credentials(target_name, chap_cred) + + def _attach_target_volume(self, target_name, vname): + """Attach target to volume and handles exceptions + + Attempts to set attach volume to specific target. + In case of failure will remove target. + :param target_name: name of target + :param use_chap: flag for using chap + """ + try: + self.ra.attach_target_vol(target_name, vname) + except jexc.JDSSException as jerr: + msg = ('Unable to attach volume {volume} to target {target} ' + 'because of {error}.') + LOG.warning(msg, {"volume": vname, + "target": target_name, + "error": jerr}) + self.ra.delete_target(target_name) + raise jerr + + def _set_target_credentials(self, target_name, cred): + """Set CHAP configuration for target and handle exceptions + + Attempts to set CHAP credentials for specific target. + In case of failure will remove target. + :param target_name: name of target + :param cred: CHAP user name and password + """ + try: + self.ra.create_target_user(target_name, cred) + + except jexc.JDSSException as jerr: + try: + self.ra.delete_target(target_name) + except jexc.JDSSException: + pass + + err_msg = (('Unable to create user %(user)s ' + 'for target %(target)s ' + 'because of %(error)s.') % { + 'target': target_name, + 'user': cred['name'], + 'error': jerr}) + + LOG.error(err_msg) + raise jexc.JDSSException(_(err_msg)) diff --git a/cinder/volume/drivers/open_e/jovian_common/jdss_common.py b/cinder/volume/drivers/open_e/jovian_common/jdss_common.py index 266c73dcd0b..f8be63900aa 100644 --- a/cinder/volume/drivers/open_e/jovian_common/jdss_common.py +++ b/cinder/volume/drivers/open_e/jovian_common/jdss_common.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +from datetime import datetime + from cinder import exception from cinder.i18n import _ @@ -30,13 +32,16 @@ def is_snapshot(name): def idname(name): - """Convert id into name""" + """Extract UUID from physical volume name""" - if name.startswith(('s_', 'v_', 't_')): + if name.startswith(('v_', 't_')): return name[2:] + if name.startswith(('s_')): + return sid_from_sname(name) + msg = _('Object name %s is incorrect') % name - raise exception.VolumeBackendAPIException(message=msg) + raise exception.VolumeDriverException(message=msg) def vname(name): @@ -47,30 +52,46 @@ def vname(name): if name.startswith('s_'): msg = _('Attempt to use snapshot %s as a volume') % name - raise exception.VolumeBackendAPIException(message=msg) + raise exception.VolumeDriverException(message=msg) if name.startswith('t_'): msg = _('Attempt to use deleted object %s as a volume') % name - raise exception.VolumeBackendAPIException(message=msg) + raise exception.VolumeDriverException(message=msg) - return 'v_' + name + return f'v_{name}' -def sname(name): - """Convert id into snapshot name""" +def sname_to_id(sname): - if name.startswith('s_'): - return name + spl = sname.split('_') - if name.startswith('v_'): - msg = _('Attempt to use volume %s as a snapshot') % name - raise exception.VolumeBackendAPIException(message=msg) + if len(spl) == 2: + return (spl[1], None) - if name.startswith('t_'): - msg = _('Attempt to use deleted object %s as a snapshot') % name - raise exception.VolumeBackendAPIException(message=msg) + return (spl[1], spl[2]) - return 's_' + name + +def sid_from_sname(name): + return sname_to_id(name)[0] + + +def vid_from_sname(name): + return sname_to_id(name)[1] + + +def sname(sid, vid): + """Convert id into snapshot name + + :param: vid: volume id + :param: sid: snapshot id + """ + if vid is None: + return 's_%(sid)s' % {'sid': sid} + return 's_%(sid)s_%(vid)s' % {'sid': sid, 'vid': vid} + + +def sname_from_snap(snapshot_struct): + return snapshot_struct['name'] def is_hidden(name): @@ -83,22 +104,33 @@ def is_hidden(name): return False -def origin_snapshot(origin_str): - """Extracts original physical snapshot name from origin record""" - - return origin_str.split("@")[1] +def origin_snapshot(vol): + """Extracts original physical snapshot name from volume dict""" + if 'origin' in vol and vol['origin'] is not None: + return vol['origin'].split("@")[1] + return None -def origin_volume(origin_str): - """Extracts original physical volume name from origin record""" +def origin_volume(vol): + """Extracts original physical volume name from volume dict""" - return origin_str.split("@")[0].split("/")[1] + if 'origin' in vol and vol['origin'] is not None: + return vol['origin'].split("@")[0].split("/")[1] + return None -def full_name_volume(name): - """Get volume id from full_name""" +def snapshot_clones(snap): + """Return list of clones associated with snapshot or return empty list""" + out = [] + clones = [] + if 'clones' not in snap: + return out + else: + clones = snap['clones'].split(',') - return name.split('/')[1] + for clone in clones: + out.append(clone.split('/')[1]) + return out def hidden(name): @@ -110,3 +142,14 @@ def hidden(name): if name[:2] == 'v_' or name[:2] == 's_': return 't_' + name[2:] return 't_' + name + + +def get_newest_snapshot_name(snapshots): + newest_date = None + sname = None + for snap in snapshots: + current_date = datetime.strptime(snap['creation'], "%Y-%m-%d %H:%M:%S") + if newest_date is None or current_date > newest_date: + newest_date = current_date + sname = snap['name'] + return sname diff --git a/cinder/volume/drivers/open_e/jovian_common/rest.py b/cinder/volume/drivers/open_e/jovian_common/rest.py index 64ffb56755a..b3ec2dc0658 100644 --- a/cinder/volume/drivers/open_e/jovian_common/rest.py +++ b/cinder/volume/drivers/open_e/jovian_common/rest.py @@ -19,7 +19,6 @@ import re from oslo_log import log as logging -from cinder import exception from cinder.i18n import _ from cinder.volume.drivers.open_e.jovian_common import exception as jexc from cinder.volume.drivers.open_e.jovian_common import rest_proxy @@ -28,23 +27,36 @@ LOG = logging.getLogger(__name__) class JovianRESTAPI(object): - """Jovian REST API proxy.""" + """Jovian REST API""" def __init__(self, config): self.pool = config.get('jovian_pool', 'Pool-0') - self.rproxy = rest_proxy.JovianRESTProxy(config) + self.rproxy = rest_proxy.JovianDSSRESTProxy(config) self.resource_dne_msg = ( re.compile(r'^Zfs resource: .* not found in this collection\.$')) + self.resource_has_clones_msg = ( + re.compile(r'^In order to delete a zvol, you must delete all of ' + 'its clones first.$')) + self.resource_has_clones_class = ( + re.compile(r'^opene.storage.zfs.ZfsOeError$')) + + self.resource_has_snapshots_msg = ( + re.compile(r"^cannot destroy '.*/.*': volume has children\nuse " + "'-r' to destroy the following datasets:\n.*")) + self.resource_has_snapshots_class = ( + re.compile(r'^zfslib.wrap.zfs.ZfsCmdError$')) + def _general_error(self, url, resp): reason = "Request %s failure" % url + LOG.debug("error resp %s", resp) if 'error' in resp: - eclass = resp.get('class', 'Unknown') - code = resp.get('code', 'Unknown') - msg = resp.get('message', 'Unknown') + eclass = resp['error'].get('class', 'Unknown') + code = resp['error'].get('code', 'Unknown') + msg = resp['error'].get('message', 'Unknown') reason = _("Request to %(url)s failed with code: %(code)s " "of type:%(eclass)s reason:%(message)s") @@ -119,8 +131,11 @@ class JovianRESTAPI(object): :param volume_name: :param volume_size: + :param sparse: thin or thick volume flag + :param block_size: size of block :return: """ + LOG.debug("create volume start") volume_size_str = str(volume_size) jbody = { 'name': volume_name, @@ -190,13 +205,11 @@ class JovianRESTAPI(object): return False def get_lun(self, volume_name): - """get_lun. + """get_lun GET /volumes/ - :param volume_name: - :return: - { - "data": + :param volume_name: zvol id + :return: volume dict { "origin": null, "referenced": "65536", @@ -230,9 +243,7 @@ class JovianRESTAPI(object): "name": "v1", "checksum": "on", "refreservation": "1076101120" - }, - "error": null - } + } """ req = '/volumes/' + volume_name @@ -252,8 +263,8 @@ class JovianRESTAPI(object): def modify_lun(self, volume_name, prop=None): """Update volume properties - :prop volume_name: volume name - :prop prop: dictionary + :param volume_name: volume name + :param prop: dictionary { : } @@ -287,8 +298,8 @@ class JovianRESTAPI(object): def modify_property_lun(self, volume_name, prop=None): """Change volume properties - :prop: volume_name: volume name - :prop: prop: dictionary of volume properties in format + :param volume_name: volume name + :param prop: dictionary of volume properties in format { "property_name": "", "property_value":""} """ @@ -311,9 +322,33 @@ class JovianRESTAPI(object): reason=resp['error']['message']) raise jexc.JDSSRESTException(request=req, reason="unknown") + def promote(self, volume_name, snapshot_name, clone_name): + """promote volume + + POST /volumes//snapshots//clones/ + /promoteclone_promote + :param volume_name: parent volume for the one that should be promoted + :param snapshot_name: snapshot that is linking parent and clone + :param clone_name: volume name that is going to be promoted + :return: + """ + jbody = {} + + req = '/volumes/' + volume_name + \ + '/snapshots/' + snapshot_name + \ + '/clones/' + clone_name + '/promote' + + LOG.debug("promote clone %s", clone_name) + resp = self.rproxy.pool_request('POST', req, json_data=jbody) + + if resp["code"] == 200: + LOG.debug("clone %s promoted", clone_name) + return + + self._general_error(req, resp) + def delete_lun(self, volume_name, recursively_children=False, - recursively_dependents=False, force_umount=False): """delete_volume. @@ -325,15 +360,12 @@ class JovianRESTAPI(object): if recursively_children: jbody['recursively_children'] = True - if recursively_dependents: - jbody['recursively_dependents'] = True - if force_umount: jbody['force_umount'] = True req = '/volumes/' + volume_name LOG.debug(("delete volume:%(vol)s " - "recursively children:%(args)s"), + "args:%(args)s"), {'vol': volume_name, 'args': jbody}) @@ -357,10 +389,20 @@ class JovianRESTAPI(object): # Handle volume busy if resp["code"] == 500 and resp["error"]: - if resp["error"]["errno"] == 1000: - LOG.warning( - "volume %s is busy", volume_name) - raise exception.VolumeIsBusy(volume_name=volume_name) + if 'message' in resp['error'] and \ + 'class' in resp['error']: + if self.resource_has_clones_msg.match( + resp['error']['message']) and \ + self.resource_has_clones_class.match( + resp['error']['class']): + LOG.warning("volume %s is busy", volume_name) + raise jexc.JDSSResourceIsBusyException(res=volume_name) + if self.resource_has_snapshots_msg.match( + resp['error']['message']) and \ + self.resource_has_snapshots_class.match( + resp['error']['class']): + LOG.warning("volume %s is busy", volume_name) + raise jexc.JDSSResourceIsBusyException(res=volume_name) raise jexc.JDSSRESTException('Failed to delete volume.') @@ -562,17 +604,27 @@ class JovianRESTAPI(object): self._general_error(req, resp) - def attach_target_vol(self, target_name, lun_name, lun_id=0): + def attach_target_vol(self, target_name, lun_name, + lun_id=0, + mode=None): """attach_target_vol. POST /san/iscsi/targets//luns - :param target_name: - :param lun_name: + :param target_name: name of the target + :param lun_name: phisical volume name to be attached + :param lun_id: id that would be assigned to volume + :param mode: one of "wt", "wb" or "ro" :return: """ req = '/san/iscsi/targets/%s/luns' % target_name jbody = {"name": lun_name, "lun": lun_id} + if mode is not None: + if mode in ['wt', 'wb', 'ro']: + jbody['mode'] = mode + else: + raise jexc.JDSSException( + _("Incoret mode for target %s" % mode)) LOG.debug("atach volume %(vol)s to target %(tar)s", {'vol': lun_name, 'tar': target_name}) @@ -671,6 +723,9 @@ class JovianRESTAPI(object): if 'sparse' in options: jbody['sparse'] = options['sparse'] + if 'ro' in options: + jbody['ro'] = options['sparse'] + LOG.debug("create volume %(vol)s from snapshot %(snap)s", {'vol': volume_name, 'snap': snapshot_name}) @@ -722,11 +777,42 @@ class JovianRESTAPI(object): self._general_error(req, resp) + def count_rollback_dependents(self, volume_name, snapshot_name): + """Count volumes and snapshots affected by rollback + + GET /volumes//snapshots//rollback + + :param str volume_name: volume that is going to be reverted + :param str snapshot_name: snapshot of a volume above + + :return: None + """ + req = ('/volumes/%(vol)s/snapshots/' + '%(snap)s/rollback') % {'vol': volume_name, + 'snap': snapshot_name} + + LOG.debug("get rollback count for volume %(vol)s to snapshot %(snap)s", + {'vol': volume_name, + 'snap': snapshot_name}) + + resp = self.rproxy.pool_request('GET', req) + + if not resp["error"] and resp["code"] == 200: + return resp['data'] + + if resp["code"] == 500: + if resp["error"]: + if resp["error"]["errno"] == 1: + raise jexc.JDSSResourceNotFoundException( + res="%(vol)s@%(snap)s" % {'vol': volume_name, + 'snap': snapshot_name}) + + self._general_error(req, resp) + def delete_snapshot(self, volume_name, snapshot_name, recursively_children=False, - recursively_dependents=False, force_umount=False): """delete_snapshot. @@ -756,17 +842,10 @@ class JovianRESTAPI(object): if recursively_children: jbody['recursively_children'] = True - if recursively_dependents: - jbody['recursively_dependents'] = True - if force_umount: jbody['force_umount'] = True - resp = dict() - if len(jbody) > 0: - resp = self.rproxy.pool_request('DELETE', req, json_data=jbody) - else: - resp = self.rproxy.pool_request('DELETE', req) + resp = self.rproxy.pool_request('DELETE', req, json_data=jbody) if resp["code"] in (200, 201, 204): LOG.debug("snapshot %s deleted", snapshot_name) @@ -779,6 +858,25 @@ class JovianRESTAPI(object): snapshot=snapshot_name) self._general_error(req, resp) + def get_snapshot(self, volume_name, snapshot_name): + + req = (('/volumes/%(vol)s/snapshots/%(snap)s') % + {'vol': volume_name, 'snap': snapshot_name}) + + LOG.debug("get snapshots for volume %s ", volume_name) + + resp = self.rproxy.pool_request('GET', req) + + if not resp["error"] and resp["code"] == 200: + return resp["data"] + + if resp['code'] == 500: + if 'message' in resp['error']: + if self.resource_dne_msg.match(resp['error']['message']): + raise jexc.JDSSResourceNotFoundException(volume_name) + + self._general_error(req, resp) + def get_snapshots(self, volume_name): """get_snapshots. diff --git a/cinder/volume/drivers/open_e/jovian_common/rest_proxy.py b/cinder/volume/drivers/open_e/jovian_common/rest_proxy.py index 16eda74a520..309f05467e5 100644 --- a/cinder/volume/drivers/open_e/jovian_common/rest_proxy.py +++ b/cinder/volume/drivers/open_e/jovian_common/rest_proxy.py @@ -31,8 +31,8 @@ from cinder.volume.drivers.open_e.jovian_common import exception as jexc LOG = logging.getLogger(__name__) -class JovianRESTProxy(object): - """Jovian REST API proxy.""" +class JovianDSSRESTProxy(object): + """Jovian REST API proxy""" def __init__(self, config): """:param config: list of config values.""" @@ -62,7 +62,7 @@ class JovianRESTProxy(object): self.user = config.get('san_login', 'admin') self.password = config.get('san_password', 'admin') self.verify = config.get('driver_ssl_cert_verify', True) - self.cert = config.get('driver_ssl_cert_path') + self.cert = config.get('driver_ssl_cert_path', None) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -76,7 +76,7 @@ class JovianRESTProxy(object): session.headers.update({'Connection': 'keep-alive', 'Content-Type': 'application/json', 'Authorization': 'Basic'}) - session.hooks['response'] = [JovianRESTProxy._handle_500] + session.hooks['response'] = [JovianDSSRESTProxy._handle_500] session.verify = self.verify if self.verify and self.cert: session.verify = self.cert @@ -101,34 +101,36 @@ class JovianRESTProxy(object): """Send request to the specific url. :param request_method: GET, POST, DELETE - :param url: where to send + :param req: where to send :param json_data: data """ out = None - for i in range(len(self.hosts)): - try: - addr = "%(base)s%(req)s" % {'base': self._get_base_url(), - 'req': req} - LOG.debug("Sending %(t)s to %(addr)s", - {'t': request_method, 'addr': addr}) - r = None - if json_data: - r = requests.Request(request_method, - addr, - data=json.dumps(json_data)) - else: - r = requests.Request(request_method, addr) + for i in range(3): + for i in range(len(self.hosts)): + try: + addr = "%(base)s%(req)s" % {'base': self._get_base_url(), + 'req': req} + LOG.debug("Sending %(t)s to %(addr)s data %(data)s", + {'t': request_method, + 'addr': addr, + 'data': json_data}) + r = None + if json_data: + r = requests.Request(request_method, + addr, + data=json.dumps(json_data)) + else: + r = requests.Request(request_method, addr) - pr = self.session.prepare_request(r) - out = self._send(pr) - except requests.exceptions.ConnectionError: - self._next_host() - continue - break + pr = self.session.prepare_request(r) + out = self._send(pr) + except requests.exceptions.ConnectionError: + self._next_host() + continue - LOG.debug("Geting %(data)s from %(t)s to %(addr)s", - {'data': out, 't': request_method, 'addr': addr}) - return out + LOG.debug("Geting %(data)s from %(t)s to %(addr)s", + {'data': out, 't': request_method, 'addr': addr}) + return out def pool_request(self, request_method, req, json_data=None): """Send request to the specific url. @@ -143,28 +145,25 @@ class JovianRESTProxy(object): {'t': request_method, 'addr': addr}) return self.request(request_method, req, json_data=json_data) - @retry((requests.exceptions.ConnectionError, - jexc.JDSSOSException), - interval=2, - backoff_rate=2, - retries=7) + @retry(json.JSONDecodeError, + retries=3) def _send(self, pr): """Send prepared request :param pr: prepared request """ - ret = dict() + ret = {} response_obj = self.session.send(pr) ret['code'] = response_obj.status_code + if ret['code'] == 204: + ret["data"] = None + return ret - try: - data = json.loads(response_obj.text) - ret["error"] = data.get("error") - ret["data"] = data.get("data") - except json.JSONDecodeError: - pass + data = json.loads(response_obj.text) + ret["error"] = data.get("error") + ret["data"] = data.get("data") return ret diff --git a/doc/source/reference/support-matrix.ini b/doc/source/reference/support-matrix.ini index 514e2c01609..02c129028da 100644 --- a/doc/source/reference/support-matrix.ini +++ b/doc/source/reference/support-matrix.ini @@ -932,7 +932,7 @@ driver.netapp_ontap=complete driver.netapp_solidfire=complete driver.nexenta=missing driver.nfs=missing -driver.opene_joviandss=complete +driver.opene_joviandss=missing driver.prophetstor=missing driver.pure=complete driver.qnap=missing diff --git a/releasenotes/notes/open-e-joviandss-disable-revert-to-snapshot-359a2e0317e618ec.yaml b/releasenotes/notes/open-e-joviandss-disable-revert-to-snapshot-359a2e0317e618ec.yaml new file mode 100644 index 00000000000..5f9e706cbf9 --- /dev/null +++ b/releasenotes/notes/open-e-joviandss-disable-revert-to-snapshot-359a2e0317e618ec.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Open-E JovianDSS driver: revert-to-snapshot has been removed. +other: + - | + Open-E JovianDSS driver: general rework of volume and snapshot creation and deletion. + - | + Open-E JovianDSS driver: network interfaces selection on JovianDSS storage has been reworked.