# Copyright (c) 2015 Coho Data, 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 binascii import errno import mock import os import six import socket import xdrlib from cinder import exception from cinder import test from cinder.volume import configuration as conf from cinder.volume.drivers import coho from cinder.volume.drivers import nfs ADDR = 'coho-datastream-addr' PATH = '/test/path' RPC_PORT = 2049 LOCAL_PATH = '/opt/cinder/mnt/test/path' VOLUME = { 'name': 'volume-bcc48c61-9691-4e5f-897c-793686093190', 'volume_id': 'bcc48c61-9691-4e5f-897c-793686093190', 'size': 128, 'volume_type': 'silver', 'volume_type_id': 'test', 'metadata': [{'key': 'type', 'service_label': 'silver'}], 'provider_location': None, 'id': 'bcc48c61-9691-4e5f-897c-793686093190', 'status': 'available', } CLONE_VOL = VOLUME.copy() CLONE_VOL['size'] = 256 SNAPSHOT = { 'name': 'snapshot-51dd4-8d8a-4aa9-9176-086c9d89e7fc', 'id': '51dd4-8d8a-4aa9-9176-086c9d89e7fc', 'size': 128, 'volume_type': None, 'provider_location': None, 'volume_size': 128, 'volume_name': 'volume-bcc48c61-9691-4e5f-897c-793686093190', 'volume_id': 'bcc48c61-9691-4e5f-897c-793686093191', } INVALID_SNAPSHOT = SNAPSHOT.copy() INVALID_SNAPSHOT['name'] = '' INVALID_HEADER_BIN = binascii.unhexlify('800000') NO_REPLY_BIN = binascii.unhexlify( 'aaaaa01000000010000000000000000000000003') MSG_DENIED_BIN = binascii.unhexlify( '00000a010000000110000000000000000000000000000003') PROC_UNAVAIL_BIN = binascii.unhexlify( '00000a010000000100000000000000000000000000000003') PROG_UNAVAIL_BIN = binascii.unhexlify( '000003c70000000100000000000000000000000000000001') PROG_MISMATCH_BIN = binascii.unhexlify( '00000f7700000001000000000000000000000000000000020000000100000001') GARBAGE_ARGS_BIN = binascii.unhexlify( '00000d6e0000000100000000000000000000000000000004') class CohoDriverTest(test.TestCase): """Test Coho Data's NFS volume driver.""" def __init__(self, *args, **kwargs): super(CohoDriverTest, self).__init__(*args, **kwargs) def setUp(self): super(CohoDriverTest, self).setUp() self.context = mock.Mock() self.configuration = mock.Mock(spec=conf.Configuration) self.configuration.max_over_subscription_ratio = 20.0 self.configuration.reserved_percentage = 0 self.configuration.volume_backend_name = 'coho-1' self.configuration.coho_rpc_port = 2049 self.configuration.nfs_shares_config = '/etc/cinder/coho_shares' self.configuration.nfs_sparsed_volumes = True self.configuration.nfs_mount_point_base = '/opt/stack/cinder/mnt' self.configuration.nfs_mount_options = None self.configuration.nas_host = None self.configuration.nas_share_path = None self.configuration.nas_mount_options = None def test_setup_failure_when_rpc_port_unconfigured(self): self.configuration.coho_rpc_port = None drv = coho.CohoDriver(configuration=self.configuration) self.mock_object(coho, 'LOG') self.mock_object(nfs.NfsDriver, 'do_setup') with self.assertRaisesRegex(exception.CohoException, ".*Coho rpc port is not configured.*"): drv.do_setup(self.context) self.assertTrue(coho.LOG.warning.called) self.assertTrue(nfs.NfsDriver.do_setup.called) def test_setup_failure_when_coho_rpc_port_is_invalid(self): self.configuration.coho_rpc_port = 99999 drv = coho.CohoDriver(configuration=self.configuration) self.mock_object(coho, 'LOG') self.mock_object(nfs.NfsDriver, 'do_setup') with self.assertRaisesRegex(exception.CohoException, "Invalid port number.*"): drv.do_setup(self.context) self.assertTrue(coho.LOG.warning.called) self.assertTrue(nfs.NfsDriver.do_setup.called) def test_create_snapshot(self): drv = coho.CohoDriver(configuration=self.configuration) mock_rpc_client = self.mock_object(coho, 'CohoRPCClient') mock_get_volume_location = self.mock_object(coho.CohoDriver, '_get_volume_location') mock_get_volume_location.return_value = ADDR, PATH drv.create_snapshot(SNAPSHOT) mock_get_volume_location.assert_has_calls( [mock.call(SNAPSHOT['volume_id'])]) mock_rpc_client.assert_has_calls( [mock.call(ADDR, self.configuration.coho_rpc_port), mock.call().create_snapshot( os.path.join(PATH, SNAPSHOT['volume_name']), SNAPSHOT['name'], 0)]) def test_delete_snapshot(self): drv = coho.CohoDriver(configuration=self.configuration) mock_rpc_client = self.mock_object(coho, 'CohoRPCClient') mock_get_volume_location = self.mock_object(coho.CohoDriver, '_get_volume_location') mock_get_volume_location.return_value = ADDR, PATH drv.delete_snapshot(SNAPSHOT) mock_get_volume_location.assert_has_calls( [mock.call(SNAPSHOT['volume_id'])]) mock_rpc_client.assert_has_calls( [mock.call(ADDR, self.configuration.coho_rpc_port), mock.call().delete_snapshot(SNAPSHOT['name'])]) def test_create_volume_from_snapshot(self): drv = coho.CohoDriver(configuration=self.configuration) mock_rpc_client = self.mock_object(coho, 'CohoRPCClient') mock_find_share = self.mock_object(drv, '_find_share') mock_find_share.return_value = ADDR + ':' + PATH drv.create_volume_from_snapshot(VOLUME, SNAPSHOT) mock_find_share.assert_has_calls( [mock.call(VOLUME['size'])]) mock_rpc_client.assert_has_calls( [mock.call(ADDR, self.configuration.coho_rpc_port), mock.call().create_volume_from_snapshot( SNAPSHOT['name'], os.path.join(PATH, VOLUME['name']))]) def test_create_cloned_volume(self): drv = coho.CohoDriver(configuration=self.configuration) mock_find_share = self.mock_object(drv, '_find_share') mock_find_share.return_value = ADDR + ':' + PATH mock_execute = self.mock_object(drv, '_execute') mock_local_path = self.mock_object(drv, 'local_path') mock_local_path.return_value = LOCAL_PATH drv.create_cloned_volume(VOLUME, CLONE_VOL) mock_find_share.assert_has_calls( [mock.call(VOLUME['size'])]) mock_local_path.assert_has_calls( [mock.call(VOLUME), mock.call(CLONE_VOL)]) mock_execute.assert_has_calls( [mock.call('cp', LOCAL_PATH, LOCAL_PATH, run_as_root=True)]) def test_extend_volume(self): drv = coho.CohoDriver(configuration=self.configuration) mock_execute = self.mock_object(drv, '_execute') mock_local_path = self.mock_object(drv, 'local_path') mock_local_path.return_value = LOCAL_PATH drv.extend_volume(VOLUME, 512) mock_local_path.assert_has_calls( [mock.call(VOLUME)]) mock_execute.assert_has_calls( [mock.call('truncate', '-s', '512G', LOCAL_PATH, run_as_root=True)]) def test_snapshot_failure_when_source_does_not_exist(self): drv = coho.CohoDriver(configuration=self.configuration) self.mock_object(coho.Client, '_make_call') mock_init_socket = self.mock_object(coho.Client, 'init_socket') mock_unpack_uint = self.mock_object(xdrlib.Unpacker, 'unpack_uint') mock_unpack_uint.return_value = errno.ENOENT mock_get_volume_location = self.mock_object(coho.CohoDriver, '_get_volume_location') mock_get_volume_location.return_value = ADDR, PATH with self.assertRaisesRegex(exception.CohoException, "No such file or directory.*"): drv.create_snapshot(SNAPSHOT) self.assertTrue(mock_init_socket.called) self.assertTrue(mock_unpack_uint.called) mock_get_volume_location.assert_has_calls( [mock.call(SNAPSHOT['volume_id'])]) def test_snapshot_failure_with_invalid_input(self): drv = coho.CohoDriver(configuration=self.configuration) self.mock_object(coho.Client, '_make_call') mock_init_socket = self.mock_object(coho.Client, 'init_socket') mock_unpack_uint = self.mock_object(xdrlib.Unpacker, 'unpack_uint') mock_unpack_uint.return_value = errno.EINVAL mock_get_volume_location = self.mock_object(coho.CohoDriver, '_get_volume_location') mock_get_volume_location.return_value = ADDR, PATH with self.assertRaisesRegex(exception.CohoException, "Invalid argument"): drv.delete_snapshot(INVALID_SNAPSHOT) self.assertTrue(mock_init_socket.called) self.assertTrue(mock_unpack_uint.called) mock_get_volume_location.assert_has_calls( [mock.call(INVALID_SNAPSHOT['volume_id'])]) def test_snapshot_failure_when_remote_is_unreachable(self): drv = coho.CohoDriver(configuration=self.configuration) mock_get_volume_location = self.mock_object(coho.CohoDriver, '_get_volume_location') mock_get_volume_location.return_value = 'uknown-address', PATH with self.assertRaisesRegex(exception.CohoException, "Failed to establish connection.*"): drv.create_snapshot(SNAPSHOT) mock_get_volume_location.assert_has_calls( [mock.call(INVALID_SNAPSHOT['volume_id'])]) def test_rpc_client_make_call_proper_order(self): """This test ensures that the RPC client logic is correct. When the RPC client's make_call function is called it creates a packet and sends it to the Coho cluster RPC server. This test ensures that the functions needed to complete the process are called in the proper order with valid arguments. """ mock_packer = self.mock_object(xdrlib, 'Packer') mock_unpacker = self.mock_object(xdrlib, 'Unpacker') mock_unpacker.return_value.unpack_uint.return_value = 0 mock_socket = self.mock_object(socket, 'socket') mock_init_call = self.mock_object(coho.Client, 'init_call') mock_init_call.return_value = (1, 2) mock_sendrecord = self.mock_object(coho.Client, '_sendrecord') mock_recvrecord = self.mock_object(coho.Client, '_recvrecord') mock_recvrecord.return_value = 'test_reply' mock_unpack_replyheader = self.mock_object(coho.Client, 'unpack_replyheader') mock_unpack_replyheader.return_value = (123, 1) rpc_client = coho.CohoRPCClient(ADDR, RPC_PORT) rpc_client.create_volume_from_snapshot('src', 'dest') self.assertTrue(mock_sendrecord.called) self.assertTrue(mock_unpack_replyheader.called) mock_packer.assert_has_calls([mock.call().reset()]) mock_unpacker.assert_has_calls( [mock.call().reset('test_reply'), mock.call().unpack_uint()]) mock_socket.assert_has_calls( [mock.call(socket.AF_INET, socket.SOCK_STREAM), mock.call().bind(('', 0)), mock.call().connect((ADDR, RPC_PORT))]) mock_init_call.assert_has_calls( [mock.call(coho.COHO1_CREATE_VOLUME_FROM_SNAPSHOT, [(six.b('src'), mock_packer().pack_string), (six.b('dest'), mock_packer().pack_string)])]) def test_rpc_client_error_in_reply_header(self): """Ensure excpetions in reply header are raised by the RPC client. Coho cluster's RPC server packs errors into the reply header. This test ensures that the RPC client parses the reply header correctly and raises exceptions on various errors that can be included in the reply header. """ mock_socket = self.mock_object(socket, 'socket') mock_recvrecord = self.mock_object(coho.Client, '_recvrecord') rpc_client = coho.CohoRPCClient(ADDR, RPC_PORT) mock_recvrecord.return_value = NO_REPLY_BIN with self.assertRaisesRegex(exception.CohoException, "no REPLY.*"): rpc_client.create_snapshot('src', 'dest', 0) mock_recvrecord.return_value = MSG_DENIED_BIN with self.assertRaisesRegex(exception.CohoException, ".*MSG_DENIED.*"): rpc_client.delete_snapshot('snapshot') mock_recvrecord.return_value = PROG_UNAVAIL_BIN with self.assertRaisesRegex(exception.CohoException, ".*PROG_UNAVAIL"): rpc_client.delete_snapshot('snapshot') mock_recvrecord.return_value = PROG_MISMATCH_BIN with self.assertRaisesRegex(exception.CohoException, ".*PROG_MISMATCH.*"): rpc_client.delete_snapshot('snapshot') mock_recvrecord.return_value = GARBAGE_ARGS_BIN with self.assertRaisesRegex(exception.CohoException, ".*GARBAGE_ARGS"): rpc_client.delete_snapshot('snapshot') mock_recvrecord.return_value = PROC_UNAVAIL_BIN with self.assertRaisesRegex(exception.CohoException, ".*PROC_UNAVAIL"): rpc_client.delete_snapshot('snapshot') self.assertTrue(mock_recvrecord.called) mock_socket.assert_has_calls( [mock.call(socket.AF_INET, socket.SOCK_STREAM), mock.call().bind(('', 0)), mock.call().connect((ADDR, RPC_PORT))]) def test_rpc_client_error_in_receive_fragment(self): """Ensure exception is raised when malformed packet is recieved.""" mock_sendrcd = self.mock_object(coho.Client, '_sendrecord') mock_socket = self.mock_object(socket, 'socket') mock_socket.return_value.recv.return_value = INVALID_HEADER_BIN rpc_client = coho.CohoRPCClient(ADDR, RPC_PORT) with self.assertRaisesRegex(exception.CohoException, "Invalid response header.*"): rpc_client.create_snapshot('src', 'dest', 0) self.assertTrue(mock_sendrcd.called) mock_socket.assert_has_calls( [mock.call(socket.AF_INET, socket.SOCK_STREAM), mock.call().bind(('', 0)), mock.call().connect((ADDR, RPC_PORT)), mock.call().recv(4)])