diff --git a/cinder/tests/unit/volume/drivers/test_hgst.py b/cinder/tests/unit/volume/drivers/test_hgst.py new file mode 100644 index 00000000000..51a8345c278 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/test_hgst.py @@ -0,0 +1,939 @@ +# Copyright (c) 2015 HGST 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 mock + +from oslo_concurrency import processutils +from oslo_log import log as logging + +from cinder import context +from cinder import exception +from cinder import test +from cinder.volume import configuration as conf +from cinder.volume.drivers.hgst import HGSTDriver +from cinder.volume import volume_types + + +LOG = logging.getLogger(__name__) + + +class HGSTTestCase(test.TestCase): + + # Need to mock these since we use them on driver creation + @mock.patch('pwd.getpwnam', return_value=1) + @mock.patch('grp.getgrnam', return_value=1) + @mock.patch('socket.gethostbyname', return_value='123.123.123.123') + def setUp(self, mock_ghn, mock_grnam, mock_pwnam): + """Set up UUT and all the flags required for later fake_executes.""" + super(HGSTTestCase, self).setUp() + self.stubs.Set(processutils, 'execute', self._fake_execute) + self._fail_vgc_cluster = False + self._fail_ip = False + self._fail_network_list = False + self._fail_domain_list = False + self._empty_domain_list = False + self._fail_host_storage = False + self._fail_space_list = False + self._fail_space_delete = False + self._fail_set_apphosts = False + self._fail_extend = False + self._request_cancel = False + self._return_blocked = 0 + self.configuration = mock.Mock(spec=conf.Configuration) + self.configuration.safe_get = self._fake_safe_get + self._reset_configuration() + self.driver = HGSTDriver(configuration=self.configuration, + execute=self._fake_execute) + + def _fake_safe_get(self, value): + """Don't throw exception on missing parameters, return None.""" + try: + val = getattr(self.configuration, value) + except AttributeError: + val = None + return val + + def _reset_configuration(self): + """Set safe and sane values for config params.""" + self.configuration.num_volume_device_scan_tries = 1 + self.configuration.volume_dd_blocksize = '1M' + self.configuration.volume_backend_name = 'hgst-1' + self.configuration.hgst_storage_servers = 'stor1:gbd0,stor2:gbd0' + self.configuration.hgst_net = 'net1' + self.configuration.hgst_redundancy = '0' + self.configuration.hgst_space_user = 'kane' + self.configuration.hgst_space_group = 'xanadu' + self.configuration.hgst_space_mode = '0777' + + def _parse_space_create(self, *cmd): + """Eats a vgc-cluster space-create command line to a dict.""" + self.created = {'storageserver': ''} + cmd = list(*cmd) + while cmd: + param = cmd.pop(0) + if param == "-n": + self.created['name'] = cmd.pop(0) + elif param == "-N": + self.created['net'] = cmd.pop(0) + elif param == "-s": + self.created['size'] = cmd.pop(0) + elif param == "--redundancy": + self.created['redundancy'] = cmd.pop(0) + elif param == "--user": + self.created['user'] = cmd.pop(0) + elif param == "--user": + self.created['user'] = cmd.pop(0) + elif param == "--group": + self.created['group'] = cmd.pop(0) + elif param == "--mode": + self.created['mode'] = cmd.pop(0) + elif param == "-S": + self.created['storageserver'] += cmd.pop(0) + "," + else: + pass + + def _parse_space_extend(self, *cmd): + """Eats a vgc-cluster space-extend commandline to a dict.""" + self.extended = {'storageserver': ''} + cmd = list(*cmd) + while cmd: + param = cmd.pop(0) + if param == "-n": + self.extended['name'] = cmd.pop(0) + elif param == "-s": + self.extended['size'] = cmd.pop(0) + elif param == "-S": + self.extended['storageserver'] += cmd.pop(0) + "," + else: + pass + if self._fail_extend: + raise processutils.ProcessExecutionError(exit_code=1) + else: + return '', '' + + def _parse_space_delete(self, *cmd): + """Eats a vgc-cluster space-delete commandline to a dict.""" + self.deleted = {} + cmd = list(*cmd) + while cmd: + param = cmd.pop(0) + if param == "-n": + self.deleted['name'] = cmd.pop(0) + else: + pass + if self._fail_space_delete: + raise processutils.ProcessExecutionError(exit_code=1) + else: + return '', '' + + def _parse_space_list(self, *cmd): + """Eats a vgc-cluster space-list commandline to a dict.""" + json = False + nameOnly = False + cmd = list(*cmd) + while cmd: + param = cmd.pop(0) + if param == "--json": + json = True + elif param == "--name-only": + nameOnly = True + elif param == "-n": + pass # Don't use the name here... + else: + pass + if self._fail_space_list: + raise processutils.ProcessExecutionError(exit_code=1) + elif nameOnly: + return "space1\nspace2\nvolume1\n", '' + elif json: + return HGST_SPACE_JSON, '' + else: + return '', '' + + def _parse_network_list(self, *cmd): + """Eat a network-list command and return error or results.""" + if self._fail_network_list: + raise processutils.ProcessExecutionError(exit_code=1) + else: + return NETWORK_LIST, '' + + def _parse_domain_list(self, *cmd): + """Eat a domain-list command and return error, empty, or results.""" + if self._fail_domain_list: + raise processutils.ProcessExecutionError(exit_code=1) + elif self._empty_domain_list: + return '', '' + else: + return "thisserver\nthatserver\nanotherserver\n", '' + + def _fake_execute(self, *cmd, **kwargs): + """Sudo hook to catch commands to allow running on all hosts.""" + cmdlist = list(cmd) + exe = cmdlist.pop(0) + if exe == 'vgc-cluster': + exe = cmdlist.pop(0) + if exe == "request-cancel": + self._request_cancel = True + if self._return_blocked > 0: + return 'Request cancelled', '' + else: + raise processutils.ProcessExecutionError(exit_code=1) + elif self._fail_vgc_cluster: + raise processutils.ProcessExecutionError(exit_code=1) + elif exe == "--version": + return "HGST Solutions V2.5.0.0.x.x.x.x.x", '' + elif exe == "space-list": + return self._parse_space_list(cmdlist) + elif exe == "space-create": + self._parse_space_create(cmdlist) + if self._return_blocked > 0: + self._return_blocked = self._return_blocked - 1 + out = "VGC_CREATE_000002\nBLOCKED\n" + raise processutils.ProcessExecutionError(stdout=out, + exit_code=1) + return '', '' + elif exe == "space-delete": + return self._parse_space_delete(cmdlist) + elif exe == "space-extend": + return self._parse_space_extend(cmdlist) + elif exe == "host-storage": + if self._fail_host_storage: + raise processutils.ProcessExecutionError(exit_code=1) + return HGST_HOST_STORAGE, '' + elif exe == "domain-list": + return self._parse_domain_list() + elif exe == "network-list": + return self._parse_network_list() + elif exe == "space-set-apphosts": + if self._fail_set_apphosts: + raise processutils.ProcessExecutionError(exit_code=1) + return '', '' + else: + raise NotImplementedError + elif exe == 'ip': + if self._fail_ip: + raise processutils.ProcessExecutionError(exit_code=1) + else: + return IP_OUTPUT, '' + elif exe == 'dd': + self.dd_count = -1 + for p in cmdlist: + if 'count=' in p: + self.dd_count = int(p[6:]) + return DD_OUTPUT, '' + else: + return '', '' + + @mock.patch('pwd.getpwnam', return_value=1) + @mock.patch('grp.getgrnam', return_value=1) + @mock.patch('socket.gethostbyname', return_value='123.123.123.123') + def test_vgc_cluster_not_present(self, mock_ghn, mock_grnam, mock_pwnam): + """Test exception when vgc-cluster returns an error.""" + # Should pass + self._fail_vgc_cluster = False + self.driver.check_for_setup_error() + # Should throw exception + self._fail_vgc_cluster = True + self.assertRaises(exception.VolumeDriverException, + self.driver.check_for_setup_error) + + @mock.patch('pwd.getpwnam', return_value=1) + @mock.patch('grp.getgrnam', return_value=1) + @mock.patch('socket.gethostbyname', return_value='123.123.123.123') + def test_parameter_redundancy_invalid(self, mock_ghn, mock_grnam, + mock_pwnam): + """Test when hgst_redundancy config parameter not 0 or 1.""" + # Should pass + self.driver.check_for_setup_error() + # Should throw exceptions + self.configuration.hgst_redundancy = '' + self.assertRaises(exception.VolumeDriverException, + self.driver.check_for_setup_error) + self.configuration.hgst_redundancy = 'Fred' + self.assertRaises(exception.VolumeDriverException, + self.driver.check_for_setup_error) + + @mock.patch('pwd.getpwnam', return_value=1) + @mock.patch('grp.getgrnam', return_value=1) + @mock.patch('socket.gethostbyname', return_value='123.123.123.123') + def test_parameter_user_invalid(self, mock_ghn, mock_grnam, mock_pwnam): + """Test exception when hgst_space_user doesn't map to UNIX user.""" + # Should pass + self.driver.check_for_setup_error() + # Should throw exceptions + mock_pwnam.side_effect = KeyError() + self.configuration.hgst_space_user = '' + self.assertRaises(exception.VolumeDriverException, + self.driver.check_for_setup_error) + self.configuration.hgst_space_user = 'Fred!`' + self.assertRaises(exception.VolumeDriverException, + self.driver.check_for_setup_error) + + @mock.patch('pwd.getpwnam', return_value=1) + @mock.patch('grp.getgrnam', return_value=1) + @mock.patch('socket.gethostbyname', return_value='123.123.123.123') + def test_parameter_group_invalid(self, mock_ghn, mock_grnam, mock_pwnam): + """Test exception when hgst_space_group doesn't map to UNIX group.""" + # Should pass + self.driver.check_for_setup_error() + # Should throw exceptions + mock_grnam.side_effect = KeyError() + self.configuration.hgst_space_group = '' + self.assertRaises(exception.VolumeDriverException, + self.driver.check_for_setup_error) + self.configuration.hgst_space_group = 'Fred!`' + self.assertRaises(exception.VolumeDriverException, + self.driver.check_for_setup_error) + + @mock.patch('pwd.getpwnam', return_value=1) + @mock.patch('grp.getgrnam', return_value=1) + @mock.patch('socket.gethostbyname', return_value='123.123.123.123') + def test_parameter_mode_invalid(self, mock_ghn, mock_grnam, mock_pwnam): + """Test exception when mode for created spaces isn't proper format.""" + # Should pass + self.driver.check_for_setup_error() + # Should throw exceptions + self.configuration.hgst_space_mode = '' + self.assertRaises(exception.VolumeDriverException, + self.driver.check_for_setup_error) + self.configuration.hgst_space_mode = 'Fred' + self.assertRaises(exception.VolumeDriverException, + self.driver.check_for_setup_error) + + @mock.patch('pwd.getpwnam', return_value=1) + @mock.patch('grp.getgrnam', return_value=1) + @mock.patch('socket.gethostbyname', return_value='123.123.123.123') + def test_parameter_net_invalid(self, mock_ghn, mock_grnam, mock_pwnam): + """Test exception when hgst_net not in the domain.""" + # Should pass + self.driver.check_for_setup_error() + # Should throw exceptions + self._fail_network_list = True + self.configuration.hgst_net = 'Fred' + self.assertRaises(exception.VolumeDriverException, + self.driver.check_for_setup_error) + self._fail_network_list = False + + @mock.patch('pwd.getpwnam', return_value=1) + @mock.patch('grp.getgrnam', return_value=1) + @mock.patch('socket.gethostbyname', return_value='123.123.123.123') + def test_ip_addr_fails(self, mock_ghn, mock_grnam, mock_pwnam): + """Test exception when IP ADDR command fails.""" + # Should pass + self.driver.check_for_setup_error() + # Throw exception, need to clear internal cached host in driver + self._fail_ip = True + self.driver._vgc_host = None + self.assertRaises(exception.VolumeDriverException, + self.driver.check_for_setup_error) + + @mock.patch('pwd.getpwnam', return_value=1) + @mock.patch('grp.getgrnam', return_value=1) + @mock.patch('socket.gethostbyname', return_value='123.123.123.123') + def test_domain_list_fails(self, mock_ghn, mock_grnam, mock_pwnam): + """Test exception when domain-list fails for the domain.""" + # Should pass + self.driver.check_for_setup_error() + # Throw exception, need to clear internal cached host in driver + self._fail_domain_list = True + self.driver._vgc_host = None + self.assertRaises(exception.VolumeDriverException, + self.driver.check_for_setup_error) + + @mock.patch('pwd.getpwnam', return_value=1) + @mock.patch('grp.getgrnam', return_value=1) + @mock.patch('socket.gethostbyname', return_value='123.123.123.123') + def test_not_in_domain(self, mock_ghn, mock_grnam, mock_pwnam): + """Test exception when Cinder host not domain member.""" + # Should pass + self.driver.check_for_setup_error() + # Throw exception, need to clear internal cached host in driver + self._empty_domain_list = True + self.driver._vgc_host = None + self.assertRaises(exception.VolumeDriverException, + self.driver.check_for_setup_error) + + @mock.patch('pwd.getpwnam', return_value=1) + @mock.patch('grp.getgrnam', return_value=1) + @mock.patch('socket.gethostbyname', return_value='123.123.123.123') + def test_parameter_storageservers_invalid(self, mock_ghn, mock_grnam, + mock_pwnam): + """Test exception when the storage servers are invalid/missing.""" + # Should pass + self.driver.check_for_setup_error() + # Storage_hosts missing + self.configuration.hgst_storage_servers = '' + self.assertRaises(exception.VolumeDriverException, + self.driver.check_for_setup_error) + # missing a : between host and devnode + self.configuration.hgst_storage_servers = 'stor1,stor2' + self.assertRaises(exception.VolumeDriverException, + self.driver.check_for_setup_error) + # missing a : between host and devnode + self.configuration.hgst_storage_servers = 'stor1:gbd0,stor2' + self.assertRaises(exception.VolumeDriverException, + self.driver.check_for_setup_error) + # Host not in cluster + self.configuration.hgst_storage_servers = 'stor1:gbd0' + self._fail_host_storage = True + self.assertRaises(exception.VolumeDriverException, + self.driver.check_for_setup_error) + + def test_update_volume_stats(self): + """Get cluster space available, should pass.""" + actual = self.driver.get_volume_stats(True) + self.assertEqual('HGST', actual['vendor_name']) + self.assertEqual('hgst', actual['storage_protocol']) + self.assertEqual(90, actual['total_capacity_gb']) + self.assertEqual(87, actual['free_capacity_gb']) + self.assertEqual(0, actual['reserved_percentage']) + + def test_update_volume_stats_redundancy(self): + """Get cluster space available, half-sized - 1 for mirrors.""" + self.configuration.hgst_redundancy = '1' + actual = self.driver.get_volume_stats(True) + self.assertEqual('HGST', actual['vendor_name']) + self.assertEqual('hgst', actual['storage_protocol']) + self.assertEqual(44, actual['total_capacity_gb']) + self.assertEqual(43, actual['free_capacity_gb']) + self.assertEqual(0, actual['reserved_percentage']) + + def test_update_volume_stats_cached(self): + """Get cached cluster space, should not call executable.""" + self._fail_host_storage = True + actual = self.driver.get_volume_stats(False) + self.assertEqual('HGST', actual['vendor_name']) + self.assertEqual('hgst', actual['storage_protocol']) + self.assertEqual(90, actual['total_capacity_gb']) + self.assertEqual(87, actual['free_capacity_gb']) + self.assertEqual(0, actual['reserved_percentage']) + + def test_update_volume_stats_error(self): + """Test that when host-storage gives an error, return unknown.""" + self._fail_host_storage = True + actual = self.driver.get_volume_stats(True) + self.assertEqual('HGST', actual['vendor_name']) + self.assertEqual('hgst', actual['storage_protocol']) + self.assertEqual('unknown', actual['total_capacity_gb']) + self.assertEqual('unknown', actual['free_capacity_gb']) + self.assertEqual(0, actual['reserved_percentage']) + + @mock.patch('socket.gethostbyname', return_value='123.123.123.123') + def test_create_volume(self, mock_ghn): + """Test volume creation, ensure appropriate size expansion/name.""" + ctxt = context.get_admin_context() + extra_specs = {} + type_ref = volume_types.create(ctxt, 'hgst-1', extra_specs) + volume = {'id': '1', 'name': 'volume1', + 'display_name': '', + 'volume_type_id': type_ref['id'], + 'size': 10} + ret = self.driver.create_volume(volume) + expected = {'redundancy': '0', 'group': 'xanadu', + 'name': 'volume10', 'mode': '0777', + 'user': 'kane', 'net': 'net1', + 'storageserver': 'stor1:gbd0,stor2:gbd0,', + 'size': '12'} + self.assertDictMatch(expected, self.created) + # Check the returned provider, note the the provider_id is hashed + expected_pid = {'provider_id': 'volume10'} + self.assertDictMatch(expected_pid, ret) + + @mock.patch('socket.gethostbyname', return_value='123.123.123.123') + def test_create_volume_name_creation_fail(self, mock_ghn): + """Test volume creation exception when can't make a hashed name.""" + ctxt = context.get_admin_context() + extra_specs = {} + type_ref = volume_types.create(ctxt, 'hgst-1', extra_specs) + volume = {'id': '1', 'name': 'volume1', + 'display_name': '', + 'volume_type_id': type_ref['id'], + 'size': 10} + self._fail_space_list = True + self.assertRaises(exception.VolumeDriverException, + self.driver.create_volume, volume) + + @mock.patch('socket.gethostbyname', return_value='123.123.123.123') + def test_create_snapshot(self, mock_ghn): + """Test creating a snapshot, ensure full data of original copied.""" + # Now snapshot the volume and check commands + snapshot = {'volume_name': 'volume10', 'volume_size': 10, + 'volume_id': 'xxx', 'display_name': 'snap10', + 'name': '123abc', 'volume_size': 10, 'id': '123abc', + 'volume': {'provider_id': 'space10'}} + ret = self.driver.create_snapshot(snapshot) + # We must copy entier underlying storage, ~12GB, not just 10GB + self.assertEqual(11444, self.dd_count) + # Check space-create command + expected = {'redundancy': '0', 'group': 'xanadu', + 'name': snapshot['display_name'], 'mode': '0777', + 'user': 'kane', 'net': 'net1', + 'storageserver': 'stor1:gbd0,stor2:gbd0,', + 'size': '12'} + self.assertDictMatch(expected, self.created) + # Check the returned provider + expected_pid = {'provider_id': 'snap10'} + self.assertDictMatch(expected_pid, ret) + + @mock.patch('socket.gethostbyname', return_value='123.123.123.123') + def test_create_cloned_volume(self, mock_ghn): + """Test creating a clone, ensure full size is copied from original.""" + ctxt = context.get_admin_context() + extra_specs = {} + type_ref = volume_types.create(ctxt, 'hgst-1', extra_specs) + orig = {'id': '1', 'name': 'volume1', 'display_name': '', + 'volume_type_id': type_ref['id'], 'size': 10, + 'provider_id': 'space_orig'} + clone = {'id': '2', 'name': 'clone1', 'display_name': '', + 'volume_type_id': type_ref['id'], 'size': 10} + pid = self.driver.create_cloned_volume(clone, orig) + # We must copy entier underlying storage, ~12GB, not just 10GB + self.assertEqual(11444, self.dd_count) + # Check space-create command + expected = {'redundancy': '0', 'group': 'xanadu', + 'name': 'clone1', 'mode': '0777', + 'user': 'kane', 'net': 'net1', + 'storageserver': 'stor1:gbd0,stor2:gbd0,', + 'size': '12'} + self.assertDictMatch(expected, self.created) + # Check the returned provider + expected_pid = {'provider_id': 'clone1'} + self.assertDictMatch(expected_pid, pid) + + @mock.patch('socket.gethostbyname', return_value='123.123.123.123') + def test_add_cinder_apphosts_fails(self, mock_ghn): + """Test exception when set-apphost can't connect volume to host.""" + ctxt = context.get_admin_context() + extra_specs = {} + type_ref = volume_types.create(ctxt, 'hgst-1', extra_specs) + orig = {'id': '1', 'name': 'volume1', 'display_name': '', + 'volume_type_id': type_ref['id'], 'size': 10, + 'provider_id': 'space_orig'} + clone = {'id': '2', 'name': 'clone1', 'display_name': '', + 'volume_type_id': type_ref['id'], 'size': 10} + self._fail_set_apphosts = True + self.assertRaises(exception.VolumeDriverException, + self.driver.create_cloned_volume, clone, orig) + + @mock.patch('socket.gethostbyname', return_value='123.123.123.123') + def test_create_volume_from_snapshot(self, mock_ghn): + """Test creating volume from snapshot, ensure full space copy.""" + ctxt = context.get_admin_context() + extra_specs = {} + type_ref = volume_types.create(ctxt, 'hgst-1', extra_specs) + snap = {'id': '1', 'name': 'volume1', 'display_name': '', + 'volume_type_id': type_ref['id'], 'size': 10, + 'provider_id': 'space_orig'} + volume = {'id': '2', 'name': 'volume2', 'display_name': '', + 'volume_type_id': type_ref['id'], 'size': 10} + pid = self.driver.create_volume_from_snapshot(volume, snap) + # We must copy entier underlying storage, ~12GB, not just 10GB + self.assertEqual(11444, self.dd_count) + # Check space-create command + expected = {'redundancy': '0', 'group': 'xanadu', + 'name': 'volume2', 'mode': '0777', + 'user': 'kane', 'net': 'net1', + 'storageserver': 'stor1:gbd0,stor2:gbd0,', + 'size': '12'} + self.assertDictMatch(expected, self.created) + # Check the returned provider + expected_pid = {'provider_id': 'volume2'} + self.assertDictMatch(expected_pid, pid) + + @mock.patch('socket.gethostbyname', return_value='123.123.123.123') + def test_create_volume_blocked(self, mock_ghn): + """Test volume creation where only initial space-create is blocked. + + This should actually pass because we are blocked byt return an error + in request-cancel, meaning that it got unblocked before we could kill + the space request. + """ + ctxt = context.get_admin_context() + extra_specs = {} + type_ref = volume_types.create(ctxt, 'hgst-1', extra_specs) + volume = {'id': '1', 'name': 'volume1', + 'display_name': '', + 'volume_type_id': type_ref['id'], + 'size': 10} + self._return_blocked = 1 # Block & fail cancel => create succeeded + ret = self.driver.create_volume(volume) + expected = {'redundancy': '0', 'group': 'xanadu', + 'name': 'volume10', 'mode': '0777', + 'user': 'kane', 'net': 'net1', + 'storageserver': 'stor1:gbd0,stor2:gbd0,', + 'size': '12'} + self.assertDictMatch(expected, self.created) + # Check the returned provider + expected_pid = {'provider_id': 'volume10'} + self.assertDictMatch(expected_pid, ret) + self.assertEqual(True, self._request_cancel) + + @mock.patch('socket.gethostbyname', return_value='123.123.123.123') + def test_create_volume_blocked_and_fail(self, mock_ghn): + """Test volume creation where space-create blocked permanently. + + This should fail because the initial create was blocked and the + request-cancel succeeded, meaning the create operation never + completed. + """ + ctxt = context.get_admin_context() + extra_specs = {} + type_ref = volume_types.create(ctxt, 'hgst-1', extra_specs) + volume = {'id': '1', 'name': 'volume1', + 'display_name': '', + 'volume_type_id': type_ref['id'], + 'size': 10} + self._return_blocked = 2 # Block & pass cancel => create failed. :( + self.assertRaises(exception.VolumeDriverException, + self.driver.create_volume, volume) + self.assertEqual(True, self._request_cancel) + + def test_delete_volume(self): + """Test deleting existing volume, ensure proper name used.""" + ctxt = context.get_admin_context() + extra_specs = {} + type_ref = volume_types.create(ctxt, 'hgst-1', extra_specs) + volume = {'id': '1', 'name': 'volume1', + 'display_name': '', + 'volume_type_id': type_ref['id'], + 'size': 10, + 'provider_id': 'volume10'} + self.driver.delete_volume(volume) + expected = {'name': 'volume10'} + self.assertDictMatch(expected, self.deleted) + + def test_delete_volume_failure_modes(self): + """Test cases where space-delete fails, but OS delete is still OK.""" + ctxt = context.get_admin_context() + extra_specs = {} + type_ref = volume_types.create(ctxt, 'hgst-1', extra_specs) + volume = {'id': '1', 'name': 'volume1', + 'display_name': '', + 'volume_type_id': type_ref['id'], + 'size': 10, + 'provider_id': 'volume10'} + self._fail_space_delete = True + # This should not throw an exception, space-delete failure not problem + self.driver.delete_volume(volume) + self._fail_space_delete = False + volume['provider_id'] = None + # This should also not throw an exception + self.driver.delete_volume(volume) + + def test_delete_snapshot(self): + """Test deleting a snapshot, ensure proper name is removed.""" + ctxt = context.get_admin_context() + extra_specs = {} + type_ref = volume_types.create(ctxt, 'hgst-1', extra_specs) + snapshot = {'id': '1', 'name': 'volume1', + 'display_name': '', + 'volume_type_id': type_ref['id'], + 'size': 10, + 'provider_id': 'snap10'} + self.driver.delete_snapshot(snapshot) + expected = {'name': 'snap10'} + self.assertDictMatch(expected, self.deleted) + + def test_extend_volume(self): + """Test extending a volume, check the size in GB vs. GiB.""" + ctxt = context.get_admin_context() + extra_specs = {} + type_ref = volume_types.create(ctxt, 'hgst-1', extra_specs) + volume = {'id': '1', 'name': 'volume1', + 'display_name': '', + 'volume_type_id': type_ref['id'], + 'size': 10, + 'provider_id': 'volume10'} + self.extended = {'name': '', 'size': '0', + 'storageserver': ''} + self.driver.extend_volume(volume, 12) + expected = {'name': 'volume10', 'size': '2', + 'storageserver': 'stor1:gbd0,stor2:gbd0,'} + self.assertDictMatch(expected, self.extended) + + def test_extend_volume_noextend(self): + """Test extending a volume where Space does not need to be enlarged. + + Because Spaces are generated somewhat larger than the requested size + from OpenStack due to the base10(HGST)/base2(OS) mismatch, they can + sometimes be larger than requested from OS. In that case a + volume_extend may actually be a noop since the volume is already large + enough to satisfy OS's request. + """ + ctxt = context.get_admin_context() + extra_specs = {} + type_ref = volume_types.create(ctxt, 'hgst-1', extra_specs) + volume = {'id': '1', 'name': 'volume1', + 'display_name': '', + 'volume_type_id': type_ref['id'], + 'size': 10, + 'provider_id': 'volume10'} + self.extended = {'name': '', 'size': '0', + 'storageserver': ''} + self.driver.extend_volume(volume, 10) + expected = {'name': '', 'size': '0', + 'storageserver': ''} + self.assertDictMatch(expected, self.extended) + + def test_space_list_fails(self): + """Test exception is thrown when we can't call space-list.""" + ctxt = context.get_admin_context() + extra_specs = {} + type_ref = volume_types.create(ctxt, 'hgst-1', extra_specs) + volume = {'id': '1', 'name': 'volume1', + 'display_name': '', + 'volume_type_id': type_ref['id'], + 'size': 10, + 'provider_id': 'volume10'} + self.extended = {'name': '', 'size': '0', + 'storageserver': ''} + self._fail_space_list = True + self.assertRaises(exception.VolumeDriverException, + self.driver.extend_volume, volume, 12) + + def test_cli_error_not_blocked(self): + """Test the _blocked handler's handlinf of a non-blocked error. + + The _handle_blocked handler is called on any process errors in the + code. If the error was not caused by a blocked command condition + (syntax error, out of space, etc.) then it should just throw the + exception and not try and retry the command. + """ + ctxt = context.get_admin_context() + extra_specs = {} + type_ref = volume_types.create(ctxt, 'hgst-1', extra_specs) + volume = {'id': '1', 'name': 'volume1', + 'display_name': '', + 'volume_type_id': type_ref['id'], + 'size': 10, + 'provider_id': 'volume10'} + self.extended = {'name': '', 'size': '0', + 'storageserver': ''} + self._fail_extend = True + self.assertRaises(exception.VolumeDriverException, + self.driver.extend_volume, volume, 12) + self.assertEqual(False, self._request_cancel) + + @mock.patch('socket.gethostbyname', return_value='123.123.123.123') + def test_initialize_connection(self, moch_ghn): + """Test that the connection_info for Nova makes sense.""" + volume = {'name': '123', 'provider_id': 'spacey'} + conn = self.driver.initialize_connection(volume, None) + expected = {'name': 'spacey', 'noremovehost': 'thisserver'} + self.assertDictMatch(expected, conn['data']) + +# Below are some command outputs we emulate +IP_OUTPUT = """ +3: em2: mtu 1500 qdisc mq state + link/ether 00:25:90:d9:18:09 brd ff:ff:ff:ff:ff:ff + inet 192.168.0.23/24 brd 192.168.0.255 scope global em2 + valid_lft forever preferred_lft forever + inet6 fe80::225:90ff:fed9:1809/64 scope link + valid_lft forever preferred_lft forever +1: lo: mtu 65536 qdisc noqueue state UNKNOWN + link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 + inet 123.123.123.123/8 scope host lo + valid_lft forever preferred_lft forever + inet 169.254.169.254/32 scope link lo + valid_lft forever preferred_lft forever + inet6 ::1/128 scope host + valid_lft forever preferred_lft forever +2: em1: mtu 1500 qdisc mq master + link/ether 00:25:90:d9:18:08 brd ff:ff:ff:ff:ff:ff + inet6 fe80::225:90ff:fed9:1808/64 scope link + valid_lft forever preferred_lft forever +""" + +HGST_HOST_STORAGE = """ +{ + "hostStatus": [ + { + "node": "tm33.virident.info", + "up": true, + "isManager": true, + "cardStatus": [ + { + "cardName": "/dev/sda3", + "cardSerialNumber": "002f09b4037a9d521c007ee4esda3", + "cardStatus": "Good", + "cardStateDetails": "Normal", + "cardActionRequired": "", + "cardTemperatureC": 0, + "deviceType": "Generic", + "cardTemperatureState": "Safe", + "partitionStatus": [ + { + "partName": "/dev/gbd0", + "partitionState": "READY", + "usableCapacityBytes": 98213822464, + "totalReadBytes": 0, + "totalWriteBytes": 0, + "remainingLifePCT": 100, + "flashReservesLeftPCT": 100, + "fmc": true, + "vspaceCapacityAvailable": 94947041280, + "vspaceReducedCapacityAvailable": 87194279936, + "_partitionID": "002f09b4037a9d521c007ee4esda3:0", + "_usedSpaceBytes": 3266781184, + "_enabledSpaceBytes": 3266781184, + "_disabledSpaceBytes": 0 + } + ] + } + ], + "driverStatus": { + "vgcdriveDriverLoaded": true, + "vhaDriverLoaded": true, + "vcacheDriverLoaded": true, + "vlvmDriverLoaded": true, + "ipDataProviderLoaded": true, + "ibDataProviderLoaded": false, + "driverUptimeSecs": 4800, + "rVersion": "20368.d55ec22.master" + }, + "totalCapacityBytes": 98213822464, + "totalUsedBytes": 3266781184, + "totalEnabledBytes": 3266781184, + "totalDisabledBytes": 0 + }, + { + "node": "tm32.virident.info", + "up": true, + "isManager": false, + "cardStatus": [], + "driverStatus": { + "vgcdriveDriverLoaded": true, + "vhaDriverLoaded": true, + "vcacheDriverLoaded": true, + "vlvmDriverLoaded": true, + "ipDataProviderLoaded": true, + "ibDataProviderLoaded": false, + "driverUptimeSecs": 0, + "rVersion": "20368.d55ec22.master" + }, + "totalCapacityBytes": 0, + "totalUsedBytes": 0, + "totalEnabledBytes": 0, + "totalDisabledBytes": 0 + } + ], + "totalCapacityBytes": 98213822464, + "totalUsedBytes": 3266781184, + "totalEnabledBytes": 3266781184, + "totalDisabledBytes": 0 +} +""" + +HGST_SPACE_JSON = """ +{ + "resources": [ + { + "resourceType": "vLVM-L", + "resourceID": "vLVM-L:698cdb43-54da-863e-1699-294a080ce4db", + "state": "OFFLINE", + "instanceStates": {}, + "redundancy": 0, + "sizeBytes": 12000000000, + "name": "volume10", + "nodes": [], + "networks": [ + "net1" + ], + "components": [ + { + "resourceType": "vLVM-S", + "resourceID": "vLVM-S:698cdb43-54da-863e-eb10-6275f47b8ed2", + "redundancy": 0, + "order": 0, + "sizeBytes": 12000000000, + "numStripes": 1, + "stripeSizeBytes": null, + "name": "volume10s00", + "state": "OFFLINE", + "instanceStates": {}, + "components": [ + { + "name": "volume10h00", + "resourceType": "vHA", + "resourceID": "vHA:3e86da54-40db-8c69-0300-0000ac10476e", + "redundancy": 0, + "sizeBytes": 12000000000, + "state": "GOOD", + "components": [ + { + "name": "volume10h00", + "vspaceType": "vHA", + "vspaceRole": "primary", + "storageObjectID": "vHA:3e86da54-40db-8c69--18130019e486", + "state": "Disconnected (DCS)", + "node": "tm33.virident.info", + "partName": "/dev/gbd0" + } + ], + "crState": "GOOD" + }, + { + "name": "volume10v00", + "resourceType": "vShare", + "resourceID": "vShare:3f86da54-41db-8c69-0300-ecf4bbcc14cc", + "redundancy": 0, + "order": 0, + "sizeBytes": 12000000000, + "state": "GOOD", + "components": [ + { + "name": "volume10v00", + "vspaceType": "vShare", + "vspaceRole": "target", + "storageObjectID": "vShare:3f86da54-41db-8c64bbcc14cc:T", + "state": "Started", + "node": "tm33.virident.info", + "partName": "/dev/gbd0_volume10h00" + } + ] + } + ] + } + ], + "_size": "12GB", + "_state": "OFFLINE", + "_ugm": "", + "_nets": "net1", + "_hosts": "tm33.virident.info(12GB,NC)", + "_ahosts": "", + "_shosts": "tm33.virident.info(12GB)", + "_name": "volume10", + "_node": "", + "_type": "vLVM-L", + "_detail": "vLVM-L:698cdb43-54da-863e-1699-294a080ce4db", + "_device": "" + } + ] +} +""" + +NETWORK_LIST = """ +Network Name Type Flags Description +------------ ---- ---------- ------------------------ +net1 IPv4 autoConfig 192.168.0.0/24 1Gb/s +net2 IPv4 autoConfig 192.168.10.0/24 10Gb/s +""" + +DD_OUTPUT = """ +1+0 records in +1+0 records out +1024 bytes (1.0 kB) copied, 0.000427529 s, 2.4 MB/s +""" diff --git a/cinder/volume/drivers/hgst.py b/cinder/volume/drivers/hgst.py new file mode 100644 index 00000000000..59b0b29f2e6 --- /dev/null +++ b/cinder/volume/drivers/hgst.py @@ -0,0 +1,602 @@ +# Copyright 2015 HGST +# 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. +""" +Desc : Driver to store Cinder volumes using HGST Flash Storage Suite +Require : HGST Flash Storage Suite +Author : Earle F. Philhower, III +""" + +import grp +import json +import math +import os +import pwd +import six +import socket +import string + +from oslo_concurrency import lockutils +from oslo_concurrency import processutils +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import units + +from cinder import exception +from cinder.i18n import _ +from cinder.i18n import _LE +from cinder.i18n import _LW +from cinder.image import image_utils +from cinder.volume import driver +from cinder.volume import utils as volutils + +LOG = logging.getLogger(__name__) + +hgst_opts = [ + cfg.StrOpt('hgst_net', + default='Net 1 (IPv4)', + help='Space network name to use for data transfer'), + cfg.StrOpt('hgst_storage_servers', + default='os:gbd0', + help='Comma separated list of Space storage servers:devices. ' + 'ex: os1_stor:gbd0,os2_stor:gbd0'), + cfg.StrOpt('hgst_redundancy', + default='0', + help='Should spaces be redundantly stored (1/0)'), + cfg.StrOpt('hgst_space_user', + default='root', + help='User to own created spaces'), + cfg.StrOpt('hgst_space_group', + default='disk', + help='Group to own created spaces'), + cfg.StrOpt('hgst_space_mode', + default='0600', + help='UNIX mode for created spaces'), +] + + +CONF = cfg.CONF +CONF.register_opts(hgst_opts) + + +class HGSTDriver(driver.VolumeDriver): + """This is the Class to set in cinder.conf (volume_driver). + + Implements a Cinder Volume driver which creates a HGST Space for each + Cinder Volume or Snapshot requested. Use the vgc-cluster CLI to do + all management operations. + + The Cinder host will nominally have all Spaces made visible to it, + while individual compute nodes will only have Spaces connected to KVM + instances connected. + """ + + VERSION = '1.0.0' + VGCCLUSTER = 'vgc-cluster' + SPACEGB = units.G - 16 * units.M # Workaround for shrinkage Bug 28320 + BLOCKED = "BLOCKED" # Exit code when a command is blocked + + def __init__(self, *args, **kwargs): + """Initialize our protocol descriptor/etc.""" + super(HGSTDriver, self).__init__(*args, **kwargs) + self.configuration.append_config_values(hgst_opts) + self._vgc_host = None + self.check_for_setup_error() + self._stats = {'driver_version': self.VERSION, + 'reserved_percentage': 0, + 'storage_protocol': 'hgst', + 'total_capacity_gb': 'unknown', + 'free_capacity_gb': 'unknown', + 'vendor_name': 'HGST', + } + backend_name = self.configuration.safe_get('volume_backend_name') + self._stats['volume_backend_name'] = backend_name or 'hgst' + self.update_volume_stats() + + def _log_cli_err(self, err): + """Dumps the full command output to a logfile in error cases.""" + LOG.error(_LE("CLI fail: '%(cmd)s' = %(code)s\nout: %(stdout)s\n" + "err: %(stderr)s"), + {'cmd': err.cmd, 'code': err.exit_code, + 'stdout': err.stdout, 'stderr': err.stderr}) + + def _find_vgc_host(self): + """Finds vgc-cluster hostname for this box.""" + params = [self.VGCCLUSTER, "domain-list", "-1"] + try: + out, unused = self._execute(*params, run_as_root=True) + except processutils.ProcessExecutionError as err: + self._log_cli_err(err) + msg = _("Unable to get list of domain members, check that " + "the cluster is running.") + raise exception.VolumeDriverException(message=msg) + domain = out.splitlines() + params = ["ip", "addr", "list"] + try: + out, unused = self._execute(*params, run_as_root=False) + except processutils.ProcessExecutionError as err: + self._log_cli_err(err) + msg = _("Unable to get list of IP addresses on this host, " + "check permissions and networking.") + raise exception.VolumeDriverException(message=msg) + nets = out.splitlines() + for host in domain: + try: + ip = socket.gethostbyname(host) + for l in nets: + x = l.strip() + if x.startswith("inet %s/" % ip): + return host + except socket.error: + pass + msg = _("Current host isn't part of HGST domain.") + raise exception.VolumeDriverException(message=msg) + + def _hostname(self): + """Returns hostname to use for cluster operations on this box.""" + if self._vgc_host is None: + self._vgc_host = self._find_vgc_host() + return self._vgc_host + + def _make_server_list(self): + """Converts a comma list into params for use by HGST CLI.""" + csv = self.configuration.safe_get('hgst_storage_servers') + servers = csv.split(",") + params = [] + for server in servers: + params.append('-S') + params.append(six.text_type(server)) + return params + + def _make_space_name(self, name): + """Generates the hashed name for the space from the name. + + This must be called in a locked context as there are race conditions + where 2 contexts could both pick what they think is an unallocated + space name, and fail later on due to that conflict. + """ + # Sanitize the name string + valid_chars = "-_.%s%s" % (string.ascii_letters, string.digits) + name = ''.join(c for c in name if c in valid_chars) + name = name.strip(".") # Remove any leading .s from evil users + name = name or "space" # In case of all illegal chars, safe default + # Start out with just the name, truncated to 14 characters + outname = name[0:13] + # See what names already defined + params = [self.VGCCLUSTER, "space-list", "--name-only"] + try: + out, unused = self._execute(*params, run_as_root=True) + except processutils.ProcessExecutionError as err: + self._log_cli_err(err) + msg = _("Unable to get list of spaces to make new name. Please " + "verify the cluster is running.") + raise exception.VolumeDriverException(message=msg) + names = out.splitlines() + # And anything in /dev/* is also illegal + names += os.listdir("/dev") # Do it the Python way! + names += ['.', '..'] # Not included above + # While there's a conflict, add incrementing digits until it passes + itr = 0 + while outname in names: + itrstr = six.text_type(itr) + outname = outname[0:13 - len(itrstr)] + itrstr + itr += 1 + return outname + + def _get_space_size_redundancy(self, space_name): + """Parse space output to get allocated size and redundancy.""" + params = [self.VGCCLUSTER, "space-list", "-n", space_name, "--json"] + try: + out, unused = self._execute(*params, run_as_root=True) + except processutils.ProcessExecutionError as err: + self._log_cli_err(err) + msg = _("Unable to get information on space %(space)s, please " + "verify that the cluster is running and " + "connected.") % {'space': space_name} + raise exception.VolumeDriverException(message=msg) + ret = json.loads(out) + retval = {} + retval['redundancy'] = int(ret['resources'][0]['redundancy']) + retval['sizeBytes'] = int(ret['resources'][0]['sizeBytes']) + return retval + + def _adjust_size_g(self, size_g): + """Adjust space size to next legal value because of redundancy.""" + # Extending requires expanding to a multiple of the # of + # storage hosts in the cluster + count = len(self._make_server_list()) / 2 # Remove -s from count + if size_g % count: + size_g = int(size_g + count) + size_g -= size_g % count + return int(math.ceil(size_g)) + + def do_setup(self, context): + pass + + def _get_space_name(self, volume): + """Pull name of /dev/ from the provider_id.""" + try: + return volume.get('provider_id') + except Exception: + return '' # Some error during create, may be able to continue + + def _handle_blocked(self, err, msg): + """Safely handle a return code of BLOCKED from a cluster command. + + Handle the case where a command is in BLOCKED state by trying to + cancel it. If the cancel fails, then the command actually did + complete. If the cancel succeeds, then throw the original error + back up the stack. + """ + if (err.stdout is not None) and (self.BLOCKED in err.stdout): + # Command is queued but did not complete in X seconds, so + # we will cancel it to keep things sane. + request = err.stdout.split('\n', 1)[0].strip() + params = [self.VGCCLUSTER, 'request-cancel'] + params += ['-r', six.text_type(request)] + throw_err = False + try: + self._execute(*params, run_as_root=True) + # Cancel succeeded, the command was aborted + # Send initial exception up the stack + LOG.error(_LE("VGC-CLUSTER command blocked and cancelled.")) + # Can't throw it here, the except below would catch it! + throw_err = True + except Exception: + # The cancel failed because the command was just completed. + # That means there was no failure, so continue with Cinder op + pass + if throw_err: + self._log_cli_err(err) + msg = _("Command %(cmd)s blocked in the CLI and was " + "cancelled") % {'cmd': six.text_type(err.cmd)} + raise exception.VolumeDriverException(message=msg) + else: + # Some other error, just throw it up the chain + self._log_cli_err(err) + raise exception.VolumeDriverException(message=msg) + + def _add_cinder_apphost(self, spacename): + """Add this host to the apphost list of a space.""" + # Connect to source volume + params = [self.VGCCLUSTER, 'space-set-apphosts'] + params += ['-n', spacename] + params += ['-A', self._hostname()] + params += ['--action', 'ADD'] # Non-error to add already existing + try: + self._execute(*params, run_as_root=True) + except processutils.ProcessExecutionError as err: + msg = _("Unable to add Cinder host to apphosts for space " + "%(space)s") % {'space': spacename} + self._handle_blocked(err, msg) + + @lockutils.synchronized('devices', 'cinder-hgst-') + def create_volume(self, volume): + """API entry to create a volume on the cluster as a HGST space. + + Creates a volume, adjusting for GiB/GB sizing. Locked to ensure we + don't have race conditions on the name we pick to use for the space. + """ + # For ease of deugging, use friendly name if it exists + volname = self._make_space_name(volume['display_name'] + or volume['name']) + volnet = self.configuration.safe_get('hgst_net') + volbytes = volume['size'] * units.Gi # OS=Base2, but HGST=Base10 + volsize_gb_cinder = int(math.ceil(float(volbytes) / + float(self.SPACEGB))) + volsize_g = self._adjust_size_g(volsize_gb_cinder) + params = [self.VGCCLUSTER, 'space-create'] + params += ['-n', six.text_type(volname)] + params += ['-N', six.text_type(volnet)] + params += ['-s', six.text_type(volsize_g)] + params += ['--redundancy', six.text_type( + self.configuration.safe_get('hgst_redundancy'))] + params += ['--user', six.text_type( + self.configuration.safe_get('hgst_space_user'))] + params += ['--group', six.text_type( + self.configuration.safe_get('hgst_space_group'))] + params += ['--mode', six.text_type( + self.configuration.safe_get('hgst_space_mode'))] + params += self._make_server_list() + params += ['-A', self._hostname()] # Make it visible only here + try: + self._execute(*params, run_as_root=True) + except processutils.ProcessExecutionError as err: + msg = _("Error in space-create for %(space)s of size " + "%(size)d GB") % {'space': volname, + 'size': int(volsize_g)} + self._handle_blocked(err, msg) + # Stash away the hashed name + provider = {} + provider['provider_id'] = volname + return provider + + def update_volume_stats(self): + """Parse the JSON output of vgc-cluster to find space available.""" + params = [self.VGCCLUSTER, "host-storage", "--json"] + try: + out, unused = self._execute(*params, run_as_root=True) + ret = json.loads(out) + cap = int(ret["totalCapacityBytes"] / units.Gi) + used = int(ret["totalUsedBytes"] / units.Gi) + avail = cap - used + if int(self.configuration.safe_get('hgst_redundancy')) == 1: + cap = int(cap / 2) + avail = int(avail / 2) + # Reduce both by 1 GB due to BZ 28320 + if cap > 0: + cap = cap - 1 + if avail > 0: + avail = avail - 1 + except processutils.ProcessExecutionError as err: + # Could be cluster still starting up, return unknown for now + LOG.warning(_LW("Unable to poll cluster free space.")) + self._log_cli_err(err) + cap = 'unknown' + avail = 'unknown' + self._stats['free_capacity_gb'] = avail + self._stats['total_capacity_gb'] = cap + self._stats['reserved_percentage'] = 0 + + def get_volume_stats(self, refresh=False): + """Return Volume statistics, potentially cached copy.""" + if refresh: + self.update_volume_stats() + return self._stats + + def create_cloned_volume(self, volume, src_vref): + """Create a cloned volume from an existing one. + + No cloning operation in the current release so simply copy using + DD to a new space. This could be a lengthy operation. + """ + # Connect to source volume + volname = self._get_space_name(src_vref) + self._add_cinder_apphost(volname) + + # Make new volume + provider = self.create_volume(volume) + self._add_cinder_apphost(provider['provider_id']) + + # And copy original into it... + info = self._get_space_size_redundancy(volname) + volutils.copy_volume( + self.local_path(src_vref), + "/dev/" + provider['provider_id'], + info['sizeBytes'] / units.Mi, + self.configuration.volume_dd_blocksize, + execute=self._execute) + + # That's all, folks! + return provider + + def copy_image_to_volume(self, context, volume, image_service, image_id): + """Fetch the image from image_service and write it to the volume.""" + image_utils.fetch_to_raw(context, + image_service, + image_id, + self.local_path(volume), + self.configuration.volume_dd_blocksize, + size=volume['size']) + + def copy_volume_to_image(self, context, volume, image_service, image_meta): + """Copy the volume to the specified image.""" + image_utils.upload_volume(context, + image_service, + image_meta, + self.local_path(volume)) + + def delete_volume(self, volume): + """Delete a Volume's underlying space.""" + volname = self._get_space_name(volume) + if volname: + params = [self.VGCCLUSTER, 'space-delete'] + params += ['-n', six.text_type(volname)] + # This can fail benignly when we are deleting a snapshot + try: + self._execute(*params, run_as_root=True) + except processutils.ProcessExecutionError as err: + LOG.warning(_LW("Unable to delete space %(space)s"), + {'space': volname}) + self._log_cli_err(err) + else: + # This can be benign when we are deleting a snapshot + LOG.warning(_LW("Attempted to delete a space that's not there.")) + + def _check_host_storage(self, server): + if ":" not in server: + msg = _("hgst_storage server %(svr)s not of format " + ":") % {'svr': server} + raise exception.VolumeDriverException(message=msg) + h, b = server.split(":") + try: + params = [self.VGCCLUSTER, 'host-storage', '-h', h] + self._execute(*params, run_as_root=True) + except processutils.ProcessExecutionError as err: + self._log_cli_err(err) + msg = _("Storage host %(svr)s not detected, verify " + "name") % {'svr': six.text_type(server)} + raise exception.VolumeDriverException(message=msg) + + def check_for_setup_error(self): + """Throw an exception if configuration values/setup isn't okay.""" + # Verify vgc-cluster exists and is executable by cinder user + try: + params = [self.VGCCLUSTER, '--version'] + self._execute(*params, run_as_root=True) + except processutils.ProcessExecutionError as err: + self._log_cli_err(err) + msg = _("Cannot run vgc-cluster command, please ensure software " + "is installed and permissions are set properly.") + raise exception.VolumeDriverException(message=msg) + + # Checks the host is identified with the HGST domain, as well as + # that vgcnode and vgcclustermgr services are running. + self._vgc_host = None + self._hostname() + + # Redundancy better be 0 or 1, otherwise no comprendo + r = six.text_type(self.configuration.safe_get('hgst_redundancy')) + if r not in ["0", "1"]: + msg = _("hgst_redundancy must be set to 0 (non-HA) or 1 (HA) in " + "cinder.conf.") + raise exception.VolumeDriverException(message=msg) + + # Verify user and group exist or we can't connect volumes + try: + pwd.getpwnam(self.configuration.safe_get('hgst_space_user')) + grp.getgrnam(self.configuration.safe_get('hgst_space_group')) + except KeyError as err: + msg = _("hgst_group %(grp)s and hgst_user %(usr)s must map to " + "valid users/groups in cinder.conf") % { + 'grp': self.configuration.safe_get('hgst_space_group'), + 'usr': self.configuration.safe_get('hgst_space_user')} + raise exception.VolumeDriverException(message=msg) + + # Verify mode is a nicely formed octal or integer + try: + int(self.configuration.safe_get('hgst_space_mode')) + except Exception as err: + msg = _("hgst_space_mode must be an octal/int in cinder.conf") + raise exception.VolumeDriverException(message=msg) + + # Validate network maps to something we know about + try: + params = [self.VGCCLUSTER, 'network-list'] + params += ['-N', self.configuration.safe_get('hgst_net')] + self._execute(*params, run_as_root=True) + except processutils.ProcessExecutionError as err: + self._log_cli_err(err) + msg = _("hgst_net %(net)s specified in cinder.conf not found " + "in cluster") % { + 'net': self.configuration.safe_get('hgst_net')} + raise exception.VolumeDriverException(message=msg) + + # Storage servers require us to split them up and check for + sl = self.configuration.safe_get('hgst_storage_servers') + if (sl is None) or (six.text_type(sl) == ""): + msg = _("hgst_storage_servers must be defined in cinder.conf") + raise exception.VolumeDriverException(message=msg) + servers = sl.split(",") + # Each server must be of the format : w/host in domain + for server in servers: + self._check_host_storage(server) + + # We made it here, we should be good to go! + return True + + def create_snapshot(self, snapshot): + """Create a snapshot volume. + + We don't yet support snaps in SW so make a new volume and dd the + source one into it. This could be a lengthy operation. + """ + origvol = {} + origvol['name'] = snapshot['volume_name'] + origvol['size'] = snapshot['volume_size'] + origvol['id'] = snapshot['volume_id'] + origvol['provider_id'] = snapshot.get('volume').get('provider_id') + # Add me to the apphosts so I can see the volume + self._add_cinder_apphost(self._get_space_name(origvol)) + + # Make snapshot volume + snapvol = {} + snapvol['display_name'] = snapshot['display_name'] + snapvol['name'] = snapshot['name'] + snapvol['size'] = snapshot['volume_size'] + snapvol['id'] = snapshot['id'] + provider = self.create_volume(snapvol) + # Create_volume attaches the volume to this host, ready to snapshot. + # Copy it using dd for now, we don't have real snapshots + # We need to copy the entire allocated volume space, Nova will allow + # full access, even beyond requested size (when our volume is larger + # due to our ~1B byte alignment or cluster makeup) + info = self._get_space_size_redundancy(origvol['provider_id']) + volutils.copy_volume( + self.local_path(origvol), + "/dev/" + provider['provider_id'], + info['sizeBytes'] / units.Mi, + self.configuration.volume_dd_blocksize, + execute=self._execute) + return provider + + def delete_snapshot(self, snapshot): + """Delete a snapshot. For now, snapshots are full volumes.""" + self.delete_volume(snapshot) + + def create_volume_from_snapshot(self, volume, snapshot): + """Create volume from a snapshot, but snaps still full volumes.""" + return self.create_cloned_volume(volume, snapshot) + + def extend_volume(self, volume, new_size): + """Extend an existing volume. + + We may not actually need to resize the space because it's size is + always rounded up to a function of the GiB/GB and number of storage + nodes. + """ + volname = self._get_space_name(volume) + info = self._get_space_size_redundancy(volname) + volnewbytes = new_size * units.Gi + new_size_g = math.ceil(float(volnewbytes) / float(self.SPACEGB)) + wantedsize_g = self._adjust_size_g(new_size_g) + havesize_g = (info['sizeBytes'] / self.SPACEGB) + if havesize_g >= wantedsize_g: + return # Already big enough, happens with redundancy + else: + # Have to extend it + delta = int(wantedsize_g - havesize_g) + params = [self.VGCCLUSTER, 'space-extend'] + params += ['-n', six.text_type(volname)] + params += ['-s', six.text_type(delta)] + params += self._make_server_list() + try: + self._execute(*params, run_as_root=True) + except processutils.ProcessExecutionError as err: + msg = _("Error in space-extend for volume %(space)s with " + "%(size)d additional GB") % {'space': volname, + 'size': delta} + self._handle_blocked(err, msg) + + def initialize_connection(self, volume, connector): + """Return connection information. + + Need to return noremovehost so that the Nova host + doesn't accidentally remove us from the apphost list if it is + running on the same host (like in devstack testing). + """ + hgst_properties = {'name': volume['provider_id'], + 'noremovehost': self._hostname()} + return {'driver_volume_type': 'hgst', + 'data': hgst_properties} + + def local_path(self, volume): + """Query the provider_id to figure out the proper devnode.""" + return "/dev/" + self._get_space_name(volume) + + def create_export(self, context, volume): + # Not needed for spaces + pass + + def remove_export(self, context, volume): + # Not needed for spaces + pass + + def terminate_connection(self, volume, connector, **kwargs): + # Not needed for spaces + pass + + def ensure_export(self, context, volume): + # Not needed for spaces + pass diff --git a/etc/cinder/rootwrap.d/volume.filters b/etc/cinder/rootwrap.d/volume.filters index a00cdcc4d5e..6d38a187d28 100644 --- a/etc/cinder/rootwrap.d/volume.filters +++ b/etc/cinder/rootwrap.d/volume.filters @@ -188,3 +188,6 @@ aureplicationmon: EnvFilter, env, root, LANG=, STONAVM_HOME=, LD_LIBRARY_PATH=, # cinder/volume/drivers/tintri.py mv: CommandFilter, mv, root + +# cinder/volume/drivers/hgst.py +vgc-cluster: CommandFilter, vgc-cluster, root