1155 lines
53 KiB
Python
1155 lines
53 KiB
Python
# Copyright (c) 2014 Pure Storage, 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 json
|
|
import urllib2
|
|
|
|
import mock
|
|
from oslo.utils import units
|
|
from oslo_concurrency import processutils
|
|
|
|
from cinder import exception
|
|
from cinder import test
|
|
from cinder.volume.drivers import pure
|
|
|
|
DRIVER_PATH = "cinder.volume.drivers.pure"
|
|
DRIVER_OBJ = DRIVER_PATH + ".PureISCSIDriver"
|
|
ARRAY_OBJ = DRIVER_PATH + ".FlashArray"
|
|
|
|
TARGET = "pure-target"
|
|
API_TOKEN = "12345678-abcd-1234-abcd-1234567890ab"
|
|
VOLUME_BACKEND_NAME = "Pure_iSCSI"
|
|
PORT_NAMES = ["ct0.eth2", "ct0.eth3", "ct1.eth2", "ct1.eth3"]
|
|
ISCSI_IPS = ["10.0.0." + str(i + 1) for i in range(len(PORT_NAMES))]
|
|
HOSTNAME = "computenode1"
|
|
PURE_HOST_NAME = pure._generate_purity_host_name(HOSTNAME)
|
|
PURE_HOST = {"name": PURE_HOST_NAME,
|
|
"hgroup": None,
|
|
"iqn": [],
|
|
"wwn": [],
|
|
}
|
|
REST_VERSION = "1.2"
|
|
VOLUME_ID = "abcdabcd-1234-abcd-1234-abcdeffedcba"
|
|
VOLUME = {"name": "volume-" + VOLUME_ID,
|
|
"id": VOLUME_ID,
|
|
"display_name": "fake_volume",
|
|
"size": 2,
|
|
"host": "irrelevant",
|
|
"volume_type": None,
|
|
"volume_type_id": None,
|
|
"consistencygroup_id": None
|
|
}
|
|
VOLUME_WITH_CGROUP = VOLUME.copy()
|
|
VOLUME_WITH_CGROUP['consistencygroup_id'] = \
|
|
"4a2f7e3a-312a-40c5-96a8-536b8a0fe074"
|
|
SRC_VOL_ID = "dc7a294d-5964-4379-a15f-ce5554734efc"
|
|
SRC_VOL = {"name": "volume-" + SRC_VOL_ID,
|
|
"id": SRC_VOL_ID,
|
|
"display_name": 'fake_src',
|
|
"size": 2,
|
|
"host": "irrelevant",
|
|
"volume_type": None,
|
|
"volume_type_id": None,
|
|
"consistencygroup_id": None
|
|
}
|
|
SNAPSHOT_ID = "04fe2f9a-d0c4-4564-a30d-693cc3657b47"
|
|
SNAPSHOT = {"name": "snapshot-" + SNAPSHOT_ID,
|
|
"id": SNAPSHOT_ID,
|
|
"volume_id": SRC_VOL_ID,
|
|
"volume_name": "volume-" + SRC_VOL_ID,
|
|
"volume_size": 2,
|
|
"display_name": "fake_snapshot",
|
|
"cgsnapshot_id": None
|
|
}
|
|
SNAPSHOT_WITH_CGROUP = SNAPSHOT.copy()
|
|
SNAPSHOT_WITH_CGROUP['cgsnapshot_id'] = \
|
|
"4a2f7e3a-312a-40c5-96a8-536b8a0fe075"
|
|
INITIATOR_IQN = "iqn.1993-08.org.debian:01:222"
|
|
CONNECTOR = {"initiator": INITIATOR_IQN, "host": HOSTNAME}
|
|
TARGET_IQN = "iqn.2010-06.com.purestorage:flasharray.12345abc"
|
|
TARGET_PORT = "3260"
|
|
ISCSI_PORTS = [{"name": name,
|
|
"iqn": TARGET_IQN,
|
|
"portal": ip + ":" + TARGET_PORT,
|
|
"wwn": None,
|
|
} for name, ip in zip(PORT_NAMES, ISCSI_IPS)]
|
|
NON_ISCSI_PORT = {"name": "ct0.fc1",
|
|
"iqn": None,
|
|
"portal": None,
|
|
"wwn": "5001500150015081",
|
|
}
|
|
PORTS_WITH = ISCSI_PORTS + [NON_ISCSI_PORT]
|
|
PORTS_WITHOUT = [NON_ISCSI_PORT]
|
|
VOLUME_CONNECTIONS = [{"host": "h1", "name": VOLUME["name"] + "-cinder"},
|
|
{"host": "h2", "name": VOLUME["name"] + "-cinder"},
|
|
]
|
|
TOTAL_SPACE = 50.0
|
|
FREE_SPACE = 32.1
|
|
SPACE_INFO = {"capacity": TOTAL_SPACE * units.Gi,
|
|
"total": (TOTAL_SPACE - FREE_SPACE) * units.Gi,
|
|
}
|
|
|
|
|
|
class PureISCSIDriverTestCase(test.TestCase):
|
|
|
|
def setUp(self):
|
|
super(PureISCSIDriverTestCase, self).setUp()
|
|
self.config = mock.Mock()
|
|
self.config.san_ip = TARGET
|
|
self.config.pure_api_token = API_TOKEN
|
|
self.config.volume_backend_name = VOLUME_BACKEND_NAME
|
|
self.driver = pure.PureISCSIDriver(configuration=self.config)
|
|
self.array = mock.create_autospec(pure.FlashArray)
|
|
self.driver._array = self.array
|
|
|
|
@mock.patch(ARRAY_OBJ, autospec=True)
|
|
@mock.patch(DRIVER_OBJ + "._choose_target_iscsi_port")
|
|
def test_do_setup(self, mock_choose_target_iscsi_port, mock_array):
|
|
mock_choose_target_iscsi_port.return_value = ISCSI_PORTS[0]
|
|
mock_array.return_value = self.array
|
|
self.driver.do_setup(None)
|
|
mock_array.assert_called_with(TARGET, API_TOKEN)
|
|
self.assertEqual(self.array, self.driver._array)
|
|
mock_choose_target_iscsi_port.assert_called_with()
|
|
self.assertEqual(ISCSI_PORTS[0], self.driver._iscsi_port)
|
|
self.assert_error_propagates(
|
|
[mock_array, mock_choose_target_iscsi_port],
|
|
self.driver.do_setup, None)
|
|
|
|
def assert_error_propagates(self, mocks, func, *args, **kwargs):
|
|
"""Assert that errors from mocks propagate to func.
|
|
|
|
Fail if exceptions raised by mocks are not seen when calling
|
|
func(*args, **kwargs). Ensure that we are really seeing exceptions
|
|
from the mocks by failing if just running func(*args, **kargs) raises
|
|
an exception itself.
|
|
"""
|
|
func(*args, **kwargs)
|
|
for mock_func in mocks:
|
|
mock_func.side_effect = exception.PureDriverException(
|
|
reason="reason")
|
|
self.assertRaises(exception.PureDriverException,
|
|
func, *args, **kwargs)
|
|
mock_func.side_effect = None
|
|
|
|
def test_generate_purity_host_name(self):
|
|
generate = pure._generate_purity_host_name
|
|
result = generate("really-long-string-thats-a-bit-too-long")
|
|
self.assertTrue(result.startswith("really-long-string-that-"))
|
|
self.assertTrue(result.endswith("-cinder"))
|
|
self.assertEqual(len(result), 63)
|
|
self.assertTrue(pure.GENERATED_NAME.match(result))
|
|
result = generate("!@#$%^-invalid&*")
|
|
self.assertTrue(result.startswith("invalid---"))
|
|
self.assertTrue(result.endswith("-cinder"))
|
|
self.assertEqual(len(result), 49)
|
|
self.assertTrue(pure.GENERATED_NAME.match(result))
|
|
|
|
def test_create_volume(self):
|
|
self.driver.create_volume(VOLUME)
|
|
self.array.create_volume.assert_called_with(
|
|
VOLUME["name"] + "-cinder", 2 * units.Gi)
|
|
self.assert_error_propagates([self.array.create_volume],
|
|
self.driver.create_volume, VOLUME)
|
|
|
|
@mock.patch(DRIVER_OBJ + "._add_volume_to_consistency_group",
|
|
autospec=True)
|
|
def test_create_volume_with_cgroup(self, mock_add_to_cgroup):
|
|
vol_name = VOLUME_WITH_CGROUP["name"] + "-cinder"
|
|
|
|
self.driver.create_volume(VOLUME_WITH_CGROUP)
|
|
|
|
mock_add_to_cgroup\
|
|
.assert_called_with(self.driver,
|
|
VOLUME_WITH_CGROUP['consistencygroup_id'],
|
|
vol_name)
|
|
|
|
def test_create_volume_from_snapshot(self):
|
|
vol_name = VOLUME["name"] + "-cinder"
|
|
snap_name = SNAPSHOT["volume_name"] + "-cinder." + SNAPSHOT["name"]
|
|
|
|
# Branch where extend unneeded
|
|
self.driver.create_volume_from_snapshot(VOLUME, SNAPSHOT)
|
|
self.array.copy_volume.assert_called_with(snap_name, vol_name)
|
|
self.assertFalse(self.array.extend_volume.called)
|
|
self.assert_error_propagates(
|
|
[self.array.copy_volume],
|
|
self.driver.create_volume_from_snapshot, VOLUME, SNAPSHOT)
|
|
self.assertFalse(self.array.extend_volume.called)
|
|
|
|
# Branch where extend needed
|
|
SNAPSHOT["volume_size"] = 1 # resize so smaller than VOLUME
|
|
self.driver.create_volume_from_snapshot(VOLUME, SNAPSHOT)
|
|
expected = [mock.call.copy_volume(snap_name, vol_name),
|
|
mock.call.extend_volume(vol_name, 2 * units.Gi)]
|
|
self.array.assert_has_calls(expected)
|
|
self.assert_error_propagates(
|
|
[self.array.copy_volume, self.array.extend_volume],
|
|
self.driver.create_volume_from_snapshot, VOLUME, SNAPSHOT)
|
|
SNAPSHOT["volume_size"] = 2 # reset size
|
|
|
|
@mock.patch(DRIVER_OBJ + "._add_volume_to_consistency_group",
|
|
autospec=True)
|
|
@mock.patch(DRIVER_OBJ + "._extend_if_needed", autospec=True)
|
|
@mock.patch(DRIVER_PATH + "._get_pgroup_vol_snap_name", autospec=True)
|
|
def test_create_volume_from_cgsnapshot(self, mock_get_snap_name,
|
|
mock_extend_if_needed,
|
|
mock_add_to_cgroup):
|
|
vol_name = VOLUME_WITH_CGROUP["name"] + "-cinder"
|
|
snap_name = "consisgroup-4a2f7e3a-312a-40c5-96a8-536b8a0f" \
|
|
"e074-cinder.4a2f7e3a-312a-40c5-96a8-536b8a0fe075."\
|
|
+ vol_name
|
|
mock_get_snap_name.return_value = snap_name
|
|
|
|
self.driver.create_volume_from_snapshot(VOLUME_WITH_CGROUP,
|
|
SNAPSHOT_WITH_CGROUP)
|
|
|
|
self.array.copy_volume.assert_called_with(snap_name, vol_name)
|
|
self.assertTrue(mock_get_snap_name.called)
|
|
self.assertTrue(mock_extend_if_needed.called)
|
|
|
|
self.driver.create_volume_from_snapshot(VOLUME_WITH_CGROUP,
|
|
SNAPSHOT_WITH_CGROUP)
|
|
mock_add_to_cgroup\
|
|
.assert_called_with(self.driver,
|
|
VOLUME_WITH_CGROUP['consistencygroup_id'],
|
|
vol_name)
|
|
|
|
def test_create_cloned_volume(self):
|
|
vol_name = VOLUME["name"] + "-cinder"
|
|
src_name = SRC_VOL["name"] + "-cinder"
|
|
# Branch where extend unneeded
|
|
self.driver.create_cloned_volume(VOLUME, SRC_VOL)
|
|
self.array.copy_volume.assert_called_with(src_name, vol_name)
|
|
self.assertFalse(self.array.extend_volume.called)
|
|
self.assert_error_propagates(
|
|
[self.array.copy_volume],
|
|
self.driver.create_cloned_volume, VOLUME, SRC_VOL)
|
|
self.assertFalse(self.array.extend_volume.called)
|
|
# Branch where extend needed
|
|
SRC_VOL["size"] = 1 # resize so smaller than VOLUME
|
|
self.driver.create_cloned_volume(VOLUME, SRC_VOL)
|
|
expected = [mock.call.copy_volume(src_name, vol_name),
|
|
mock.call.extend_volume(vol_name, 2 * units.Gi)]
|
|
self.array.assert_has_calls(expected)
|
|
self.assert_error_propagates(
|
|
[self.array.copy_volume, self.array.extend_volume],
|
|
self.driver.create_cloned_volume, VOLUME, SRC_VOL)
|
|
SRC_VOL["size"] = 2 # reset size
|
|
|
|
@mock.patch(DRIVER_OBJ + "._add_volume_to_consistency_group",
|
|
autospec=True)
|
|
def test_create_cloned_volume_with_cgroup(self, mock_add_to_cgroup):
|
|
vol_name = VOLUME_WITH_CGROUP["name"] + "-cinder"
|
|
|
|
self.driver.create_cloned_volume(VOLUME_WITH_CGROUP, SRC_VOL)
|
|
|
|
mock_add_to_cgroup\
|
|
.assert_called_with(self.driver,
|
|
VOLUME_WITH_CGROUP['consistencygroup_id'],
|
|
vol_name)
|
|
|
|
def test_delete_volume_already_deleted(self):
|
|
self.array.list_volume_hosts.side_effect = exception.PureAPIException(
|
|
code=400, reason="Volume does not exist")
|
|
self.driver.delete_volume(VOLUME)
|
|
self.assertFalse(self.array.destroy_volume.called)
|
|
self.array.list_volume_hosts.side_effect = None
|
|
self.assert_error_propagates([self.array.destroy_volume],
|
|
self.driver.delete_volume, VOLUME)
|
|
# Testing case where array.destroy_volume returns an exception
|
|
# because volume already deleted
|
|
self.array.destroy_volume.side_effect = exception.PureAPIException(
|
|
code=400, reason="Volume does not exist")
|
|
self.driver.delete_volume(VOLUME)
|
|
self.assertTrue(self.array.destroy_volume.called)
|
|
self.array.destroy_volume.side_effect = None
|
|
self.assert_error_propagates([self.array.destroy_volume],
|
|
self.driver.delete_volume, VOLUME)
|
|
|
|
def test_delete_volume(self):
|
|
vol_name = VOLUME["name"] + "-cinder"
|
|
self.driver.delete_volume(VOLUME)
|
|
expected = [mock.call.destroy_volume(vol_name)]
|
|
self.array.assert_has_calls(expected)
|
|
self.array.destroy_volume.side_effect = exception.PureAPIException(
|
|
code=400, reason="reason")
|
|
self.driver.delete_snapshot(SNAPSHOT)
|
|
self.array.destroy_volume.side_effect = None
|
|
self.assert_error_propagates([self.array.destroy_volume],
|
|
self.driver.delete_volume, VOLUME)
|
|
|
|
def test_delete_connected_volume(self):
|
|
vol_name = VOLUME["name"] + "-cinder"
|
|
host_name_a = "ha"
|
|
host_name_b = "hb"
|
|
self.array.list_volume_hosts.return_value = [{
|
|
"host": host_name_a,
|
|
"lun": 7,
|
|
"name": vol_name,
|
|
"size": 3221225472
|
|
}, {
|
|
"host": host_name_b,
|
|
"lun": 2,
|
|
"name": vol_name,
|
|
"size": 3221225472
|
|
}]
|
|
|
|
self.driver.delete_volume(VOLUME)
|
|
expected = [mock.call.list_volume_hosts(vol_name),
|
|
mock.call.disconnect_host(host_name_a, vol_name),
|
|
mock.call.disconnect_host(host_name_b, vol_name),
|
|
mock.call.destroy_volume(vol_name)]
|
|
self.array.assert_has_calls(expected)
|
|
|
|
def test_create_snapshot(self):
|
|
vol_name = SRC_VOL["name"] + "-cinder"
|
|
self.driver.create_snapshot(SNAPSHOT)
|
|
self.array.create_snapshot.assert_called_with(vol_name,
|
|
SNAPSHOT["name"])
|
|
self.assert_error_propagates([self.array.create_snapshot],
|
|
self.driver.create_snapshot, SNAPSHOT)
|
|
|
|
def test_delete_snapshot(self):
|
|
snap_name = SNAPSHOT["volume_name"] + "-cinder." + SNAPSHOT["name"]
|
|
self.driver.delete_snapshot(SNAPSHOT)
|
|
expected = [mock.call.destroy_volume(snap_name)]
|
|
self.array.assert_has_calls(expected)
|
|
self.array.destroy_volume.side_effect = exception.PureAPIException(
|
|
code=400, reason="reason")
|
|
self.driver.delete_snapshot(SNAPSHOT)
|
|
self.array.destroy_volume.side_effect = None
|
|
self.assert_error_propagates([self.array.destroy_volume],
|
|
self.driver.delete_snapshot, SNAPSHOT)
|
|
|
|
@mock.patch(DRIVER_OBJ + "._connect")
|
|
@mock.patch(DRIVER_OBJ + "._get_target_iscsi_port")
|
|
def test_initialize_connection(self, mock_get_iscsi_port, mock_connection):
|
|
mock_get_iscsi_port.return_value = ISCSI_PORTS[0]
|
|
mock_connection.return_value = {"vol": VOLUME["name"] + "-cinder",
|
|
"lun": 1,
|
|
}
|
|
result = {"driver_volume_type": "iscsi",
|
|
"data": {"target_iqn": TARGET_IQN,
|
|
"target_portal": ISCSI_IPS[0] + ":" + TARGET_PORT,
|
|
"target_lun": 1,
|
|
"target_discovered": True,
|
|
"access_mode": "rw",
|
|
},
|
|
}
|
|
real_result = self.driver.initialize_connection(VOLUME, CONNECTOR)
|
|
self.assertDictMatch(result, real_result)
|
|
mock_get_iscsi_port.assert_called_with()
|
|
mock_connection.assert_called_with(VOLUME, CONNECTOR)
|
|
self.assert_error_propagates([mock_get_iscsi_port, mock_connection],
|
|
self.driver.initialize_connection,
|
|
VOLUME, CONNECTOR)
|
|
|
|
@mock.patch(DRIVER_OBJ + "._choose_target_iscsi_port")
|
|
@mock.patch(DRIVER_OBJ + "._run_iscsiadm_bare")
|
|
def test_get_target_iscsi_port(self, mock_iscsiadm, mock_choose_port):
|
|
self.driver._iscsi_port = ISCSI_PORTS[1]
|
|
self.assertEqual(self.driver._get_target_iscsi_port(), ISCSI_PORTS[1])
|
|
mock_iscsiadm.assert_called_with(["-m", "discovery",
|
|
"-t", "sendtargets",
|
|
"-p", ISCSI_PORTS[1]["portal"]])
|
|
self.assertFalse(mock_choose_port.called)
|
|
mock_iscsiadm.side_effect = [processutils.ProcessExecutionError, None]
|
|
mock_choose_port.return_value = ISCSI_PORTS[2]
|
|
self.assertEqual(self.driver._get_target_iscsi_port(), ISCSI_PORTS[2])
|
|
mock_choose_port.assert_called_with()
|
|
mock_iscsiadm.side_effect = processutils.ProcessExecutionError
|
|
self.assert_error_propagates([mock_choose_port],
|
|
self.driver._get_target_iscsi_port)
|
|
|
|
@mock.patch(DRIVER_OBJ + "._run_iscsiadm_bare")
|
|
def test_choose_target_iscsi_port(self, mock_iscsiadm):
|
|
self.array.list_ports.return_value = PORTS_WITHOUT
|
|
self.assertRaises(exception.PureDriverException,
|
|
self.driver._choose_target_iscsi_port)
|
|
self.array.list_ports.return_value = PORTS_WITH
|
|
self.assertEqual(ISCSI_PORTS[0],
|
|
self.driver._choose_target_iscsi_port())
|
|
self.assert_error_propagates([mock_iscsiadm, self.array.list_ports],
|
|
self.driver._choose_target_iscsi_port)
|
|
|
|
@mock.patch(DRIVER_OBJ + "._get_host", autospec=True)
|
|
@mock.patch(DRIVER_PATH + "._generate_purity_host_name", autospec=True)
|
|
def test_connect(self, mock_generate, mock_host):
|
|
vol_name = VOLUME["name"] + "-cinder"
|
|
result = {"vol": vol_name, "lun": 1}
|
|
# Branch where host already exists
|
|
mock_host.return_value = PURE_HOST
|
|
self.array.connect_host.return_value = {"vol": vol_name, "lun": 1}
|
|
real_result = self.driver._connect(VOLUME, CONNECTOR)
|
|
self.assertEqual(result, real_result)
|
|
mock_host.assert_called_with(self.driver, CONNECTOR)
|
|
self.assertFalse(mock_generate.called)
|
|
self.assertFalse(self.array.create_host.called)
|
|
self.array.connect_host.assert_called_with(PURE_HOST_NAME, vol_name)
|
|
# Branch where new host is created
|
|
mock_host.return_value = None
|
|
mock_generate.return_value = PURE_HOST_NAME
|
|
real_result = self.driver._connect(VOLUME, CONNECTOR)
|
|
mock_host.assert_called_with(self.driver, CONNECTOR)
|
|
mock_generate.assert_called_with(HOSTNAME)
|
|
self.array.create_host.assert_called_with(PURE_HOST_NAME,
|
|
iqnlist=[INITIATOR_IQN])
|
|
self.assertEqual(result, real_result)
|
|
# Branch where host is needed
|
|
mock_generate.reset_mock()
|
|
self.array.reset_mock()
|
|
self.assert_error_propagates(
|
|
[mock_host, mock_generate, self.array.connect_host,
|
|
self.array.create_host],
|
|
self.driver._connect, VOLUME, CONNECTOR)
|
|
|
|
@mock.patch(DRIVER_OBJ + "._get_host", autospec=True)
|
|
def test_connect_already_connected(self, mock_host):
|
|
mock_host.return_value = PURE_HOST
|
|
expected = {"host": PURE_HOST_NAME, "lun": 1}
|
|
self.array.list_volume_hosts.return_value = \
|
|
[expected, {"host": "extra", "lun": 2}]
|
|
self.array.connect_host.side_effect = exception.PureAPIException(
|
|
code=400, reason="Connection already exists")
|
|
actual = self.driver._connect(VOLUME, CONNECTOR)
|
|
self.assertEqual(expected, actual)
|
|
self.assertTrue(self.array.connect_host.called)
|
|
self.assertTrue(self.array.list_volume_hosts)
|
|
|
|
@mock.patch(DRIVER_OBJ + "._get_host", autospec=True)
|
|
def test_connect_already_connected_list_hosts_empty(self, mock_host):
|
|
mock_host.return_value = PURE_HOST
|
|
self.array.list_volume_hosts.return_value = []
|
|
self.array.connect_host.side_effect = exception.PureAPIException(
|
|
code=400, reason="Connection already exists")
|
|
self.assertRaises(exception.PureDriverException,
|
|
lambda: self.driver._connect(VOLUME, CONNECTOR))
|
|
self.assertTrue(self.array.connect_host.called)
|
|
self.assertTrue(self.array.list_volume_hosts)
|
|
|
|
@mock.patch(DRIVER_OBJ + "._get_host", autospec=True)
|
|
def test_connect_already_connected_list_hosts_exception(self, mock_host):
|
|
mock_host.return_value = PURE_HOST
|
|
self.array.list_volume_hosts.side_effect = \
|
|
exception.PureAPIException(code=400, reason="")
|
|
self.array.connect_host.side_effect = exception.PureAPIException(
|
|
code=400, reason="Connection already exists")
|
|
self.assertRaises(exception.PureAPIException,
|
|
lambda: self.driver._connect(VOLUME, CONNECTOR))
|
|
self.assertTrue(self.array.connect_host.called)
|
|
self.assertTrue(self.array.list_volume_hosts)
|
|
|
|
def test_get_host(self):
|
|
good_host = PURE_HOST.copy()
|
|
good_host.update(iqn=["another-wrong-iqn", INITIATOR_IQN])
|
|
bad_host = {"name": "bad-host", "iqn": ["wrong-iqn"]}
|
|
self.array.list_hosts.return_value = [bad_host]
|
|
real_result = self.driver._get_host(CONNECTOR)
|
|
self.assertIs(real_result, None)
|
|
self.array.list_hosts.return_value.append(good_host)
|
|
real_result = self.driver._get_host(CONNECTOR)
|
|
self.assertEqual(real_result, good_host)
|
|
self.assert_error_propagates([self.array.list_hosts],
|
|
self.driver._get_host, CONNECTOR)
|
|
|
|
@mock.patch(DRIVER_OBJ + "._get_host", autospec=True)
|
|
def test_terminate_connection(self, mock_host):
|
|
vol_name = VOLUME["name"] + "-cinder"
|
|
mock_host.return_value = {"name": "some-host"}
|
|
# Branch with manually created host
|
|
self.driver.terminate_connection(VOLUME, CONNECTOR)
|
|
self.array.disconnect_host.assert_called_with("some-host", vol_name)
|
|
self.assertFalse(self.array.list_host_connections.called)
|
|
self.assertFalse(self.array.delete_host.called)
|
|
# Branch with host added to host group
|
|
self.array.reset_mock()
|
|
self.array.list_host_connections.return_value = []
|
|
mock_host.return_value = PURE_HOST.copy()
|
|
mock_host.return_value.update(hgroup="some-group")
|
|
self.driver.terminate_connection(VOLUME, CONNECTOR)
|
|
self.array.disconnect_host.assert_called_with(PURE_HOST_NAME, vol_name)
|
|
self.assertTrue(self.array.list_host_connections.called)
|
|
self.assertTrue(self.array.delete_host.called)
|
|
# Branch with host still having connected volumes
|
|
self.array.reset_mock()
|
|
self.array.list_host_connections.return_value = [
|
|
{"lun": 2, "name": PURE_HOST_NAME, "vol": "some-vol"}]
|
|
mock_host.return_value = PURE_HOST
|
|
self.driver.terminate_connection(VOLUME, CONNECTOR)
|
|
self.array.disconnect_host.assert_called_with(PURE_HOST_NAME, vol_name)
|
|
self.array.list_host_connections.assert_called_with(PURE_HOST_NAME,
|
|
private=True)
|
|
self.assertFalse(self.array.delete_host.called)
|
|
# Branch where host gets deleted
|
|
self.array.reset_mock()
|
|
self.array.list_host_connections.return_value = []
|
|
self.driver.terminate_connection(VOLUME, CONNECTOR)
|
|
self.array.disconnect_host.assert_called_with(PURE_HOST_NAME, vol_name)
|
|
self.array.list_host_connections.assert_called_with(PURE_HOST_NAME,
|
|
private=True)
|
|
self.array.delete_host.assert_called_with(PURE_HOST_NAME)
|
|
# Branch where connection is missing and the host is still deleted
|
|
self.array.reset_mock()
|
|
self.array.disconnect_host.side_effect = exception.PureAPIException(
|
|
code=400, reason="reason")
|
|
self.driver.terminate_connection(VOLUME, CONNECTOR)
|
|
self.array.disconnect_host.assert_called_with(PURE_HOST_NAME, vol_name)
|
|
self.array.list_host_connections.assert_called_with(PURE_HOST_NAME,
|
|
private=True)
|
|
self.array.delete_host.assert_called_with(PURE_HOST_NAME)
|
|
# Branch where an unexpected exception occurs
|
|
self.array.reset_mock()
|
|
self.array.disconnect_host.side_effect = exception.PureAPIException(
|
|
code=500, reason="unexpected exception")
|
|
self.assertRaises(exception.PureAPIException,
|
|
self.driver.terminate_connection, VOLUME, CONNECTOR)
|
|
self.array.disconnect_host.assert_called_with(PURE_HOST_NAME, vol_name)
|
|
self.assertFalse(self.array.list_host_connections.called)
|
|
self.assertFalse(self.array.delete_host.called)
|
|
|
|
def test_get_volume_stats(self):
|
|
self.assertEqual(self.driver.get_volume_stats(), {})
|
|
self.array.get_array.return_value = SPACE_INFO
|
|
result = {"volume_backend_name": VOLUME_BACKEND_NAME,
|
|
"vendor_name": "Pure Storage",
|
|
"driver_version": self.driver.VERSION,
|
|
"storage_protocol": "iSCSI",
|
|
"total_capacity_gb": TOTAL_SPACE,
|
|
"free_capacity_gb": FREE_SPACE,
|
|
"reserved_percentage": 0,
|
|
"consistencygroup_support": True
|
|
}
|
|
real_result = self.driver.get_volume_stats(refresh=True)
|
|
self.assertDictMatch(result, real_result)
|
|
self.assertDictMatch(result, self.driver._stats)
|
|
|
|
def test_extend_volume(self):
|
|
vol_name = VOLUME["name"] + "-cinder"
|
|
self.driver.extend_volume(VOLUME, 3)
|
|
self.array.extend_volume.assert_called_with(vol_name, 3 * units.Gi)
|
|
self.assert_error_propagates([self.array.extend_volume],
|
|
self.driver.extend_volume, VOLUME, 3)
|
|
|
|
def test_get_pgroup_name_from_id(self):
|
|
id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074"
|
|
expected_name = "consisgroup-%s-cinder" % id
|
|
actual_name = pure._get_pgroup_name_from_id(id)
|
|
self.assertEqual(expected_name, actual_name)
|
|
|
|
def test_get_pgroup_snap_suffix(self):
|
|
cgsnap = mock.Mock()
|
|
cgsnap.id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074"
|
|
expected_suffix = "cgsnapshot-%s-cinder" % cgsnap.id
|
|
actual_suffix = pure._get_pgroup_snap_suffix(cgsnap)
|
|
self.assertEqual(expected_suffix, actual_suffix)
|
|
|
|
def test_get_pgroup_snap_name(self):
|
|
cg_id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074"
|
|
cgsnap_id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe075"
|
|
|
|
mock_cgsnap = mock.Mock()
|
|
mock_cgsnap.consistencygroup_id = cg_id
|
|
mock_cgsnap.id = cgsnap_id
|
|
expected_name = "consisgroup-%(cg)s-cinder.cgsnapshot-%(snap)s-cinder"\
|
|
% {"cg": cg_id, "snap": cgsnap_id}
|
|
|
|
actual_name = pure._get_pgroup_snap_name(mock_cgsnap)
|
|
|
|
self.assertEqual(expected_name, actual_name)
|
|
|
|
def test_get_pgroup_vol_snap_name(self):
|
|
cg_id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074"
|
|
cgsnap_id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe075"
|
|
volume_name = "volume-4a2f7e3a-312a-40c5-96a8-536b8a0fe075"
|
|
|
|
mock_snap = mock.Mock()
|
|
mock_snap.cgsnapshot = mock.Mock()
|
|
mock_snap.cgsnapshot.consistencygroup_id = cg_id
|
|
mock_snap.cgsnapshot.id = cgsnap_id
|
|
mock_snap.volume_name = volume_name
|
|
|
|
expected_name = "consisgroup-%(cg)s-cinder.cgsnapshot-%(snap)s-cinder"\
|
|
".%(vol)s-cinder" % {"cg": cg_id,
|
|
"snap": cgsnap_id,
|
|
"vol": volume_name}
|
|
|
|
actual_name = pure._get_pgroup_vol_snap_name(mock_snap)
|
|
|
|
self.assertEqual(expected_name, actual_name)
|
|
|
|
def test_create_consistencygroup(self):
|
|
mock_cgroup = mock.Mock()
|
|
mock_cgroup.id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074"
|
|
|
|
model_update = self.driver.create_consistencygroup(None, mock_cgroup)
|
|
|
|
expected_name = pure._get_pgroup_name_from_id(mock_cgroup.id)
|
|
self.array.create_pgroup.assert_called_with(expected_name)
|
|
self.assertEqual({'status': 'available'}, model_update)
|
|
|
|
self.assert_error_propagates(
|
|
[self.array.create_pgroup],
|
|
self.driver.create_consistencygroup, None, mock_cgroup)
|
|
|
|
@mock.patch(DRIVER_OBJ + ".delete_volume", autospec=True)
|
|
def test_delete_consistencygroup(self, mock_delete_volume):
|
|
mock_cgroup = mock.MagicMock()
|
|
mock_cgroup.id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074"
|
|
mock_cgroup['status'] = "deleted"
|
|
mock_context = mock.Mock()
|
|
self.driver.db = mock.Mock()
|
|
mock_volume = mock.MagicMock()
|
|
expected_volumes = [mock_volume]
|
|
self.driver.db.volume_get_all_by_group.return_value = expected_volumes
|
|
|
|
model_update, volumes = \
|
|
self.driver.delete_consistencygroup(mock_context, mock_cgroup)
|
|
|
|
expected_name = pure._get_pgroup_name_from_id(mock_cgroup.id)
|
|
self.array.delete_pgroup.assert_called_with(expected_name)
|
|
self.assertEqual(expected_volumes, volumes)
|
|
self.assertEqual(mock_cgroup['status'], model_update['status'])
|
|
mock_delete_volume.assert_called_with(self.driver, mock_volume)
|
|
|
|
self.array.delete_pgroup.side_effect = exception.PureAPIException(
|
|
code=400, reason="Protection group has been destroyed.")
|
|
self.driver.delete_consistencygroup(mock_context, mock_cgroup)
|
|
self.array.delete_pgroup.assert_called_with(expected_name)
|
|
mock_delete_volume.assert_called_with(self.driver, mock_volume)
|
|
|
|
self.array.delete_pgroup.side_effect = exception.PureAPIException(
|
|
code=400, reason="Protection group does not exist")
|
|
self.driver.delete_consistencygroup(mock_context, mock_cgroup)
|
|
self.array.delete_pgroup.assert_called_with(expected_name)
|
|
mock_delete_volume.assert_called_with(self.driver, mock_volume)
|
|
|
|
self.array.delete_pgroup.side_effect = exception.PureAPIException(
|
|
code=400, reason="Some other error")
|
|
self.assertRaises(exception.PureAPIException,
|
|
self.driver.delete_consistencygroup,
|
|
mock_context,
|
|
mock_volume)
|
|
|
|
self.array.delete_pgroup.side_effect = exception.PureAPIException(
|
|
code=500, reason="Another different error")
|
|
self.assertRaises(exception.PureAPIException,
|
|
self.driver.delete_consistencygroup,
|
|
mock_context,
|
|
mock_volume)
|
|
|
|
self.array.delete_pgroup.side_effect = None
|
|
self.assert_error_propagates(
|
|
[self.array.delete_pgroup],
|
|
self.driver.delete_consistencygroup, mock_context, mock_cgroup)
|
|
|
|
def test_create_cgsnapshot(self):
|
|
mock_cgsnap = mock.Mock()
|
|
mock_cgsnap.id = "4a2f7e3a-312a-40c5-96a8-536b8a0fe074"
|
|
mock_cgsnap.consistencygroup_id = \
|
|
"4a2f7e3a-312a-40c5-96a8-536b8a0fe075"
|
|
mock_context = mock.Mock()
|
|
self.driver.db = mock.Mock()
|
|
mock_snap = mock.MagicMock()
|
|
expected_snaps = [mock_snap]
|
|
self.driver.db.snapshot_get_all_for_cgsnapshot.return_value = \
|
|
expected_snaps
|
|
|
|
model_update, snapshots = \
|
|
self.driver.create_cgsnapshot(mock_context, mock_cgsnap)
|
|
|
|
expected_pgroup_name = \
|
|
pure._get_pgroup_name_from_id(mock_cgsnap.consistencygroup_id)
|
|
expected_snap_suffix = pure._get_pgroup_snap_suffix(mock_cgsnap)
|
|
self.array.create_pgroup_snapshot\
|
|
.assert_called_with(expected_pgroup_name, expected_snap_suffix)
|
|
self.assertEqual({'status': 'available'}, model_update)
|
|
self.assertEqual(expected_snaps, snapshots)
|
|
self.assertEqual('available', mock_snap.status)
|
|
|
|
self.assert_error_propagates(
|
|
[self.array.create_pgroup_snapshot],
|
|
self.driver.create_cgsnapshot, mock_context, mock_cgsnap)
|
|
|
|
@mock.patch(DRIVER_PATH + "._get_pgroup_snap_name", autospec=True)
|
|
def test_delete_cgsnapshot(self, mock_get_snap_name):
|
|
snap_name = "consisgroup-4a2f7e3a-312a-40c5-96a8-536b8a0f" \
|
|
"e074-cinder.4a2f7e3a-312a-40c5-96a8-536b8a0fe075"
|
|
mock_get_snap_name.return_value = snap_name
|
|
mock_cgsnap = mock.Mock()
|
|
mock_cgsnap.status = 'deleted'
|
|
mock_context = mock.Mock()
|
|
mock_snap = mock.MagicMock()
|
|
expected_snaps = [mock_snap]
|
|
self.driver.db = mock.Mock()
|
|
self.driver.db.snapshot_get_all_for_cgsnapshot.return_value = \
|
|
expected_snaps
|
|
|
|
model_update, snapshots = \
|
|
self.driver.delete_cgsnapshot(mock_context, mock_cgsnap)
|
|
|
|
self.array.delete_pgroup_snapshot.assert_called_with(snap_name)
|
|
self.assertEqual({'status': mock_cgsnap.status}, model_update)
|
|
self.assertEqual(expected_snaps, snapshots)
|
|
self.assertEqual('deleted', mock_snap.status)
|
|
|
|
self.array.delete_pgroup_snapshot.side_effect = \
|
|
exception.PureAPIException(
|
|
code=400,
|
|
reason="Protection group snapshot has been destroyed."
|
|
)
|
|
self.driver.delete_cgsnapshot(mock_context, mock_cgsnap)
|
|
self.array.delete_pgroup_snapshot.assert_called_with(snap_name)
|
|
|
|
self.array.delete_pgroup_snapshot.side_effect = \
|
|
exception.PureAPIException(
|
|
code=400,
|
|
reason="Protection group snapshot does not exist"
|
|
)
|
|
self.driver.delete_cgsnapshot(mock_context, mock_cgsnap)
|
|
self.array.delete_pgroup_snapshot.assert_called_with(snap_name)
|
|
|
|
self.array.delete_pgroup_snapshot.side_effect = \
|
|
exception.PureAPIException(
|
|
code=400,
|
|
reason="Some other error"
|
|
)
|
|
self.assertRaises(exception.PureAPIException,
|
|
self.driver.delete_cgsnapshot,
|
|
mock_context,
|
|
mock_cgsnap)
|
|
|
|
self.array.delete_pgroup_snapshot.side_effect = \
|
|
exception.PureAPIException(
|
|
code=500,
|
|
reason="Another different error"
|
|
)
|
|
self.assertRaises(exception.PureAPIException,
|
|
self.driver.delete_cgsnapshot,
|
|
mock_context,
|
|
mock_cgsnap)
|
|
|
|
self.array.delete_pgroup_snapshot.side_effect = None
|
|
|
|
self.assert_error_propagates(
|
|
[self.array.delete_pgroup_snapshot],
|
|
self.driver.delete_cgsnapshot, mock_context, mock_cgsnap)
|
|
|
|
|
|
class FlashArrayBaseTestCase(test.TestCase):
|
|
|
|
def setUp(self):
|
|
super(FlashArrayBaseTestCase, self).setUp()
|
|
array = FakeFlashArray()
|
|
array._target = TARGET
|
|
array._rest_version = REST_VERSION
|
|
array._root_url = "https://%s/api/%s/" % (TARGET, REST_VERSION)
|
|
array._api_token = API_TOKEN
|
|
self.array = array
|
|
|
|
def assert_error_propagates(self, mocks, func, *args, **kwargs):
|
|
"""Assert that errors from mocks propagate to func.
|
|
|
|
Fail if exceptions raised by mocks are not seen when calling
|
|
func(*args, **kwargs). Ensure that we are really seeing exceptions
|
|
from the mocks by failing if just running func(*args, **kargs) raises
|
|
an exception itself.
|
|
"""
|
|
func(*args, **kwargs)
|
|
for mock_func in mocks:
|
|
mock_func.side_effect = exception.PureAPIException(reason="reason")
|
|
self.assertRaises(exception.PureAPIException,
|
|
func, *args, **kwargs)
|
|
mock_func.side_effect = None
|
|
|
|
|
|
class FlashArrayInitTestCase(FlashArrayBaseTestCase):
|
|
|
|
@mock.patch(ARRAY_OBJ + "._start_session", autospec=True)
|
|
@mock.patch(ARRAY_OBJ + "._choose_rest_version", autospec=True)
|
|
@mock.patch(DRIVER_PATH + ".urllib2.build_opener", autospec=True)
|
|
def test_init(self, mock_build_opener, mock_choose, mock_start):
|
|
opener = mock.Mock()
|
|
mock_build_opener.return_value = opener
|
|
mock_choose.return_value = REST_VERSION
|
|
array = pure.FlashArray(TARGET, API_TOKEN)
|
|
mock_choose.assert_called_with(array)
|
|
mock_start.assert_called_with(array)
|
|
self.assertEqual(array._target, TARGET)
|
|
self.assertEqual(array._api_token, API_TOKEN)
|
|
self.assertEqual(array._rest_version, REST_VERSION)
|
|
self.assertIs(array._opener, opener)
|
|
self.assert_error_propagates([mock_choose, mock_start],
|
|
pure.FlashArray, TARGET, API_TOKEN)
|
|
|
|
|
|
class FlashArrayHttpRequestTestCase(FlashArrayBaseTestCase):
|
|
|
|
def setUp(self):
|
|
super(FlashArrayHttpRequestTestCase, self).setUp()
|
|
self.method = "POST"
|
|
self.path = "path"
|
|
self.path_template = "https://%s/api/%s/%s"
|
|
self.full_path = self.path_template % (TARGET, REST_VERSION,
|
|
self.path)
|
|
self.headers = {"Content-Type": "application/json"}
|
|
self.data = {"list": [1, 2, 3]}
|
|
self.data_json = json.dumps(self.data)
|
|
self.response_json = '[{"hello": "world"}, "!"]'
|
|
self.result = json.loads(self.response_json)
|
|
self.error_msg = "error-msg"
|
|
self.response = mock.Mock(spec=["read", "readline", "info"])
|
|
self.response.read.return_value = self.response_json
|
|
self.response.read.side_effect = None
|
|
self.response.info.return_value = self.headers
|
|
self.response.info.side_effect = None
|
|
|
|
def make_call(self, method=None, path=None, data=None):
|
|
method = method if method else self.method
|
|
path = path if path else self.full_path
|
|
data = data if data else self.data_json
|
|
return mock.call(FakeRequest(method, path, headers=self.headers), data)
|
|
|
|
def test_http_request_success(self):
|
|
self.array._opener.open.return_value = self.response
|
|
real_result = self.array._http_request(
|
|
self.method, self.path, self.data)
|
|
self.assertEqual(self.result, real_result)
|
|
self.assertEqual(self.array._opener.open.call_args_list,
|
|
[self.make_call()])
|
|
|
|
def test_http_request_401_error(self):
|
|
self.array._opener.open.return_value = self.response
|
|
error = urllib2.HTTPError(self.full_path, 401, self.error_msg,
|
|
None, self.response)
|
|
self.array._opener.open.side_effect = iter([error] +
|
|
[self.response] * 2)
|
|
real_result = self.array._http_request(
|
|
self.method, self.path, self.data)
|
|
self.assertEqual(self.result, real_result)
|
|
expected = [self.make_call(),
|
|
self.make_call("POST", self.path_template %
|
|
(TARGET, REST_VERSION, "auth/session"),
|
|
json.dumps({"api_token": API_TOKEN})),
|
|
self.make_call()]
|
|
self.assertEqual(self.array._opener.open.call_args_list, expected)
|
|
self.array._opener.open.reset_mock()
|
|
self.array._opener.open.side_effect = iter([error, error])
|
|
self.assertRaises(exception.PureAPIException,
|
|
self.array._http_request,
|
|
self.method, self.path, self.data)
|
|
self.array._opener.open.reset_mock()
|
|
self.array._opener.open.side_effect = iter([error, self.response,
|
|
error])
|
|
self.assertRaises(exception.PureAPIException,
|
|
self.array._http_request,
|
|
self.method, self.path, self.data)
|
|
|
|
@mock.patch(ARRAY_OBJ + "._choose_rest_version", autospec=True)
|
|
def test_http_request_450_error(self, mock_choose):
|
|
mock_choose.return_value = "1.1"
|
|
error = urllib2.HTTPError(self.full_path, 450, self.error_msg,
|
|
None, self.response)
|
|
self.array._opener.open.side_effect = iter([error, self.response])
|
|
real_result = self.array._http_request(
|
|
self.method, self.path, self.data)
|
|
self.assertEqual(self.result, real_result)
|
|
expected = [self.make_call(),
|
|
self.make_call(path=self.path_template %
|
|
(TARGET, "1.1", self.path))]
|
|
self.assertEqual(self.array._opener.open.call_args_list, expected)
|
|
mock_choose.assert_called_with(self.array)
|
|
self.array._opener.open.side_effect = error
|
|
self.assertRaises(exception.PureAPIException,
|
|
self.array._http_request,
|
|
self.method, self.path, self.data)
|
|
self.array._opener.open.reset_mock()
|
|
mock_choose.reset_mock()
|
|
self.array._opener.open.side_effect = error
|
|
mock_choose.side_effect = exception.PureAPIException(reason="reason")
|
|
self.assertRaises(exception.PureAPIException,
|
|
self.array._http_request,
|
|
self.method, self.path, self.data)
|
|
|
|
def test_http_request_http_error(self):
|
|
self.array._opener.open.return_value = self.response
|
|
error = urllib2.HTTPError(self.full_path, 500, self.error_msg,
|
|
None, self.response)
|
|
self.array._opener.open.side_effect = error
|
|
self.assertRaises(exception.PureAPIException,
|
|
self.array._http_request,
|
|
self.method, self.path, self.data)
|
|
self.assertEqual(self.array._opener.open.call_args_list,
|
|
[self.make_call()])
|
|
|
|
def test_http_request_url_error(self):
|
|
self.array._opener.open.return_value = self.response
|
|
error = urllib2.URLError(self.error_msg)
|
|
self.array._opener.open.side_effect = error
|
|
# try/except used to ensure is instance of type but not subtype
|
|
try:
|
|
self.array._http_request(self.method, self.path, self.data)
|
|
except exception.PureDriverException as err:
|
|
self.assertFalse(isinstance(err, exception.PureAPIException))
|
|
else:
|
|
self.assertTrue(False, "expected failure, but passed")
|
|
self.assertEqual(self.array._opener.open.call_args_list,
|
|
[self.make_call()])
|
|
|
|
def test_http_request_other_error(self):
|
|
self.array._opener.open.return_value = self.response
|
|
self.assert_error_propagates([self.array._opener.open],
|
|
self.array._http_request,
|
|
self.method, self.path, self.data)
|
|
|
|
# Test with _http_requests rather than rest calls to ensure
|
|
# root_url change happens properly
|
|
def test_choose_rest_version(self):
|
|
response_string = '{"version": ["0.1", "1.4", "1.3", "1.0"]}'
|
|
self.response.read.return_value = response_string
|
|
self.array._opener.open.return_value = self.response
|
|
result = self.array._choose_rest_version()
|
|
self.assertEqual(result, "1.3")
|
|
self.array._opener.open.assert_called_with(FakeRequest(
|
|
"GET", "https://%s/api/api_version" % TARGET,
|
|
headers=self.headers), "null")
|
|
self.array._opener.open.reset_mock()
|
|
self.response.read.return_value = '{"version": ["0.1", "1.4"]}'
|
|
self.assertRaises(exception.PureDriverException,
|
|
self.array._choose_rest_version)
|
|
|
|
|
|
@mock.patch(ARRAY_OBJ + "._http_request", autospec=True)
|
|
class FlashArrayRESTTestCase(FlashArrayBaseTestCase):
|
|
|
|
def setUp(self):
|
|
super(FlashArrayRESTTestCase, self).setUp()
|
|
self.kwargs = {"kwarg1": "val1", "kwarg2": "val2"}
|
|
self.result = "expected_return"
|
|
|
|
def test_choose_rest_version(self, mock_req):
|
|
mock_req.return_value = {"version": ["0.1", "1.3", "1.1", "1.0"]}
|
|
self.assert_error_propagates([mock_req],
|
|
self.array._choose_rest_version)
|
|
|
|
def test_start_session(self, mock_req):
|
|
self.array._start_session()
|
|
data = {"api_token": API_TOKEN}
|
|
mock_req.assert_called_with(self.array, "POST", "auth/session",
|
|
data, reestablish_session=False)
|
|
self.assert_error_propagates([mock_req], self.array._start_session)
|
|
|
|
def test_get_array(self, mock_req):
|
|
mock_req.return_value = self.result
|
|
result = self.array.get_array(**self.kwargs)
|
|
self.assertEqual(result, self.result)
|
|
mock_req.assert_called_with(self.array, "GET", "array", self.kwargs)
|
|
self.assert_error_propagates([mock_req], self.array.get_array,
|
|
**self.kwargs)
|
|
|
|
def test_create_volume(self, mock_req):
|
|
mock_req.return_value = self.result
|
|
result = self.array.create_volume("vol-name", "5G")
|
|
self.assertEqual(result, self.result)
|
|
mock_req.assert_called_with(self.array, "POST", "volume/vol-name",
|
|
{"size": "5G"})
|
|
self.assert_error_propagates([mock_req], self.array.create_volume,
|
|
"vol-name", "5G")
|
|
|
|
def test_copy_volume(self, mock_req):
|
|
mock_req.return_value = self.result
|
|
result = self.array.copy_volume("src-name", "dest-name")
|
|
self.assertEqual(result, self.result)
|
|
mock_req.assert_called_with(self.array, "POST", "volume/dest-name",
|
|
{"source": "src-name"})
|
|
self.assert_error_propagates([mock_req], self.array.copy_volume,
|
|
"dest-name", "src-name")
|
|
|
|
def test_create_snapshot(self, mock_req):
|
|
mock_req.return_value = [self.result, "second-arg"]
|
|
result = self.array.create_snapshot("vol-name", "suff")
|
|
self.assertEqual(result, self.result)
|
|
mock_req.assert_called_with(
|
|
self.array, "POST", "volume",
|
|
{"source": ["vol-name"], "suffix": "suff", "snap": True})
|
|
self.assert_error_propagates([mock_req], self.array.create_snapshot,
|
|
"vol-name", "suff")
|
|
|
|
def test_destroy_volume(self, mock_req):
|
|
mock_req.return_value = self.result
|
|
result = self.array.destroy_volume("vol-name")
|
|
self.assertEqual(result, self.result)
|
|
mock_req.assert_called_with(self.array, "DELETE", "volume/vol-name")
|
|
self.assert_error_propagates([mock_req], self.array.destroy_volume,
|
|
"vol-name")
|
|
|
|
def test_extend_volume(self, mock_req):
|
|
mock_req.return_value = self.result
|
|
result = self.array.extend_volume("vol-name", "5G")
|
|
self.assertEqual(result, self.result)
|
|
mock_req.assert_called_with(self.array, "PUT", "volume/vol-name",
|
|
{"size": "5G", "truncate": False})
|
|
self.assert_error_propagates([mock_req], self.array.extend_volume,
|
|
"vol-name", "5G")
|
|
|
|
def test_list_hosts(self, mock_req):
|
|
mock_req.return_value = self.result
|
|
result = self.array.list_hosts(**self.kwargs)
|
|
self.assertEqual(result, self.result)
|
|
mock_req.assert_called_with(self.array, "GET", "host", self.kwargs)
|
|
self.assert_error_propagates([mock_req], self.array.list_hosts,
|
|
**self.kwargs)
|
|
|
|
def test_create_host(self, mock_req):
|
|
mock_req.return_value = self.result
|
|
host_name = "host1"
|
|
params = {'iqnlist': ['iqn1']}
|
|
result = self.array.create_host(host_name, iqnlist=['iqn1'])
|
|
self.assertEqual(result, self.result)
|
|
mock_req.assert_called_with(self.array, "POST", "host/" + host_name,
|
|
params)
|
|
self.assert_error_propagates([mock_req], self.array.create_host,
|
|
host_name, iqnlist=['iqn1'])
|
|
|
|
def test_delete_host(self, mock_req):
|
|
mock_req.return_value = self.result
|
|
host_name = "host1"
|
|
result = self.array.delete_host(host_name)
|
|
self.assertEqual(result, self.result)
|
|
mock_req.assert_called_with(self.array, "DELETE", "host/" + host_name)
|
|
self.assert_error_propagates([mock_req], self.array.delete_host,
|
|
host_name)
|
|
|
|
def test_connect_host(self, mock_req):
|
|
mock_req.return_value = self.result
|
|
result = self.array.connect_host("host-name", "vol-name",
|
|
**self.kwargs)
|
|
self.assertEqual(result, self.result)
|
|
mock_req.assert_called_with(self.array, "POST",
|
|
"host/host-name/volume/vol-name",
|
|
self.kwargs)
|
|
self.assert_error_propagates([mock_req], self.array.connect_host,
|
|
"host-name", "vol-name", **self.kwargs)
|
|
|
|
def test_disconnect_host(self, mock_req):
|
|
mock_req.return_value = self.result
|
|
result = self.array.disconnect_host("host-name", "vol-name")
|
|
self.assertEqual(result, self.result)
|
|
mock_req.assert_called_with(self.array, "DELETE",
|
|
"host/host-name/volume/vol-name")
|
|
self.assert_error_propagates([mock_req], self.array.disconnect_host,
|
|
"host-name", "vol-name")
|
|
|
|
def test_list_ports(self, mock_req):
|
|
mock_req.return_value = self.result
|
|
result = self.array.list_ports(**self.kwargs)
|
|
self.assertEqual(result, self.result)
|
|
mock_req.assert_called_with(self.array, "GET", "port", self.kwargs)
|
|
self.assert_error_propagates([mock_req], self.array.list_ports,
|
|
**self.kwargs)
|
|
|
|
def test_list_volume_hosts(self, mock_req):
|
|
mock_req.return_value = self.result
|
|
result = self.array.list_volume_hosts("vol-name")
|
|
self.assertEqual(result, self.result)
|
|
mock_req.assert_called_with(self.array, "GET", "volume/vol-name/host")
|
|
self.assert_error_propagates([mock_req], self.array.list_volume_hosts,
|
|
"vol-name")
|
|
|
|
def test_create_pgroup(self, mock_req):
|
|
mock_req.return_value = self.result
|
|
pgroup_name = "cgroup_id"
|
|
result = self.array.create_pgroup(pgroup_name)
|
|
self.assertEqual(self.result, result)
|
|
req_url = "pgroup/" + pgroup_name
|
|
mock_req.assert_called_with(self.array, "POST", req_url)
|
|
self.assert_error_propagates([mock_req], self.array.create_pgroup,
|
|
pgroup_name)
|
|
|
|
def test_delete_pgroup(self, mock_req):
|
|
mock_req.return_value = self.result
|
|
pgroup_name = "cgroup_id"
|
|
result = self.array.delete_pgroup(pgroup_name)
|
|
self.assertEqual(self.result, result)
|
|
req_url = "pgroup/" + pgroup_name
|
|
mock_req.assert_called_with(self.array, "DELETE", req_url)
|
|
self.assert_error_propagates([mock_req], self.array.delete_pgroup,
|
|
pgroup_name)
|
|
|
|
def test_create_pgroup_snapshot(self, mock_req):
|
|
mock_req.return_value = self.result
|
|
pgroup_name = "cgroup_id"
|
|
snap_suffix = "snap_suffix"
|
|
result = self.array.create_pgroup_snapshot(pgroup_name, snap_suffix)
|
|
self.assertEqual(self.result, result)
|
|
expected_params = {
|
|
"snap": True,
|
|
"suffix": snap_suffix,
|
|
"source": [pgroup_name]
|
|
}
|
|
mock_req.assert_called_with(self.array, "POST", "pgroup",
|
|
expected_params)
|
|
self.assert_error_propagates([mock_req],
|
|
self.array.create_pgroup_snapshot,
|
|
pgroup_name, snap_suffix)
|
|
|
|
def test_delete_pgroup_snapshot(self, mock_req):
|
|
mock_req.return_value = self.result
|
|
snapshot_name = "snap1"
|
|
result = self.array.delete_pgroup_snapshot(snapshot_name)
|
|
self.assertEqual(self.result, result)
|
|
req_url = "pgroup/" + snapshot_name
|
|
mock_req.assert_called_with(self.array, "DELETE", req_url)
|
|
self.assert_error_propagates([mock_req],
|
|
self.array.delete_pgroup_snapshot,
|
|
snapshot_name)
|
|
|
|
def test_add_volume_to_pgroup(self, mock_req):
|
|
mock_req.return_value = self.result
|
|
pgroup_name = "cgroup_id"
|
|
volume_name = "myvol-1"
|
|
expected_params = {"addvollist": [volume_name]}
|
|
result = self.array.add_volume_to_pgroup(pgroup_name, volume_name)
|
|
self.assertEqual(self.result, result)
|
|
req_url = "pgroup/" + pgroup_name
|
|
mock_req.assert_called_with(self.array, "PUT", req_url,
|
|
expected_params)
|
|
self.assert_error_propagates([mock_req],
|
|
self.array.add_volume_to_pgroup,
|
|
pgroup_name, volume_name)
|
|
|
|
|
|
class FakeFlashArray(pure.FlashArray):
|
|
|
|
def __init__(self):
|
|
self._opener = mock.Mock()
|
|
|
|
|
|
class FakeRequest(urllib2.Request):
|
|
|
|
def __init__(self, method, *args, **kwargs):
|
|
urllib2.Request.__init__(self, *args, **kwargs)
|
|
self.get_method = lambda: method
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, urllib2.Request):
|
|
return False
|
|
return (self.get_method() == other.get_method() and
|
|
self.get_full_url() == other.get_full_url() and
|
|
self.header_items() == other.header_items())
|
|
|
|
def __ne__(self, other):
|
|
return not (self == other)
|