diff --git a/octane/commands/upgrade_db.py b/octane/commands/upgrade_db.py index 570f012a..ae47e318 100644 --- a/octane/commands/upgrade_db.py +++ b/octane/commands/upgrade_db.py @@ -58,6 +58,8 @@ def upgrade_db(orig_id, seed_id, db_role_name): db.mysqldump_restore_to_env(seed_env, db_role_name, fname) db.db_sync(seed_env) + if db.does_perform_cinder_volume_update_host(orig_env): + db.cinder_volume_update_host(orig_env, seed_env) def upgrade_db_with_graph(orig_id, seed_id): diff --git a/octane/magic_consts.py b/octane/magic_consts.py index d840be84..17ed36b8 100644 --- a/octane/magic_consts.py +++ b/octane/magic_consts.py @@ -67,6 +67,7 @@ OSD_UPGRADE_REQUIRED_PACKAGES = [ COBBLER_DROP_VERSION = "7.0" CEPH_UPSTART_VERSION = "7.0" NOVA_FLAVOR_DATA_MIGRATION_VERSION = "7.0" +CINDER_UPDATE_VOLUME_HOST_VERSION = "7.0" MIRRORS_EXTRA_DIRS = ["ubuntu-full", "mos-ubuntu"] @@ -167,3 +168,4 @@ COMPUTE_PREUPGRADE_PACKAGES = { } ASTUTE_YAML = "/etc/fuel/astute.yaml" +CINDER_CONF = "/etc/cinder/cinder.conf" diff --git a/octane/tests/test_db.py b/octane/tests/test_db.py index 071f6b22..a8337027 100644 --- a/octane/tests/test_db.py +++ b/octane/tests/test_db.py @@ -121,3 +121,70 @@ def test_nova_migrate_flavor_data(mocker, statuses, is_error, is_timeout): db.nova_migrate_flavor_data(env, attempts=attempts) FLAVOR_STATUS = "{0} instances matched query, {1} completed" + + +@pytest.mark.parametrize(("version", "result"), [ + ("6.1", False), + ("7.0", True), + ("8.0", False), +]) +def test_does_perform_cinder_volume_update_host(version, result): + env = mock.Mock(data={"fuel_version": version}) + assert db.does_perform_cinder_volume_update_host(env) == result + + +def test_cinder_volume_update_host(mocker): + mock_orig_env = mock.Mock() + mock_new_env = mock.Mock() + + mock_orig_cont = mock.Mock() + mock_new_cont = mock.Mock() + + mock_get = mocker.patch("octane.util.env.get_one_controller") + mock_get.side_effect = [mock_orig_cont, mock_new_cont] + + mock_get_current = mocker.patch("octane.util.db.get_current_host") + mock_get_new = mocker.patch("octane.util.db.get_new_host") + + mock_ssh = mocker.patch("octane.util.ssh.call") + db.cinder_volume_update_host(mock_orig_env, mock_new_env) + mock_ssh.assert_called_once_with( + ["cinder-manage", "volume", "update_host", + "--currenthost", mock_get_current.return_value, + "--newhost", mock_get_new.return_value], + node=mock_new_cont, parse_levels=True) + assert mock_get.call_args_list == [ + mock.call(mock_orig_env), + mock.call(mock_new_env), + ] + mock_get_current.assert_called_once_with(mock_orig_cont) + mock_get_new.assert_called_once_with(mock_new_cont) + + +@pytest.mark.parametrize(("func", "content", "expected"), [ + (db.get_current_host, [ + (None, "DEFAULT", None, None), + (None, "DEFAULT", "host", "fakehost"), + (None, "DEFAULT", "volume_backend_name", "fakebackend"), + ], "fakehost#fakebackend"), + (db.get_new_host, [ + (None, "DEFAULT", None, None), + (None, "DEFAULT", "host", "fakehost_default"), + (None, "RBD-backend", None, None), + (None, "RBD-backend", "volume_backend_name", "fakebackend"), + ], "fakehost_default@fakebackend#RBD-backend"), + (db.get_new_host, [ + (None, "DEFAULT", None, None), + (None, "DEFAULT", "host", "fakehost_default"), + (None, "RBD-backend", None, None), + (None, "RBD-backend", "backend_host", "fakehost_specific"), + (None, "RBD-backend", "volume_backend_name", "fakebackend"), + ], "fakehost_specific@fakebackend#RBD-backend"), +]) +def test_get_hosts_functional(mocker, func, content, expected): + mock_node = mock.Mock() + mocker.patch("octane.util.ssh.sftp") + mock_iter = mocker.patch("octane.util.helpers.iterate_parameters") + mock_iter.return_value = content + result = func(mock_node) + assert expected == result diff --git a/octane/tests/test_helpers.py b/octane/tests/test_helpers.py index c65fbe9f..70d3827b 100644 --- a/octane/tests/test_helpers.py +++ b/octane/tests/test_helpers.py @@ -9,6 +9,9 @@ # 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 + import pytest from octane.util import helpers @@ -85,3 +88,33 @@ NORMALIZED_DATA = [ def test_normalized_cliff_show_json(data, normalized_data): res = helpers.normalized_cliff_show_json(data) assert res == normalized_data + + +@pytest.mark.parametrize(("source", "parameters_to_get", "parameters"), [ + ([ + (None, None, "option1", "value1"), + (None, "section1", None, None), + (None, "section1", None, None), + (None, "section1", "option2", "value2"), + (None, "section1", "option3", "value31"), + (None, "section2", None, None), + (None, "section2", "option4", "value4"), + (None, "section2", "option3", "value32"), + (None, "section3", "option3", "value33"), + ], { + "opt2": [("section1", "option2")], + "opt3": [("section1", "option3"), ("section2", "option3")], + "opt4": [("section1", "option4"), ("section2", "option4")], + }, { + "opt2": "value2", + "opt3": "value32", + "opt4": "value4", + }), +]) +def test_get_parameters(mocker, source, parameters_to_get, parameters): + mock_fp = mock.Mock() + mock_iter = mocker.patch("octane.util.helpers.iterate_parameters") + mock_iter.return_value = source + result = helpers.get_parameters(mock_fp, parameters_to_get) + mock_iter.assert_called_once_with(mock_fp) + assert result == parameters diff --git a/octane/tests/test_util_node.py b/octane/tests/test_util_node.py index 0b9f7969..c88cf8f2 100644 --- a/octane/tests/test_util_node.py +++ b/octane/tests/test_util_node.py @@ -200,3 +200,45 @@ def test_restart_nova_services(mocker, node, stdout, nova_services_to_restart): call_mock.assert_any_call(["service", service, "restart"], node=node) call_output_mock.assert_called_once_with( ["service", "--status-all"], node=node) + + +@pytest.mark.parametrize( + ("parameters", "parameters_to_get", "required", "ensure", "error"), [ + ({ + "opt1": "value1", + "opt2": "value2", + }, { + "opt1": [("section1", "option1")], + "opt2": [("section2", "option2")], + }, ("section1/option1", "section2/option2"), True, False), + + ({}, { + "opt1": [("section1", "option1")], + }, ("section1/option1"), True, True), + + ({}, { + "opt1": [("section1", "option1")], + }, (), False, False), + ] +) +def test_get_parameters(mocker, parameters, parameters_to_get, required, + ensure, error): + mock_node = mock.Mock(data={"id": 1}) + filename = "fake/filename.conf" + mock_sftp = mocker.patch("octane.util.ssh.sftp") + mock_get = mocker.patch("octane.util.helpers.get_parameters") + mock_get.return_value = parameters + if ensure and error: + msg = ("Could not get parameters from the file " + "node-1[fake/filename.conf]: {parameters}" + .format(parameters=", ".join(required))) + with pytest.raises(node_util.AbsentParametersError, message=msg): + node_util.get_parameters(mock_node, filename, parameters_to_get, + ensure=ensure) + else: + result = node_util.get_parameters( + mock_node, filename, parameters_to_get, ensure=ensure) + assert result == parameters + mock_get.assert_called_once_with( + mock_sftp.return_value.open.return_value.__enter__.return_value, + parameters_to_get) diff --git a/octane/util/db.py b/octane/util/db.py index cea4693b..c3ac3a2b 100644 --- a/octane/util/db.py +++ b/octane/util/db.py @@ -19,6 +19,7 @@ from distutils import version from octane import magic_consts from octane.util import env as env_util +from octane.util import node as node_util from octane.util import ssh @@ -73,6 +74,49 @@ FLAVOR_STATUS_RE = re.compile( "(?P[0-9]+) completed$") +def does_perform_cinder_volume_update_host(env): + env_version = version.StrictVersion(env.data["fuel_version"]) + return env_version == \ + version.StrictVersion(magic_consts.CINDER_UPDATE_VOLUME_HOST_VERSION) + + +def cinder_volume_update_host(orig_env, new_env): + orig_controller = env_util.get_one_controller(orig_env) + new_controller = env_util.get_one_controller(new_env) + current_host = get_current_host(orig_controller) + new_host = get_new_host(new_controller) + ssh.call(["cinder-manage", "volume", "update_host", + "--currenthost", current_host, + "--newhost", new_host], + node=new_controller, parse_levels=True) + + +def get_current_host(node): + parameters = node_util.get_parameters(node, magic_consts.CINDER_CONF, { + "host": [("DEFAULT", "host")], + "backend": [("DEFAULT", "volume_backend_name")], + }) + # NOTE(akscram): result = "rbd:volumes#DEFAULT" + result = "{host}#{backend}".format( + host=parameters["host"], + backend=parameters["backend"], + ) + return result + + +def get_new_host(node): + parameters = node_util.get_parameters(node, magic_consts.CINDER_CONF, { + "host": [("DEFAULT", "host"), ("RBD-backend", "backend_host")], + "backend": [("RBD-backend", "volume_backend_name")], + }) + # NOTE(akscram): result = "rbd:volumes@RBD-backend#RBD-backend" + result = "{host}@{backend}#RBD-backend".format( + host=parameters["host"], + backend=parameters["backend"], + ) + return result + + def mysqldump_from_env(env, role_name, dbs, fname): node = env_util.get_one_node_of(env, role_name) cmd = [ diff --git a/octane/util/helpers.py b/octane/util/helpers.py index b422a81e..2254cfac 100644 --- a/octane/util/helpers.py +++ b/octane/util/helpers.py @@ -53,6 +53,19 @@ def iterate_parameters(fp): yield line, section, None, None +def get_parameters(fp, parameters_to_get): + parameters_map = {} + for key, values in parameters_to_get.items(): + for value in values: + parameters_map[value] = key + parameters = {} + for _, section, parameter, value in iterate_parameters(fp): + parameter_name = parameters_map.get((section, parameter)) + if parameter_name is not None and value is not None: + parameters[parameter_name] = value + return parameters + + def normalized_cliff_show_json(data): if isinstance(data, list): return {i['Field']: i['Value'] for i in data} diff --git a/octane/util/node.py b/octane/util/node.py index 6bd423e2..c45da3f8 100644 --- a/octane/util/node.py +++ b/octane/util/node.py @@ -18,6 +18,7 @@ import sys import time from distutils import version +from octane.util import helpers from octane.util import ssh LOG = logging.getLogger(__name__) @@ -182,3 +183,32 @@ def restart_nova_services(node): _, status, _, service = service_line.split() if status == "+" and service.startswith("nova"): ssh.call(["service", service, "restart"], node=node) + + +class AbsentParametersError(Exception): + msg = "Could not get parameters from the file " \ + "node-{node_id}[{filename}]: {parameters}" + + def __init__(self, node_id, filename, parameters): + super(AbsentParametersError, self).__init__(self.msg.format( + node_id=node_id, + filename=filename, + parameters=", ".join(parameters), + )) + + +def get_parameters(node, filename, parameters_to_get, ensure=True): + with ssh.sftp(node).open(filename) as fp: + parameters = helpers.get_parameters(fp, parameters_to_get) + if ensure: + required_parameters = set(parameters_to_get) + current_parameters = set(parameters) + absent_parameters = required_parameters - current_parameters + if absent_parameters: + flat_parameters = [] + for aparam in absent_parameters: + for param in parameters_to_get[aparam]: + flat_parameters.append("/".join(param)) + raise AbsentParametersError( + node.data["id"], filename, flat_parameters) + return parameters