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