Browse Source

Perform cinder-manage volume update_host

The backend name for volumes was changed since Kilo. To make these
volumes back manageable in Mitaka their hostnames have to be changed.
The `cinder-manage volume update_host` command is suitable to do that.

Change-Id: Ieeadd81c714186d58a3e3dfe5fd1223c40d16996
Partial-Bug: #1624341
changes/35/371935/4
Ilya Kharin 2 years ago
parent
commit
33a8b5abb2

+ 2
- 0
octane/commands/upgrade_db.py View File

@@ -58,6 +58,8 @@ def upgrade_db(orig_id, seed_id, db_role_name):
58 58
 
59 59
     db.mysqldump_restore_to_env(seed_env, db_role_name, fname)
60 60
     db.db_sync(seed_env)
61
+    if db.does_perform_cinder_volume_update_host(orig_env):
62
+        db.cinder_volume_update_host(orig_env, seed_env)
61 63
 
62 64
 
63 65
 def upgrade_db_with_graph(orig_id, seed_id):

+ 2
- 0
octane/magic_consts.py View File

@@ -67,6 +67,7 @@ OSD_UPGRADE_REQUIRED_PACKAGES = [
67 67
 COBBLER_DROP_VERSION = "7.0"
68 68
 CEPH_UPSTART_VERSION = "7.0"
69 69
 NOVA_FLAVOR_DATA_MIGRATION_VERSION = "7.0"
70
+CINDER_UPDATE_VOLUME_HOST_VERSION = "7.0"
70 71
 
71 72
 
72 73
 MIRRORS_EXTRA_DIRS = ["ubuntu-full", "mos-ubuntu"]
@@ -167,3 +168,4 @@ COMPUTE_PREUPGRADE_PACKAGES = {
167 168
 }
168 169
 
169 170
 ASTUTE_YAML = "/etc/fuel/astute.yaml"
171
+CINDER_CONF = "/etc/cinder/cinder.conf"

+ 67
- 0
octane/tests/test_db.py View File

@@ -121,3 +121,70 @@ def test_nova_migrate_flavor_data(mocker, statuses, is_error, is_timeout):
121 121
         db.nova_migrate_flavor_data(env, attempts=attempts)
122 122
 
123 123
 FLAVOR_STATUS = "{0} instances matched query, {1} completed"
124
+
125
+
126
+@pytest.mark.parametrize(("version", "result"), [
127
+    ("6.1", False),
128
+    ("7.0", True),
129
+    ("8.0", False),
130
+])
131
+def test_does_perform_cinder_volume_update_host(version, result):
132
+    env = mock.Mock(data={"fuel_version": version})
133
+    assert db.does_perform_cinder_volume_update_host(env) == result
134
+
135
+
136
+def test_cinder_volume_update_host(mocker):
137
+    mock_orig_env = mock.Mock()
138
+    mock_new_env = mock.Mock()
139
+
140
+    mock_orig_cont = mock.Mock()
141
+    mock_new_cont = mock.Mock()
142
+
143
+    mock_get = mocker.patch("octane.util.env.get_one_controller")
144
+    mock_get.side_effect = [mock_orig_cont, mock_new_cont]
145
+
146
+    mock_get_current = mocker.patch("octane.util.db.get_current_host")
147
+    mock_get_new = mocker.patch("octane.util.db.get_new_host")
148
+
149
+    mock_ssh = mocker.patch("octane.util.ssh.call")
150
+    db.cinder_volume_update_host(mock_orig_env, mock_new_env)
151
+    mock_ssh.assert_called_once_with(
152
+        ["cinder-manage", "volume", "update_host",
153
+         "--currenthost", mock_get_current.return_value,
154
+         "--newhost", mock_get_new.return_value],
155
+        node=mock_new_cont, parse_levels=True)
156
+    assert mock_get.call_args_list == [
157
+        mock.call(mock_orig_env),
158
+        mock.call(mock_new_env),
159
+    ]
160
+    mock_get_current.assert_called_once_with(mock_orig_cont)
161
+    mock_get_new.assert_called_once_with(mock_new_cont)
162
+
163
+
164
+@pytest.mark.parametrize(("func", "content", "expected"), [
165
+    (db.get_current_host, [
166
+        (None, "DEFAULT", None, None),
167
+        (None, "DEFAULT", "host", "fakehost"),
168
+        (None, "DEFAULT", "volume_backend_name", "fakebackend"),
169
+    ], "fakehost#fakebackend"),
170
+    (db.get_new_host, [
171
+        (None, "DEFAULT", None, None),
172
+        (None, "DEFAULT", "host", "fakehost_default"),
173
+        (None, "RBD-backend", None, None),
174
+        (None, "RBD-backend", "volume_backend_name", "fakebackend"),
175
+    ], "fakehost_default@fakebackend#RBD-backend"),
176
+    (db.get_new_host, [
177
+        (None, "DEFAULT", None, None),
178
+        (None, "DEFAULT", "host", "fakehost_default"),
179
+        (None, "RBD-backend", None, None),
180
+        (None, "RBD-backend", "backend_host", "fakehost_specific"),
181
+        (None, "RBD-backend", "volume_backend_name", "fakebackend"),
182
+    ], "fakehost_specific@fakebackend#RBD-backend"),
183
+])
184
+def test_get_hosts_functional(mocker, func, content, expected):
185
+    mock_node = mock.Mock()
186
+    mocker.patch("octane.util.ssh.sftp")
187
+    mock_iter = mocker.patch("octane.util.helpers.iterate_parameters")
188
+    mock_iter.return_value = content
189
+    result = func(mock_node)
190
+    assert expected == result

+ 33
- 0
octane/tests/test_helpers.py View File

@@ -9,6 +9,9 @@
9 9
 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10 10
 # License for the specific language governing permissions and limitations
11 11
 # under the License.
12
+
13
+import mock
14
+
12 15
 import pytest
13 16
 
14 17
 from octane.util import helpers
@@ -85,3 +88,33 @@ NORMALIZED_DATA = [
85 88
 def test_normalized_cliff_show_json(data, normalized_data):
86 89
     res = helpers.normalized_cliff_show_json(data)
87 90
     assert res == normalized_data
91
+
92
+
93
+@pytest.mark.parametrize(("source", "parameters_to_get", "parameters"), [
94
+    ([
95
+        (None, None, "option1", "value1"),
96
+        (None, "section1", None, None),
97
+        (None, "section1", None, None),
98
+        (None, "section1", "option2", "value2"),
99
+        (None, "section1", "option3", "value31"),
100
+        (None, "section2", None, None),
101
+        (None, "section2", "option4", "value4"),
102
+        (None, "section2", "option3", "value32"),
103
+        (None, "section3", "option3", "value33"),
104
+    ], {
105
+        "opt2": [("section1", "option2")],
106
+        "opt3": [("section1", "option3"), ("section2", "option3")],
107
+        "opt4": [("section1", "option4"), ("section2", "option4")],
108
+    }, {
109
+        "opt2": "value2",
110
+        "opt3": "value32",
111
+        "opt4": "value4",
112
+    }),
113
+])
114
+def test_get_parameters(mocker, source, parameters_to_get, parameters):
115
+    mock_fp = mock.Mock()
116
+    mock_iter = mocker.patch("octane.util.helpers.iterate_parameters")
117
+    mock_iter.return_value = source
118
+    result = helpers.get_parameters(mock_fp, parameters_to_get)
119
+    mock_iter.assert_called_once_with(mock_fp)
120
+    assert result == parameters

+ 42
- 0
octane/tests/test_util_node.py View File

@@ -200,3 +200,45 @@ def test_restart_nova_services(mocker, node, stdout, nova_services_to_restart):
200 200
         call_mock.assert_any_call(["service", service, "restart"], node=node)
201 201
     call_output_mock.assert_called_once_with(
202 202
         ["service", "--status-all"], node=node)
203
+
204
+
205
+@pytest.mark.parametrize(
206
+    ("parameters", "parameters_to_get", "required", "ensure", "error"), [
207
+        ({
208
+            "opt1": "value1",
209
+            "opt2": "value2",
210
+        }, {
211
+            "opt1": [("section1", "option1")],
212
+            "opt2": [("section2", "option2")],
213
+        }, ("section1/option1", "section2/option2"), True, False),
214
+
215
+        ({}, {
216
+            "opt1": [("section1", "option1")],
217
+        }, ("section1/option1"), True, True),
218
+
219
+        ({}, {
220
+            "opt1": [("section1", "option1")],
221
+        }, (), False, False),
222
+    ]
223
+)
224
+def test_get_parameters(mocker, parameters, parameters_to_get, required,
225
+                        ensure, error):
226
+    mock_node = mock.Mock(data={"id": 1})
227
+    filename = "fake/filename.conf"
228
+    mock_sftp = mocker.patch("octane.util.ssh.sftp")
229
+    mock_get = mocker.patch("octane.util.helpers.get_parameters")
230
+    mock_get.return_value = parameters
231
+    if ensure and error:
232
+        msg = ("Could not get parameters from the file "
233
+               "node-1[fake/filename.conf]: {parameters}"
234
+               .format(parameters=", ".join(required)))
235
+        with pytest.raises(node_util.AbsentParametersError, message=msg):
236
+            node_util.get_parameters(mock_node, filename, parameters_to_get,
237
+                                     ensure=ensure)
238
+    else:
239
+        result = node_util.get_parameters(
240
+            mock_node, filename, parameters_to_get, ensure=ensure)
241
+        assert result == parameters
242
+    mock_get.assert_called_once_with(
243
+        mock_sftp.return_value.open.return_value.__enter__.return_value,
244
+        parameters_to_get)

+ 44
- 0
octane/util/db.py View File

@@ -19,6 +19,7 @@ from distutils import version
19 19
 
20 20
 from octane import magic_consts
21 21
 from octane.util import env as env_util
22
+from octane.util import node as node_util
22 23
 from octane.util import ssh
23 24
 
24 25
 
@@ -73,6 +74,49 @@ FLAVOR_STATUS_RE = re.compile(
73 74
     "(?P<completed>[0-9]+) completed$")
74 75
 
75 76
 
77
+def does_perform_cinder_volume_update_host(env):
78
+    env_version = version.StrictVersion(env.data["fuel_version"])
79
+    return env_version == \
80
+        version.StrictVersion(magic_consts.CINDER_UPDATE_VOLUME_HOST_VERSION)
81
+
82
+
83
+def cinder_volume_update_host(orig_env, new_env):
84
+    orig_controller = env_util.get_one_controller(orig_env)
85
+    new_controller = env_util.get_one_controller(new_env)
86
+    current_host = get_current_host(orig_controller)
87
+    new_host = get_new_host(new_controller)
88
+    ssh.call(["cinder-manage", "volume", "update_host",
89
+              "--currenthost", current_host,
90
+              "--newhost", new_host],
91
+             node=new_controller, parse_levels=True)
92
+
93
+
94
+def get_current_host(node):
95
+    parameters = node_util.get_parameters(node, magic_consts.CINDER_CONF, {
96
+        "host": [("DEFAULT", "host")],
97
+        "backend": [("DEFAULT", "volume_backend_name")],
98
+    })
99
+    # NOTE(akscram): result = "rbd:volumes#DEFAULT"
100
+    result = "{host}#{backend}".format(
101
+        host=parameters["host"],
102
+        backend=parameters["backend"],
103
+    )
104
+    return result
105
+
106
+
107
+def get_new_host(node):
108
+    parameters = node_util.get_parameters(node, magic_consts.CINDER_CONF, {
109
+        "host": [("DEFAULT", "host"), ("RBD-backend", "backend_host")],
110
+        "backend": [("RBD-backend", "volume_backend_name")],
111
+    })
112
+    # NOTE(akscram): result = "rbd:volumes@RBD-backend#RBD-backend"
113
+    result = "{host}@{backend}#RBD-backend".format(
114
+        host=parameters["host"],
115
+        backend=parameters["backend"],
116
+    )
117
+    return result
118
+
119
+
76 120
 def mysqldump_from_env(env, role_name, dbs, fname):
77 121
     node = env_util.get_one_node_of(env, role_name)
78 122
     cmd = [

+ 13
- 0
octane/util/helpers.py View File

@@ -53,6 +53,19 @@ def iterate_parameters(fp):
53 53
         yield line, section, None, None
54 54
 
55 55
 
56
+def get_parameters(fp, parameters_to_get):
57
+    parameters_map = {}
58
+    for key, values in parameters_to_get.items():
59
+        for value in values:
60
+            parameters_map[value] = key
61
+    parameters = {}
62
+    for _, section, parameter, value in iterate_parameters(fp):
63
+        parameter_name = parameters_map.get((section, parameter))
64
+        if parameter_name is not None and value is not None:
65
+            parameters[parameter_name] = value
66
+    return parameters
67
+
68
+
56 69
 def normalized_cliff_show_json(data):
57 70
     if isinstance(data, list):
58 71
         return {i['Field']: i['Value'] for i in data}

+ 30
- 0
octane/util/node.py View File

@@ -18,6 +18,7 @@ import sys
18 18
 import time
19 19
 
20 20
 from distutils import version
21
+from octane.util import helpers
21 22
 from octane.util import ssh
22 23
 
23 24
 LOG = logging.getLogger(__name__)
@@ -182,3 +183,32 @@ def restart_nova_services(node):
182 183
         _, status, _, service = service_line.split()
183 184
         if status == "+" and service.startswith("nova"):
184 185
             ssh.call(["service", service, "restart"], node=node)
186
+
187
+
188
+class AbsentParametersError(Exception):
189
+    msg = "Could not get parameters from the file " \
190
+          "node-{node_id}[{filename}]: {parameters}"
191
+
192
+    def __init__(self, node_id, filename, parameters):
193
+        super(AbsentParametersError, self).__init__(self.msg.format(
194
+            node_id=node_id,
195
+            filename=filename,
196
+            parameters=", ".join(parameters),
197
+        ))
198
+
199
+
200
+def get_parameters(node, filename, parameters_to_get, ensure=True):
201
+    with ssh.sftp(node).open(filename) as fp:
202
+        parameters = helpers.get_parameters(fp, parameters_to_get)
203
+    if ensure:
204
+        required_parameters = set(parameters_to_get)
205
+        current_parameters = set(parameters)
206
+        absent_parameters = required_parameters - current_parameters
207
+        if absent_parameters:
208
+            flat_parameters = []
209
+            for aparam in absent_parameters:
210
+                for param in parameters_to_get[aparam]:
211
+                    flat_parameters.append("/".join(param))
212
+            raise AbsentParametersError(
213
+                node.data["id"], filename, flat_parameters)
214
+    return parameters

Loading…
Cancel
Save