From ea36b5e6a858fdf6840916a387255dcc7cbac1c1 Mon Sep 17 00:00:00 2001 From: "Earle F. Philhower, III" Date: Thu, 28 May 2015 15:03:47 -0700 Subject: [PATCH] Implement Cinder Volume driver for HGST Solutions Enables native support for HGST Solutions software Spaces as Cinder volumes or snapshots. Each Cinder Volume or Snapshot is mapped to a single HGST Space. This space may be named differently from the actual volume/snap ID and so we store the Space Name<->ID mapping in the volume provider_id. Snapshots are not supported with the current HGST Solutions software, so they are implemented as heavyweight copies in the driver. All Spaces are made visible on the Cinder host for speed of access, and only the spaces requested by Nova instances are actually made visible on other members of the cluster. Not all nodes need SSD storage to take advantage of these volumes, cinder.conf entries specify which nodes share their local SSDs. Prerequisites: HGST Solutions is a software-SAN-like package which allows local SSDs in a cluster to be combined into a single storage pool. The driver has a series of configuration options which must be set in the cinder.conf, prefixed with hgst_*. Nodes should have the HGST software installed and connected to the HGST domain prior to rolling out Nova nodes using this storage. Additional patches required for full functionality (being tracked under the same blueprint): OS-brick patch @ https://review.openstack.org/#/c/186588/ Nova patch @ https://review.openstack.org/#/c/186594/ Nova patch required until os-brick<->nova connection finalized in https://review.openstack.org/#/c/175569/ Change-Id: Ie0ff03856edd4b5610f4412951ea7c970ad63c8c Implements: blueprint add-volume-driver-hgst-solutions --- cinder/tests/unit/volume/drivers/test_hgst.py | 939 ++++++++++++++++++ cinder/volume/drivers/hgst.py | 602 +++++++++++ etc/cinder/rootwrap.d/volume.filters | 3 + 3 files changed, 1544 insertions(+) create mode 100644 cinder/tests/unit/volume/drivers/test_hgst.py create mode 100644 cinder/volume/drivers/hgst.py 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 000000000..51a8345c2 --- /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 000000000..59b0b29f2 --- /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 a00cdcc4d..6d38a187d 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 -- 2.45.2