blob: 138d2951a058ec6db3fb7ece10cbd0cf29593753 [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
14import six
15from six.moves import zip_longest
Oleh Hryhorov5cfb9d32018-09-11 16:55:24 +000016import time
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +030017import salt
Oleh Hryhorov5cfb9d32018-09-11 16:55:24 +000018from salt.exceptions import CommandExecutionError
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +030019
20LOG = logging.getLogger(__name__)
21
22KEYSTONE_LOADED = False
23
24
25def __virtual__():
26 """Only load if the nova module is in __salt__"""
27 if 'keystonev3.project_get_details' in __salt__:
28 global KEYSTONE_LOADED
29 KEYSTONE_LOADED = True
30 return 'novav21'
31
32
33class SaltModuleCallException(Exception):
34
35 def __init__(self, result_dict, *args, **kwargs):
36 super(SaltModuleCallException, self).__init__(*args, **kwargs)
37 self.result_dict = result_dict
38
39
40def _get_failure_function_mapping():
41 return {
42 'create': _create_failed,
43 'update': _update_failed,
44 'find': _find_failed,
45 'delete': _delete_failed,
46 }
47
48
49def _call_nova_salt_module(call_string, name, module_name='novav21'):
50 def inner(*args, **kwargs):
51 func = __salt__['%s.%s' % (module_name, call_string)]
52 result = func(*args, **kwargs)
53 if not result['result']:
54 ret = _get_failure_function_mapping()[func._action_type](
55 name, func._resource_human_readable_name)
56 ret['comment'] += '\nStatus code: %s\n%s' % (result['status_code'],
57 result['comment'])
58 raise SaltModuleCallException(ret)
59 return result['body'].get(func._body_response_key)
60 return inner
61
62
63def _error_handler(fun):
64 @six.wraps(fun)
65 def inner(*args, **kwargs):
66 try:
67 return fun(*args, **kwargs)
68 except SaltModuleCallException as e:
69 return e.result_dict
70 return inner
71
72
73@_error_handler
74def flavor_present(name, cloud_name, vcpus=1, ram=256, disk=0, flavor_id=None,
75 extra_specs=None):
76 """Ensures that the flavor exists"""
77 extra_specs = extra_specs or {}
78 # There is no way to query flavors by name
79 flavors = _call_nova_salt_module('flavor_list', name)(
80 detail=True, cloud_name=cloud_name)
81 flavor = [flavor for flavor in flavors if flavor['name'] == name]
82 # Flavor names are unique, there is either 1 or 0 with requested name
83 if flavor:
84 flavor = flavor[0]
85 current_extra_specs = _call_nova_salt_module(
86 'flavor_get_extra_specs', name)(
87 flavor['id'], cloud_name=cloud_name)
88 to_delete = set(current_extra_specs) - set(extra_specs)
89 to_add = set(extra_specs) - set(current_extra_specs)
90 for spec in to_delete:
91 _call_nova_salt_module('flavor_delete_extra_spec', name)(
92 flavor['id'], spec, cloud_name=cloud_name)
93 _call_nova_salt_module('flavor_add_extra_specs', name)(
94 flavor['id'], cloud_name=cloud_name, **extra_specs)
95 if to_delete or to_add:
96 ret = _updated(name, 'Flavor', extra_specs)
97 else:
98 ret = _no_change(name, 'Flavor')
99 else:
100 flavor = _call_nova_salt_module('flavor_create', name)(
101 name, vcpus, ram, disk, id=flavor_id, cloud_name=cloud_name)
102 _call_nova_salt_module('flavor_add_extra_specs', name)(
103 flavor['id'], cloud_name=cloud_name, **extra_specs)
104 flavor['extra_specs'] = extra_specs
105 ret = _created(name, 'Flavor', flavor)
106 return ret
107
108
109@_error_handler
110def flavor_absent(name, cloud_name):
111 """Ensure flavor is absent"""
112 # There is no way to query flavors by name
113 flavors = _call_nova_salt_module('flavor_list', name)(
114 detail=True, cloud_name=cloud_name)
115 flavor = [flavor for flavor in flavors if flavor['name'] == name]
116 # Flavor names are unique, there is either 1 or 0 with requested name
117 if flavor:
118 _call_nova_salt_module('flavor_delete', name)(
119 flavor[0]['id'], cloud_name=cloud_name)
120 return _deleted(name, 'Flavor')
121 return _non_existent(name, 'Flavor')
122
123
124def _get_keystone_project_id_by_name(project_name, cloud_name):
125 if not KEYSTONE_LOADED:
126 LOG.error("Keystone module not found, can not look up project ID "
127 "by name")
128 return None
129 project = __salt__['keystonev3.project_get_details'](
130 project_name, cloud_name=cloud_name)
131 if not project:
132 return None
133 return project['project']['id']
134
135
136@_error_handler
137def quota_present(name, cloud_name, **kwargs):
138 """Ensures that the nova quota exists
139
140 :param name: project name to ensure quota for.
141 """
142 project_name = name
143 project_id = _get_keystone_project_id_by_name(project_name, cloud_name)
144 changes = {}
145 if not project_id:
146 ret = _update_failed(project_name, 'Project quota')
147 ret['comment'] += ('\nCould not retrieve keystone project %s' %
148 project_name)
149 return ret
150 quota = _call_nova_salt_module('quota_list', project_name)(
151 project_id, cloud_name=cloud_name)
152 for key, value in kwargs.items():
153 if quota.get(key) != value:
154 changes[key] = value
155 if changes:
156 _call_nova_salt_module('quota_update', project_name)(
157 project_id, cloud_name=cloud_name, **changes)
158 return _updated(project_name, 'Project quota', changes)
159 else:
160 return _no_change(project_name, 'Project quota')
161
162
163@_error_handler
164def quota_absent(name, cloud_name):
165 """Ensures that the nova quota set to default
166
167 :param name: project name to reset quota for.
168 """
169 project_name = name
170 project_id = _get_keystone_project_id_by_name(project_name, cloud_name)
171 if not project_id:
172 ret = _delete_failed(project_name, 'Project quota')
173 ret['comment'] += ('\nCould not retrieve keystone project %s' %
174 project_name)
175 return ret
176 _call_nova_salt_module('quota_delete', name)(
177 project_id, cloud_name=cloud_name)
178 return _deleted(name, 'Project quota')
179
180
181@_error_handler
182def aggregate_present(name, cloud_name, availability_zone_name=None,
183 hosts=None, metadata=None):
184 """Ensures that the nova aggregate exists"""
185 aggregates = _call_nova_salt_module('aggregate_list', name)(
186 cloud_name=cloud_name)
187 aggregate_exists = [agg for agg in aggregates
188 if agg['name'] == name]
189 metadata = metadata or {}
190 hosts = hosts or []
191 if availability_zone_name:
192 metadata.update(availability_zone=availability_zone_name)
193 if not aggregate_exists:
194 aggregate = _call_nova_salt_module('aggregate_create', name)(
195 name, availability_zone_name, cloud_name=cloud_name)
196 if metadata:
197 _call_nova_salt_module('aggregate_set_metadata', name)(
Dzmitry Stremkouski6d57e8c2020-04-15 18:40:47 +0200198 name, cloud_name=cloud_name, **metadata)
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300199 aggregate['metadata'] = metadata
200 for host in hosts or []:
201 _call_nova_salt_module('aggregate_add_host', name)(
202 name, host, cloud_name=cloud_name)
203 aggregate['hosts'] = hosts
204 return _created(name, 'Host aggregate', aggregate)
205 else:
206 aggregate = aggregate_exists[0]
207 changes = {}
208 existing_meta = set(aggregate['metadata'].items())
209 requested_meta = set(metadata.items())
Dzmitry Stremkouski6d57e8c2020-04-15 18:40:47 +0200210 if existing_meta - requested_meta or requested_meta - existing_meta:
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300211 _call_nova_salt_module('aggregate_set_metadata', name)(
212 name, cloud_name=cloud_name, **metadata)
213 changes['metadata'] = metadata
214 hosts_to_add = set(hosts) - set(aggregate['hosts'])
215 hosts_to_remove = set(aggregate['hosts']) - set(hosts)
Dzmitry Stremkouski7f5c9242020-02-16 17:02:44 +0100216 if hosts and (hosts_to_remove or hosts_to_add):
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300217 for host in hosts_to_add:
218 _call_nova_salt_module('aggregate_add_host', name)(
219 name, host, cloud_name=cloud_name)
220 for host in hosts_to_remove:
221 _call_nova_salt_module('aggregate_remove_host', name)(
222 name, host, cloud_name=cloud_name)
223 changes['hosts'] = hosts
224 if changes:
225 return _updated(name, 'Host aggregate', changes)
226 else:
227 return _no_change(name, 'Host aggregate')
228
229
230@_error_handler
231def aggregate_absent(name, cloud_name):
232 """Ensure aggregate is absent"""
233 existing_aggregates = _call_nova_salt_module('aggregate_list', name)(
234 cloud_name=cloud_name)
235 matching_aggs = [agg for agg in existing_aggregates
236 if agg['name'] == name]
237 if matching_aggs:
238 _call_nova_salt_module('aggregate_delete', name)(
239 name, cloud_name=cloud_name)
240 return _deleted(name, 'Host Aggregate')
241 return _non_existent(name, 'Host Aggregate')
242
243
244@_error_handler
245def keypair_present(name, cloud_name, public_key_file=None, public_key=None):
246 """Ensures that the Nova key-pair exists"""
247 existing_keypairs = _call_nova_salt_module('keypair_list', name)(
248 cloud_name=cloud_name)
249 matching_kps = [kp for kp in existing_keypairs
250 if kp['keypair']['name'] == name]
251 if public_key_file and not public_key:
252 with salt.utils.fopen(public_key_file, 'r') as f:
253 public_key = f.read()
254 if not public_key:
255 ret = _create_failed(name, 'Keypair')
256 ret['comment'] += '\nPlease specify public key for keypair creation.'
257 return ret
258 if matching_kps:
259 # Keypair names are unique, there is either 1 or 0 with requested name
260 kp = matching_kps[0]['keypair']
261 if kp['public_key'] != public_key:
262 _call_nova_salt_module('keypair_delete', name)(
263 name, cloud_name=cloud_name)
264 else:
265 return _no_change(name, 'Keypair')
266 res = _call_nova_salt_module('keypair_create', name)(
267 name, cloud_name=cloud_name, public_key=public_key)
268 return _created(name, 'Keypair', res)
269
270
271@_error_handler
272def keypair_absent(name, cloud_name):
273 """Ensure keypair is absent"""
274 existing_keypairs = _call_nova_salt_module('keypair_list', name)(
275 cloud_name=cloud_name)
276 matching_kps = [kp for kp in existing_keypairs
277 if kp['keypair']['name'] == name]
278 if matching_kps:
279 _call_nova_salt_module('keypair_delete', name)(
280 name, cloud_name=cloud_name)
281 return _deleted(name, 'Keypair')
282 return _non_existent(name, 'Keypair')
283
284
285def cell_present(name='cell1', transport_url='none:///', db_engine='mysql',
286 db_name='nova_upgrade', db_user='nova', db_password=None,
287 db_address='0.0.0.0'):
288 """Ensure nova cell is present
289
290 For newly created cells this state also runs discover_hosts and
291 map_instances."""
292 cell_info = __salt__['cmd.shell'](
293 "nova-manage cell_v2 list_cells --verbose | "
294 "awk '/%s/ {print $4,$6,$8}'" % name).split()
295 db_connection = (
296 '%(db_engine)s+pymysql://%(db_user)s:%(db_password)s@'
297 '%(db_address)s/%(db_name)s?charset=utf8' % {
298 'db_engine': db_engine, 'db_user': db_user,
299 'db_password': db_password, 'db_address': db_address,
300 'db_name': db_name})
301 args = {'transport_url': transport_url, 'db_connection': db_connection}
302 # There should be at least 1 component printed to cell_info
303 if len(cell_info) >= 1:
304 cell_info = dict(zip_longest(
305 ('cell_uuid', 'existing_transport_url', 'existing_db_connection'),
306 cell_info))
307 cell_uuid, existing_transport_url, existing_db_connection = cell_info
308 command_string = ''
309 if existing_transport_url != transport_url:
310 command_string = (
311 '%s --transport-url %%(transport_url)s' % command_string)
312 if existing_db_connection != db_connection:
313 command_string = (
314 '%s --database_connection %%(db_connection)s' % command_string)
315 if not command_string:
316 return _no_change(name, 'Nova cell')
317 try:
318 __salt__['cmd.shell'](
319 ('nova-manage cell_v2 update_cell --cell_uuid %s %s' % (
320 cell_uuid, command_string)) % args)
321 LOG.warning("Updating the transport_url or database_connection "
322 "fields on a running system will NOT result in all "
323 "nodes immediately using the new values. Use caution "
324 "when changing these values.")
325 ret = _updated(name, 'Nova cell', args)
326 except Exception as e:
327 ret = _update_failed(name, 'Nova cell')
328 ret['comment'] += '\nException: %s' % e
329 return ret
330 args.update(name=name)
331 try:
332 cell_uuid = __salt__['cmd.shell'](
333 'nova-manage cell_v2 create_cell --name %(name)s '
334 '--transport-url %(transport_url)s '
335 '--database_connection %(db_connection)s --verbose' % args)
336 __salt__['cmd.shell']('nova-manage cell_v2 discover_hosts '
337 '--cell_uuid %s --verbose' % cell_uuid)
338 __salt__['cmd.shell']('nova-manage cell_v2 map_instances '
339 '--cell_uuid %s' % cell_uuid)
340 ret = _created(name, 'Nova cell', args)
341 except Exception as e:
342 ret = _create_failed(name, 'Nova cell')
343 ret['comment'] += '\nException: %s' % e
344 return ret
345
346
347def cell_absent(name, force=False):
348 """Ensure cell is absent"""
349 cell_uuid = __salt__['cmd.shell'](
350 "nova-manage cell_v2 list_cells | awk '/%s/ {print $4}'" % name)
351 if not cell_uuid:
352 return _non_existent(name, 'Nova cell')
353 try:
354 __salt__['cmd.shell'](
355 'nova-manage cell_v2 delete_cell --cell_uuid %s %s' % (
356 cell_uuid, '--force' if force else ''))
357 ret = _deleted(name, 'Nova cell')
358 except Exception as e:
359 ret = _delete_failed(name, 'Nova cell')
360 ret['comment'] += '\nException: %s' % e
361 return ret
362
363
Vladyslav Drok8c124c32019-12-10 12:01:19 +0100364def instances_mapped_to_cell(name, max_count=None, timeout=60, runas='nova'):
Vladyslav Drok9adb24d2019-02-06 15:34:31 +0100365 """Ensure that all instances in the cell are mapped
366
367 :param name: cell name.
Vladyslav Drok8c124c32019-12-10 12:01:19 +0100368 :param max_count: how many instances to map in one iteration. If there are
369 lots of instances present in the cell database, consider setting higher
370 value. By default nova will run in batches of 50.
Vladyslav Drok9adb24d2019-02-06 15:34:31 +0100371 :param timeout: amount of time in seconds mapping process should finish in.
372 :param runas: username to run the shell commands under.
373 """
374 test = __opts__.get('test', False)
375 cell_uuid = __salt__['cmd.shell'](
376 "nova-manage cell_v2 list_cells 2>/dev/null | "
377 "awk '/%s/ {print $4}'" % name, runas=runas)
378 result = {'name': name, 'changes': {}, 'result': False}
379 if not cell_uuid:
380 result['comment'] = (
381 'Failed to map all instances in cell {0}, it does not exist'
382 .format(name))
383 return result
Vladyslav Drok8c124c32019-12-10 12:01:19 +0100384 command = 'nova-manage cell_v2 map_instances --cell_uuid %s' % cell_uuid
385 if max_count:
386 command += ' --max-count %s' % max_count
Vladyslav Drok9adb24d2019-02-06 15:34:31 +0100387 start_time = time.time()
388 if not test:
389 while True:
Vladyslav Drok8c124c32019-12-10 12:01:19 +0100390 rc = __salt__['cmd.retcode'](command, runas=runas)
Vladyslav Drok9adb24d2019-02-06 15:34:31 +0100391 if rc == 0 or time.time() - start_time > timeout:
392 break
393 if rc != 0:
394 result['comment'] = (
395 'Failed to map all instances in cell {0} in {1} seconds'
396 .format(name, timeout))
397 return result
398 result['comment'] = 'All instances mapped in cell {0}'.format(name)
399 if test:
400 result['comment'] = 'TEST: {}'.format(result['comment'])
401 result['result'] = True
402 return result
403
404
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300405def _db_version_update(db, version, human_readable_resource_name):
406 existing_version = __salt__['cmd.shell'](
407 'nova-manage %s version 2>/dev/null' % db)
408 try:
409 existing_version = int(existing_version)
410 version = int(version)
411 except Exception as e:
412 ret = _update_failed(existing_version,
413 human_readable_resource_name)
414 ret['comment'] += ('\nCan not convert existing or requested version '
415 'to integer, exception: %s' % e)
416 LOG.error(ret['comment'])
417 return ret
418 if existing_version < version:
419 try:
420 __salt__['cmd.shell'](
421 'nova-manage %s sync --version %s' % (db, version))
422 ret = _updated(existing_version, human_readable_resource_name,
423 {db: '%s sync --version %s' % (db, version)})
424 except Exception as e:
425 ret = _update_failed(existing_version,
426 human_readable_resource_name)
427 ret['comment'] += '\nException: %s' % e
428 return ret
429 return _no_change(existing_version, human_readable_resource_name)
430
431
432def api_db_version_present(name=None, version="20"):
433 """Ensures that specific api_db version is present"""
434 return _db_version_update('api_db', version, 'Nova API database version')
435
436
437def db_version_present(name=None, version="334"):
438 """Ensures that specific db version is present"""
439 return _db_version_update('db', version, 'Nova database version')
440
441
442def online_data_migrations_present(name=None, api_db_version="20",
443 db_version="334"):
444 """Runs online_data_migrations if databases are of specific versions"""
445 ret = {'name': 'online_data_migrations', 'changes': {}, 'result': False,
446 'comment': 'Current nova api_db version != {0} or nova db version '
447 '!= {1}.'.format(api_db_version, db_version)}
448 cur_api_db_version = __salt__['cmd.shell'](
449 'nova-manage api_db version 2>/dev/null')
450 cur_db_version = __salt__['cmd.shell'](
451 'nova-manage db version 2>/dev/null')
452 try:
453 cur_api_db_version = int(cur_api_db_version)
454 cur_db_version = int(cur_db_version)
455 api_db_version = int(api_db_version)
456 db_version = int(db_version)
457 except Exception as e:
458 LOG.error(ret['comment'])
459 ret['comment'] = ('\nCan not convert existing or requested database '
460 'versions to integer, exception: %s' % e)
461 return ret
462 if cur_api_db_version == api_db_version and cur_db_version == db_version:
463 try:
464 __salt__['cmd.shell']('nova-manage db online_data_migrations')
465 ret['result'] = True
466 ret['comment'] = ('nova-manage db online_data_migrations was '
467 'executed successfuly')
468 ret['changes']['online_data_migrations'] = (
469 'online_data_migrations run on nova api_db version {0} and '
470 'nova db version {1}'.format(api_db_version, db_version))
471 except Exception as e:
472 ret['comment'] = (
473 'Failed to execute online_data_migrations on nova api_db '
474 'version %s and nova db version %s, exception: %s' % (
475 api_db_version, db_version, e))
476 return ret
477
478
Oleh Hryhorov5cfb9d32018-09-11 16:55:24 +0000479@_error_handler
480def service_enabled(name, cloud_name, binary="nova-compute"):
481 """Ensures that the service is enabled on the host
482
483 :param name: name of a host where service is running
484 :param service: name of the service have to be run
485 """
486 changes = {}
487
488 services = _call_nova_salt_module('services_list', name)(
Oleksandr Shyshkoc74c4772018-11-29 15:17:34 +0000489 name, binary=binary, cloud_name=cloud_name)
Oleh Hryhorov5cfb9d32018-09-11 16:55:24 +0000490 enabled_service = [s for s in services if s['binary'] == binary
491 and s['status'] == 'enabled' and s['host'] == name]
492 if len(enabled_service) > 0:
493 ret = _no_change(name, 'Compute services')
494 else:
495 changes = _call_nova_salt_module('services_update', name)(
496 name, binary, 'enable', cloud_name=cloud_name)
497 ret = _updated(name, 'Compute services', changes)
498
499 return ret
500
501@_error_handler
502def service_disabled(name, cloud_name, binary="nova-compute", disabled_reason=None):
503 """Ensures that the service is disabled on the host
504
505 :param name: name of a host where service is running
506 :param service: name of the service have to be disabled
507 """
508
509 changes = {}
510 kwargs = {}
511
512 if disabled_reason is not None:
513 kwargs['disabled_reason'] = disabled_reason
514
515 services = _call_nova_salt_module('services_list', name)(
Oleksandr Shyshkoc74c4772018-11-29 15:17:34 +0000516 name, binary=binary, cloud_name=cloud_name)
Oleh Hryhorov5cfb9d32018-09-11 16:55:24 +0000517 disabled_service = [s for s in services if s['binary'] == binary
518 and s['status'] == 'disabled' and s['host'] == name]
519 if len(disabled_service) > 0:
520 ret = _no_change(name, 'Compute services')
521 else:
522 changes = _call_nova_salt_module('services_update', name)(
523 name, binary, 'disable', cloud_name=cloud_name, **kwargs)
524 ret = _updated(name, 'Compute services', changes)
525
526 return ret
527
528
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300529def _find_failed(name, resource):
530 return {
531 'name': name, 'changes': {}, 'result': False,
532 'comment': 'Failed to find {0}s with name {1}'.format(resource, name)}
533
534
535def _created(name, resource, changes):
536 return {
537 'name': name, 'changes': changes, 'result': True,
538 'comment': '{0} {1} created'.format(resource, name)}
539
540
541def _create_failed(name, resource):
542 return {
543 'name': name, 'changes': {}, 'result': False,
544 'comment': '{0} {1} creation failed'.format(resource, name)}
545
546
547def _no_change(name, resource):
548 return {
549 'name': name, 'changes': {}, 'result': True,
550 'comment': '{0} {1} already is in the desired state'.format(
551 resource, name)}
552
553
554def _updated(name, resource, changes):
555 return {
556 'name': name, 'changes': changes, 'result': True,
557 'comment': '{0} {1} was updated'.format(resource, name)}
558
559
560def _update_failed(name, resource):
561 return {
562 'name': name, 'changes': {}, 'result': False,
563 'comment': '{0} {1} update failed'.format(resource, name)}
564
565
566def _deleted(name, resource):
567 return {
568 'name': name, 'changes': {}, 'result': True,
569 'comment': '{0} {1} deleted'.format(resource, name)}
570
571
572def _delete_failed(name, resource):
573 return {
574 'name': name, 'changes': {}, 'result': False,
575 'comment': '{0} {1} deletion failed'.format(resource, name)}
576
577
578def _non_existent(name, resource):
579 return {
580 'name': name, 'changes': {}, 'result': True,
581 'comment': '{0} {1} does not exist'.format(resource, name)}