blob: 5b1448f25c919c3b15d1041d43ccd84832d8aa12 [file] [log] [blame]
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +03001# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13import logging
Oleh Hryhorov5cfb9d32018-09-11 16:55:24 +000014import time
Vladyslav Drokd182bf12019-01-11 14:02:38 +020015
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +030016import salt
Oleh Hryhorov5cfb9d32018-09-11 16:55:24 +000017from salt.exceptions import CommandExecutionError
Vladyslav Drokd182bf12019-01-11 14:02:38 +020018import six
19from six.moves import urllib
20from six.moves import zip_longest
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +030021
22LOG = logging.getLogger(__name__)
23
24KEYSTONE_LOADED = False
25
26
27def __virtual__():
28 """Only load if the nova module is in __salt__"""
29 if 'keystonev3.project_get_details' in __salt__:
30 global KEYSTONE_LOADED
31 KEYSTONE_LOADED = True
32 return 'novav21'
33
34
35class SaltModuleCallException(Exception):
36
37 def __init__(self, result_dict, *args, **kwargs):
38 super(SaltModuleCallException, self).__init__(*args, **kwargs)
39 self.result_dict = result_dict
40
41
42def _get_failure_function_mapping():
43 return {
44 'create': _create_failed,
45 'update': _update_failed,
46 'find': _find_failed,
47 'delete': _delete_failed,
48 }
49
50
51def _call_nova_salt_module(call_string, name, module_name='novav21'):
52 def inner(*args, **kwargs):
53 func = __salt__['%s.%s' % (module_name, call_string)]
54 result = func(*args, **kwargs)
55 if not result['result']:
56 ret = _get_failure_function_mapping()[func._action_type](
57 name, func._resource_human_readable_name)
58 ret['comment'] += '\nStatus code: %s\n%s' % (result['status_code'],
59 result['comment'])
60 raise SaltModuleCallException(ret)
61 return result['body'].get(func._body_response_key)
62 return inner
63
64
65def _error_handler(fun):
66 @six.wraps(fun)
67 def inner(*args, **kwargs):
68 try:
69 return fun(*args, **kwargs)
70 except SaltModuleCallException as e:
71 return e.result_dict
72 return inner
73
74
75@_error_handler
76def flavor_present(name, cloud_name, vcpus=1, ram=256, disk=0, flavor_id=None,
Dzmitry Stremkouskid37f2c82020-09-11 13:13:58 +020077 extra_specs=None, is_public=False):
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +030078 """Ensures that the flavor exists"""
79 extra_specs = extra_specs or {}
Dzmitry Stremkouskid37f2c82020-09-11 13:13:58 +020080 # There is no way to query flavors by name. And we always list both
81 # public and private flavors
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +030082 flavors = _call_nova_salt_module('flavor_list', name)(
Dzmitry Stremkouskid37f2c82020-09-11 13:13:58 +020083 detail=True, is_public=None, cloud_name=cloud_name)
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +030084 flavor = [flavor for flavor in flavors if flavor['name'] == name]
85 # Flavor names are unique, there is either 1 or 0 with requested name
Dzmitry Stremkouskid37f2c82020-09-11 13:13:58 +020086 # TODO: check all the vcpus, ram etc. and delete the existing flavor if
87 # something does not match, as it is impossible to update exising flavor
88 # apart from its extra specs
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +030089 if flavor:
90 flavor = flavor[0]
91 current_extra_specs = _call_nova_salt_module(
92 'flavor_get_extra_specs', name)(
93 flavor['id'], cloud_name=cloud_name)
94 to_delete = set(current_extra_specs) - set(extra_specs)
95 to_add = set(extra_specs) - set(current_extra_specs)
96 for spec in to_delete:
97 _call_nova_salt_module('flavor_delete_extra_spec', name)(
98 flavor['id'], spec, cloud_name=cloud_name)
99 _call_nova_salt_module('flavor_add_extra_specs', name)(
100 flavor['id'], cloud_name=cloud_name, **extra_specs)
101 if to_delete or to_add:
102 ret = _updated(name, 'Flavor', extra_specs)
103 else:
104 ret = _no_change(name, 'Flavor')
105 else:
106 flavor = _call_nova_salt_module('flavor_create', name)(
Dzmitry Stremkouskid37f2c82020-09-11 13:13:58 +0200107 name, vcpus, ram, disk, id=flavor_id, cloud_name=cloud_name,
108 **{"os-flavor-access:is_public": is_public})
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300109 _call_nova_salt_module('flavor_add_extra_specs', name)(
110 flavor['id'], cloud_name=cloud_name, **extra_specs)
111 flavor['extra_specs'] = extra_specs
112 ret = _created(name, 'Flavor', flavor)
113 return ret
114
115
116@_error_handler
117def flavor_absent(name, cloud_name):
118 """Ensure flavor is absent"""
Vladyslav Drokaed745f2020-10-27 17:20:32 +0100119 # There is no way to query flavors by name. And we always list both
120 # public and private flavors
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300121 flavors = _call_nova_salt_module('flavor_list', name)(
Vladyslav Drokaed745f2020-10-27 17:20:32 +0100122 detail=True, is_public=None, cloud_name=cloud_name)
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300123 flavor = [flavor for flavor in flavors if flavor['name'] == name]
124 # Flavor names are unique, there is either 1 or 0 with requested name
125 if flavor:
126 _call_nova_salt_module('flavor_delete', name)(
127 flavor[0]['id'], cloud_name=cloud_name)
128 return _deleted(name, 'Flavor')
129 return _non_existent(name, 'Flavor')
130
131
132def _get_keystone_project_id_by_name(project_name, cloud_name):
133 if not KEYSTONE_LOADED:
134 LOG.error("Keystone module not found, can not look up project ID "
135 "by name")
136 return None
137 project = __salt__['keystonev3.project_get_details'](
138 project_name, cloud_name=cloud_name)
139 if not project:
140 return None
141 return project['project']['id']
142
143
144@_error_handler
145def quota_present(name, cloud_name, **kwargs):
146 """Ensures that the nova quota exists
147
148 :param name: project name to ensure quota for.
149 """
150 project_name = name
151 project_id = _get_keystone_project_id_by_name(project_name, cloud_name)
152 changes = {}
153 if not project_id:
154 ret = _update_failed(project_name, 'Project quota')
155 ret['comment'] += ('\nCould not retrieve keystone project %s' %
156 project_name)
157 return ret
158 quota = _call_nova_salt_module('quota_list', project_name)(
159 project_id, cloud_name=cloud_name)
160 for key, value in kwargs.items():
161 if quota.get(key) != value:
162 changes[key] = value
163 if changes:
164 _call_nova_salt_module('quota_update', project_name)(
165 project_id, cloud_name=cloud_name, **changes)
166 return _updated(project_name, 'Project quota', changes)
167 else:
168 return _no_change(project_name, 'Project quota')
169
170
171@_error_handler
172def quota_absent(name, cloud_name):
173 """Ensures that the nova quota set to default
174
175 :param name: project name to reset quota for.
176 """
177 project_name = name
178 project_id = _get_keystone_project_id_by_name(project_name, cloud_name)
179 if not project_id:
180 ret = _delete_failed(project_name, 'Project quota')
181 ret['comment'] += ('\nCould not retrieve keystone project %s' %
182 project_name)
183 return ret
184 _call_nova_salt_module('quota_delete', name)(
185 project_id, cloud_name=cloud_name)
186 return _deleted(name, 'Project quota')
187
188
189@_error_handler
190def aggregate_present(name, cloud_name, availability_zone_name=None,
191 hosts=None, metadata=None):
192 """Ensures that the nova aggregate exists"""
193 aggregates = _call_nova_salt_module('aggregate_list', name)(
194 cloud_name=cloud_name)
195 aggregate_exists = [agg for agg in aggregates
196 if agg['name'] == name]
197 metadata = metadata or {}
198 hosts = hosts or []
199 if availability_zone_name:
200 metadata.update(availability_zone=availability_zone_name)
201 if not aggregate_exists:
202 aggregate = _call_nova_salt_module('aggregate_create', name)(
203 name, availability_zone_name, cloud_name=cloud_name)
204 if metadata:
205 _call_nova_salt_module('aggregate_set_metadata', name)(
Dzmitry Stremkouskif81957c2020-04-15 18:40:47 +0200206 name, cloud_name=cloud_name, **metadata)
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300207 aggregate['metadata'] = metadata
208 for host in hosts or []:
209 _call_nova_salt_module('aggregate_add_host', name)(
210 name, host, cloud_name=cloud_name)
211 aggregate['hosts'] = hosts
212 return _created(name, 'Host aggregate', aggregate)
213 else:
214 aggregate = aggregate_exists[0]
215 changes = {}
216 existing_meta = set(aggregate['metadata'].items())
217 requested_meta = set(metadata.items())
Dzmitry Stremkouskif81957c2020-04-15 18:40:47 +0200218 if existing_meta - requested_meta or requested_meta - existing_meta:
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300219 _call_nova_salt_module('aggregate_set_metadata', name)(
220 name, cloud_name=cloud_name, **metadata)
221 changes['metadata'] = metadata
222 hosts_to_add = set(hosts) - set(aggregate['hosts'])
223 hosts_to_remove = set(aggregate['hosts']) - set(hosts)
Dzmitry Stremkouski8babb812020-02-16 17:02:44 +0100224 if hosts and (hosts_to_remove or hosts_to_add):
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300225 for host in hosts_to_add:
226 _call_nova_salt_module('aggregate_add_host', name)(
227 name, host, cloud_name=cloud_name)
228 for host in hosts_to_remove:
229 _call_nova_salt_module('aggregate_remove_host', name)(
230 name, host, cloud_name=cloud_name)
231 changes['hosts'] = hosts
232 if changes:
233 return _updated(name, 'Host aggregate', changes)
234 else:
235 return _no_change(name, 'Host aggregate')
236
237
238@_error_handler
239def aggregate_absent(name, cloud_name):
240 """Ensure aggregate is absent"""
241 existing_aggregates = _call_nova_salt_module('aggregate_list', name)(
242 cloud_name=cloud_name)
243 matching_aggs = [agg for agg in existing_aggregates
244 if agg['name'] == name]
245 if matching_aggs:
246 _call_nova_salt_module('aggregate_delete', name)(
247 name, cloud_name=cloud_name)
248 return _deleted(name, 'Host Aggregate')
249 return _non_existent(name, 'Host Aggregate')
250
251
252@_error_handler
253def keypair_present(name, cloud_name, public_key_file=None, public_key=None):
254 """Ensures that the Nova key-pair exists"""
255 existing_keypairs = _call_nova_salt_module('keypair_list', name)(
256 cloud_name=cloud_name)
257 matching_kps = [kp for kp in existing_keypairs
258 if kp['keypair']['name'] == name]
259 if public_key_file and not public_key:
260 with salt.utils.fopen(public_key_file, 'r') as f:
261 public_key = f.read()
262 if not public_key:
263 ret = _create_failed(name, 'Keypair')
264 ret['comment'] += '\nPlease specify public key for keypair creation.'
265 return ret
266 if matching_kps:
267 # Keypair names are unique, there is either 1 or 0 with requested name
268 kp = matching_kps[0]['keypair']
269 if kp['public_key'] != public_key:
270 _call_nova_salt_module('keypair_delete', name)(
271 name, cloud_name=cloud_name)
272 else:
273 return _no_change(name, 'Keypair')
274 res = _call_nova_salt_module('keypair_create', name)(
275 name, cloud_name=cloud_name, public_key=public_key)
276 return _created(name, 'Keypair', res)
277
278
279@_error_handler
280def keypair_absent(name, cloud_name):
281 """Ensure keypair is absent"""
282 existing_keypairs = _call_nova_salt_module('keypair_list', name)(
283 cloud_name=cloud_name)
284 matching_kps = [kp for kp in existing_keypairs
285 if kp['keypair']['name'] == name]
286 if matching_kps:
287 _call_nova_salt_module('keypair_delete', name)(
288 name, cloud_name=cloud_name)
289 return _deleted(name, 'Keypair')
290 return _non_existent(name, 'Keypair')
291
292
Vladyslav Drokd182bf12019-01-11 14:02:38 +0200293def _urlencode(dictionary):
294 return '&'.join([
295 '='.join((urllib.parse.quote(key), urllib.parse.quote(str(value))))
296 for key, value in dictionary.items()])
297
298
299def _cmd_raising_helper(command_string, test=False, runas='nova'):
300 if test:
301 LOG.info('This is a test run of the following command: "%s"' %
302 command_string)
303 return ''
304 result = __salt__['cmd.run_all'](command_string, python_shell=True,
305 runas=runas)
306 if result.get('retcode', 0) != 0:
307 raise CommandExecutionError(
308 "Command '%(cmd)s' returned error code %(code)s." %
309 {'cmd': command_string, 'code': result.get('retcode')})
310 return result['stdout']
311
312
313def cell_present(
314 name, db_name=None, db_user=None, db_password=None, db_address=None,
315 messaging_hosts=None, messaging_user=None, messaging_password=None,
316 messaging_virtual_host=None, db_engine='mysql',
317 messaging_engine='rabbit', messaging_query_params=None,
318 db_query_params=None, runas='nova', test=False):
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300319 """Ensure nova cell is present
320
321 For newly created cells this state also runs discover_hosts and
Vladyslav Drokd182bf12019-01-11 14:02:38 +0200322 map_instances.
323
324 :param name: name of the cell to be present.
325 :param db_engine: cell database engine.
326 :param db_name: name of the cell database.
327 :param db_user: username for the cell database.
328 :param db_password: password for the cell database.
329 :param db_address: cell database host. If not provided, other db parameters
330 passed to this function are ignored, create/update cell commands will
331 be using connection strings from the config files.
332 :param messaging_engine: cell messaging engine.
333 :param messaging_hosts: a list of dictionaries of messaging hosts of the
334 cell, containing host and optional port keys. port key defaults
335 to 5672. If not provided, other messaging parameters passed to this
336 function are ignored, create/update cell commands will be using
337 connection strings from the config files.
338 :param messaging_user: username for cell messaging hosts.
339 :param messaging_password: password for cell messaging hosts.
340 :param messaging_virtual_host: cell messaging vhost.
341 :param messaging_query_params: dictionary of query params to append to
342 transport URL.
343 :param db_query_params: dictionary of query params to append to database
344 connection URL. charset=utf8 will always be present in query params.
345 :param runas: username to run the shell commands under.
346 :param test: if this is a test run, actual commands changing state won't
347 be run. False by default.
348 """
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300349 cell_info = __salt__['cmd.shell'](
Vladyslav Drokd182bf12019-01-11 14:02:38 +0200350 "nova-manage cell_v2 list_cells --verbose 2>/dev/null | "
351 "awk '/%s/ {print $4,$6,$8}'" % name, runas=runas).split()
352 if messaging_hosts:
353 transport_url = '%s://' % messaging_engine
354 for h in messaging_hosts:
355 if 'host' not in h:
356 ret = _create_failed(name, 'Nova cell')
357 ret['comment'] = (
358 'messaging_hosts parameter of cell_present call should be '
359 'a list of dicts containing host and optionally port keys')
360 return ret
361 transport_url = (
362 '%(transport_url)s%(user)s:%(password)s@%(host)s:%(port)s,' %
363 {'transport_url': transport_url, 'user': messaging_user,
364 'password': messaging_password, 'host': h['host'],
365 'port': h.get('port', 5672)})
366 transport_url = '%(transport_url)s/%(messaging_virtual_host)s' % {
367 'transport_url': transport_url.rstrip(','),
368 'messaging_virtual_host': messaging_virtual_host}
369 if messaging_query_params:
370 transport_url = '%(transport_url)s?%(query)s' % {
371 'transport_url': transport_url,
372 'query': _urlencode(messaging_query_params)}
373 else:
374 transport_url = None
375 if db_address:
376 db_connection = (
377 '%(db_engine)s+pymysql://%(db_user)s:%(db_password)s@'
378 '%(db_address)s/%(db_name)s' % {
379 'db_engine': db_engine, 'db_user': db_user,
380 'db_password': db_password, 'db_address': db_address,
381 'db_name': db_name})
382 if not db_query_params:
383 db_query_params = {}
384 db_query_params['charset'] = 'utf8'
385 db_connection = '%(db_connection)s?%(query)s' % {
386 'db_connection': db_connection,
387 'query': _urlencode(db_query_params)}
388 else:
389 db_connection = None
390 changes = {}
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300391 # There should be at least 1 component printed to cell_info
392 if len(cell_info) >= 1:
393 cell_info = dict(zip_longest(
394 ('cell_uuid', 'existing_transport_url', 'existing_db_connection'),
395 cell_info))
Vladyslav Drokd182bf12019-01-11 14:02:38 +0200396 command_string = (
397 '--transport-url \'%(transport_url)s\' '
398 if transport_url else '' +
399 '--database_connection \'%(db_connection)s\''
400 if db_connection else '')
401 if cell_info['existing_transport_url'] != transport_url:
402 changes['transport_url'] = transport_url
403 if cell_info['existing_db_connection'] != db_connection:
404 changes['db_connection'] = db_connection
405 if not changes:
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300406 return _no_change(name, 'Nova cell')
407 try:
Vladyslav Drokd182bf12019-01-11 14:02:38 +0200408 _cmd_raising_helper(
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300409 ('nova-manage cell_v2 update_cell --cell_uuid %s %s' % (
Vladyslav Drokd182bf12019-01-11 14:02:38 +0200410 cell_info['cell_uuid'], command_string)) % {
411 'transport_url': transport_url,
412 'db_connection': db_connection}, test=test, runas=runas)
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300413 LOG.warning("Updating the transport_url or database_connection "
414 "fields on a running system will NOT result in all "
415 "nodes immediately using the new values. Use caution "
Vladyslav Drokd182bf12019-01-11 14:02:38 +0200416 "when changing these values. You need to restart all "
417 "nova services on all controllers after this action.")
418 ret = _updated(name, 'Nova cell', changes)
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300419 except Exception as e:
420 ret = _update_failed(name, 'Nova cell')
421 ret['comment'] += '\nException: %s' % e
422 return ret
Vladyslav Drokd182bf12019-01-11 14:02:38 +0200423 changes = {'transport_url': transport_url, 'db_connection': db_connection,
424 'name': name}
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300425 try:
Vladyslav Drokd182bf12019-01-11 14:02:38 +0200426 _cmd_raising_helper((
427 'nova-manage cell_v2 create_cell --name %(name)s ' +
428 ('--transport-url \'%(transport_url)s\' ' if transport_url else '')
429 + ('--database_connection \'%(db_connection)s\' '
430 if db_connection else '') + '--verbose ') % changes,
431 test=test, runas=runas)
432 ret = _created(name, 'Nova cell', changes)
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300433 except Exception as e:
434 ret = _create_failed(name, 'Nova cell')
435 ret['comment'] += '\nException: %s' % e
436 return ret
437
438
Vladyslav Drokd182bf12019-01-11 14:02:38 +0200439def cell_absent(name, force=False, runas='nova', test=False):
440 """Ensure cell is absent
441
442 :param name: name of the cell to delete.
443 :param force: force cell deletion even if it contains host mappings
444 (host entries will be removed as well).
445 :param runas: username to run the shell commands under.
446 :param test: if this is a test run, actual commands changing state won't
447 be run. False by default.
448 """
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300449 cell_uuid = __salt__['cmd.shell'](
Vladyslav Drokd182bf12019-01-11 14:02:38 +0200450 "nova-manage cell_v2 list_cells 2>/dev/null | awk '/%s/ {print $4}'" %
451 name, runas=runas)
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300452 if not cell_uuid:
453 return _non_existent(name, 'Nova cell')
454 try:
Vladyslav Drokd182bf12019-01-11 14:02:38 +0200455 # Note that if the cell contains any hosts, you need to pass force
456 # parameter for the deletion to succeed. Cell that has any instance
457 # mappings can not be deleted even with force.
458 _cmd_raising_helper(
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300459 'nova-manage cell_v2 delete_cell --cell_uuid %s %s' % (
Vladyslav Drokd182bf12019-01-11 14:02:38 +0200460 cell_uuid, '--force' if force else ''), test=test, runas=runas)
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300461 ret = _deleted(name, 'Nova cell')
462 except Exception as e:
463 ret = _delete_failed(name, 'Nova cell')
464 ret['comment'] += '\nException: %s' % e
465 return ret
466
467
Vladyslav Drok13c1fef2019-12-10 12:01:19 +0100468def instances_mapped_to_cell(name, max_count=None, timeout=60, runas='nova'):
Vladyslav Drokd182bf12019-01-11 14:02:38 +0200469 """Ensure that all instances in the cell are mapped
470
471 :param name: cell name.
Vladyslav Drok13c1fef2019-12-10 12:01:19 +0100472 :param max_count: how many instances to map in one iteration. If there are
473 lots of instances present in the cell database, consider setting higher
474 value. By default nova will run in batches of 50.
Vladyslav Drokd182bf12019-01-11 14:02:38 +0200475 :param timeout: amount of time in seconds mapping process should finish in.
476 :param runas: username to run the shell commands under.
477 """
Vladyslav Drok8a7631f2019-02-06 15:34:31 +0100478 test = __opts__.get('test', False)
Vladyslav Drokd182bf12019-01-11 14:02:38 +0200479 cell_uuid = __salt__['cmd.shell'](
480 "nova-manage cell_v2 list_cells 2>/dev/null | "
481 "awk '/%s/ {print $4}'" % name, runas=runas)
482 result = {'name': name, 'changes': {}, 'result': False}
483 if not cell_uuid:
484 result['comment'] = (
485 'Failed to map all instances in cell {0}, it does not exist'
486 .format(name))
487 return result
Vladyslav Drok13c1fef2019-12-10 12:01:19 +0100488 command = 'nova-manage cell_v2 map_instances --cell_uuid %s' % cell_uuid
489 if max_count:
490 command += ' --max-count %s' % max_count
Vladyslav Drokd182bf12019-01-11 14:02:38 +0200491 start_time = time.time()
Vladyslav Drok8a7631f2019-02-06 15:34:31 +0100492 if not test:
493 while True:
Vladyslav Drok13c1fef2019-12-10 12:01:19 +0100494 rc = __salt__['cmd.retcode'](command, runas=runas)
Vladyslav Drok8a7631f2019-02-06 15:34:31 +0100495 if rc == 0 or time.time() - start_time > timeout:
496 break
497 if rc != 0:
498 result['comment'] = (
499 'Failed to map all instances in cell {0} in {1} seconds'
500 .format(name, timeout))
501 return result
Vladyslav Drokd182bf12019-01-11 14:02:38 +0200502 result['comment'] = 'All instances mapped in cell {0}'.format(name)
Vladyslav Drok8a7631f2019-02-06 15:34:31 +0100503 if test:
504 result['comment'] = 'TEST: {}'.format(result['comment'])
Vladyslav Drokd182bf12019-01-11 14:02:38 +0200505 result['result'] = True
506 return result
507
508
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300509def _db_version_update(db, version, human_readable_resource_name):
510 existing_version = __salt__['cmd.shell'](
511 'nova-manage %s version 2>/dev/null' % db)
512 try:
513 existing_version = int(existing_version)
514 version = int(version)
515 except Exception as e:
516 ret = _update_failed(existing_version,
517 human_readable_resource_name)
518 ret['comment'] += ('\nCan not convert existing or requested version '
519 'to integer, exception: %s' % e)
520 LOG.error(ret['comment'])
521 return ret
522 if existing_version < version:
523 try:
524 __salt__['cmd.shell'](
525 'nova-manage %s sync --version %s' % (db, version))
526 ret = _updated(existing_version, human_readable_resource_name,
527 {db: '%s sync --version %s' % (db, version)})
528 except Exception as e:
529 ret = _update_failed(existing_version,
530 human_readable_resource_name)
531 ret['comment'] += '\nException: %s' % e
532 return ret
533 return _no_change(existing_version, human_readable_resource_name)
534
535
536def api_db_version_present(name=None, version="20"):
537 """Ensures that specific api_db version is present"""
538 return _db_version_update('api_db', version, 'Nova API database version')
539
540
541def db_version_present(name=None, version="334"):
542 """Ensures that specific db version is present"""
543 return _db_version_update('db', version, 'Nova database version')
544
545
546def online_data_migrations_present(name=None, api_db_version="20",
547 db_version="334"):
548 """Runs online_data_migrations if databases are of specific versions"""
549 ret = {'name': 'online_data_migrations', 'changes': {}, 'result': False,
550 'comment': 'Current nova api_db version != {0} or nova db version '
551 '!= {1}.'.format(api_db_version, db_version)}
552 cur_api_db_version = __salt__['cmd.shell'](
553 'nova-manage api_db version 2>/dev/null')
554 cur_db_version = __salt__['cmd.shell'](
555 'nova-manage db version 2>/dev/null')
556 try:
557 cur_api_db_version = int(cur_api_db_version)
558 cur_db_version = int(cur_db_version)
559 api_db_version = int(api_db_version)
560 db_version = int(db_version)
561 except Exception as e:
562 LOG.error(ret['comment'])
563 ret['comment'] = ('\nCan not convert existing or requested database '
564 'versions to integer, exception: %s' % e)
565 return ret
566 if cur_api_db_version == api_db_version and cur_db_version == db_version:
567 try:
568 __salt__['cmd.shell']('nova-manage db online_data_migrations')
569 ret['result'] = True
570 ret['comment'] = ('nova-manage db online_data_migrations was '
571 'executed successfuly')
572 ret['changes']['online_data_migrations'] = (
573 'online_data_migrations run on nova api_db version {0} and '
574 'nova db version {1}'.format(api_db_version, db_version))
575 except Exception as e:
576 ret['comment'] = (
577 'Failed to execute online_data_migrations on nova api_db '
578 'version %s and nova db version %s, exception: %s' % (
579 api_db_version, db_version, e))
580 return ret
581
582
Oleh Hryhorov5cfb9d32018-09-11 16:55:24 +0000583@_error_handler
584def service_enabled(name, cloud_name, binary="nova-compute"):
585 """Ensures that the service is enabled on the host
586
587 :param name: name of a host where service is running
588 :param service: name of the service have to be run
589 """
590 changes = {}
591
592 services = _call_nova_salt_module('services_list', name)(
Oleksandr Shyshkoc74c4772018-11-29 15:17:34 +0000593 name, binary=binary, cloud_name=cloud_name)
Oleh Hryhorov5cfb9d32018-09-11 16:55:24 +0000594 enabled_service = [s for s in services if s['binary'] == binary
595 and s['status'] == 'enabled' and s['host'] == name]
596 if len(enabled_service) > 0:
597 ret = _no_change(name, 'Compute services')
598 else:
599 changes = _call_nova_salt_module('services_update', name)(
600 name, binary, 'enable', cloud_name=cloud_name)
601 ret = _updated(name, 'Compute services', changes)
602
603 return ret
604
605@_error_handler
606def service_disabled(name, cloud_name, binary="nova-compute", disabled_reason=None):
607 """Ensures that the service is disabled on the host
608
609 :param name: name of a host where service is running
610 :param service: name of the service have to be disabled
611 """
612
613 changes = {}
614 kwargs = {}
615
616 if disabled_reason is not None:
617 kwargs['disabled_reason'] = disabled_reason
618
619 services = _call_nova_salt_module('services_list', name)(
Oleksandr Shyshkoc74c4772018-11-29 15:17:34 +0000620 name, binary=binary, cloud_name=cloud_name)
Oleh Hryhorov5cfb9d32018-09-11 16:55:24 +0000621 disabled_service = [s for s in services if s['binary'] == binary
622 and s['status'] == 'disabled' and s['host'] == name]
623 if len(disabled_service) > 0:
624 ret = _no_change(name, 'Compute services')
625 else:
626 changes = _call_nova_salt_module('services_update', name)(
627 name, binary, 'disable', cloud_name=cloud_name, **kwargs)
628 ret = _updated(name, 'Compute services', changes)
629
630 return ret
631
632
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300633def _find_failed(name, resource):
634 return {
635 'name': name, 'changes': {}, 'result': False,
636 'comment': 'Failed to find {0}s with name {1}'.format(resource, name)}
637
638
639def _created(name, resource, changes):
640 return {
641 'name': name, 'changes': changes, 'result': True,
642 'comment': '{0} {1} created'.format(resource, name)}
643
644
645def _create_failed(name, resource):
646 return {
647 'name': name, 'changes': {}, 'result': False,
648 'comment': '{0} {1} creation failed'.format(resource, name)}
649
650
651def _no_change(name, resource):
652 return {
653 'name': name, 'changes': {}, 'result': True,
654 'comment': '{0} {1} already is in the desired state'.format(
655 resource, name)}
656
657
658def _updated(name, resource, changes):
659 return {
660 'name': name, 'changes': changes, 'result': True,
661 'comment': '{0} {1} was updated'.format(resource, name)}
662
663
664def _update_failed(name, resource):
665 return {
666 'name': name, 'changes': {}, 'result': False,
667 'comment': '{0} {1} update failed'.format(resource, name)}
668
669
670def _deleted(name, resource):
671 return {
672 'name': name, 'changes': {}, 'result': True,
673 'comment': '{0} {1} deleted'.format(resource, name)}
674
675
676def _delete_failed(name, resource):
677 return {
678 'name': name, 'changes': {}, 'result': False,
679 'comment': '{0} {1} deletion failed'.format(resource, name)}
680
681
682def _non_existent(name, resource):
683 return {
684 'name': name, 'changes': {}, 'result': True,
685 'comment': '{0} {1} does not exist'.format(resource, name)}