blob: 1feefeed42445ed73722c3c9cc2d9f44258aebc6 [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
16
17import salt
18
19LOG = logging.getLogger(__name__)
20
21KEYSTONE_LOADED = False
22
23
24def __virtual__():
25 """Only load if the nova module is in __salt__"""
26 if 'keystonev3.project_get_details' in __salt__:
27 global KEYSTONE_LOADED
28 KEYSTONE_LOADED = True
29 return 'novav21'
30
31
32class SaltModuleCallException(Exception):
33
34 def __init__(self, result_dict, *args, **kwargs):
35 super(SaltModuleCallException, self).__init__(*args, **kwargs)
36 self.result_dict = result_dict
37
38
39def _get_failure_function_mapping():
40 return {
41 'create': _create_failed,
42 'update': _update_failed,
43 'find': _find_failed,
44 'delete': _delete_failed,
45 }
46
47
48def _call_nova_salt_module(call_string, name, module_name='novav21'):
49 def inner(*args, **kwargs):
50 func = __salt__['%s.%s' % (module_name, call_string)]
51 result = func(*args, **kwargs)
52 if not result['result']:
53 ret = _get_failure_function_mapping()[func._action_type](
54 name, func._resource_human_readable_name)
55 ret['comment'] += '\nStatus code: %s\n%s' % (result['status_code'],
56 result['comment'])
57 raise SaltModuleCallException(ret)
58 return result['body'].get(func._body_response_key)
59 return inner
60
61
62def _error_handler(fun):
63 @six.wraps(fun)
64 def inner(*args, **kwargs):
65 try:
66 return fun(*args, **kwargs)
67 except SaltModuleCallException as e:
68 return e.result_dict
69 return inner
70
71
72@_error_handler
73def flavor_present(name, cloud_name, vcpus=1, ram=256, disk=0, flavor_id=None,
74 extra_specs=None):
75 """Ensures that the flavor exists"""
76 extra_specs = extra_specs or {}
77 # There is no way to query flavors by name
78 flavors = _call_nova_salt_module('flavor_list', name)(
79 detail=True, cloud_name=cloud_name)
80 flavor = [flavor for flavor in flavors if flavor['name'] == name]
81 # Flavor names are unique, there is either 1 or 0 with requested name
82 if flavor:
83 flavor = flavor[0]
84 current_extra_specs = _call_nova_salt_module(
85 'flavor_get_extra_specs', name)(
86 flavor['id'], cloud_name=cloud_name)
87 to_delete = set(current_extra_specs) - set(extra_specs)
88 to_add = set(extra_specs) - set(current_extra_specs)
89 for spec in to_delete:
90 _call_nova_salt_module('flavor_delete_extra_spec', name)(
91 flavor['id'], spec, cloud_name=cloud_name)
92 _call_nova_salt_module('flavor_add_extra_specs', name)(
93 flavor['id'], cloud_name=cloud_name, **extra_specs)
94 if to_delete or to_add:
95 ret = _updated(name, 'Flavor', extra_specs)
96 else:
97 ret = _no_change(name, 'Flavor')
98 else:
99 flavor = _call_nova_salt_module('flavor_create', name)(
100 name, vcpus, ram, disk, id=flavor_id, cloud_name=cloud_name)
101 _call_nova_salt_module('flavor_add_extra_specs', name)(
102 flavor['id'], cloud_name=cloud_name, **extra_specs)
103 flavor['extra_specs'] = extra_specs
104 ret = _created(name, 'Flavor', flavor)
105 return ret
106
107
108@_error_handler
109def flavor_absent(name, cloud_name):
110 """Ensure flavor is absent"""
111 # There is no way to query flavors by name
112 flavors = _call_nova_salt_module('flavor_list', name)(
113 detail=True, cloud_name=cloud_name)
114 flavor = [flavor for flavor in flavors if flavor['name'] == name]
115 # Flavor names are unique, there is either 1 or 0 with requested name
116 if flavor:
117 _call_nova_salt_module('flavor_delete', name)(
118 flavor[0]['id'], cloud_name=cloud_name)
119 return _deleted(name, 'Flavor')
120 return _non_existent(name, 'Flavor')
121
122
123def _get_keystone_project_id_by_name(project_name, cloud_name):
124 if not KEYSTONE_LOADED:
125 LOG.error("Keystone module not found, can not look up project ID "
126 "by name")
127 return None
128 project = __salt__['keystonev3.project_get_details'](
129 project_name, cloud_name=cloud_name)
130 if not project:
131 return None
132 return project['project']['id']
133
134
135@_error_handler
136def quota_present(name, cloud_name, **kwargs):
137 """Ensures that the nova quota exists
138
139 :param name: project name to ensure quota for.
140 """
141 project_name = name
142 project_id = _get_keystone_project_id_by_name(project_name, cloud_name)
143 changes = {}
144 if not project_id:
145 ret = _update_failed(project_name, 'Project quota')
146 ret['comment'] += ('\nCould not retrieve keystone project %s' %
147 project_name)
148 return ret
149 quota = _call_nova_salt_module('quota_list', project_name)(
150 project_id, cloud_name=cloud_name)
151 for key, value in kwargs.items():
152 if quota.get(key) != value:
153 changes[key] = value
154 if changes:
155 _call_nova_salt_module('quota_update', project_name)(
156 project_id, cloud_name=cloud_name, **changes)
157 return _updated(project_name, 'Project quota', changes)
158 else:
159 return _no_change(project_name, 'Project quota')
160
161
162@_error_handler
163def quota_absent(name, cloud_name):
164 """Ensures that the nova quota set to default
165
166 :param name: project name to reset quota for.
167 """
168 project_name = name
169 project_id = _get_keystone_project_id_by_name(project_name, cloud_name)
170 if not project_id:
171 ret = _delete_failed(project_name, 'Project quota')
172 ret['comment'] += ('\nCould not retrieve keystone project %s' %
173 project_name)
174 return ret
175 _call_nova_salt_module('quota_delete', name)(
176 project_id, cloud_name=cloud_name)
177 return _deleted(name, 'Project quota')
178
179
180@_error_handler
181def aggregate_present(name, cloud_name, availability_zone_name=None,
182 hosts=None, metadata=None):
183 """Ensures that the nova aggregate exists"""
184 aggregates = _call_nova_salt_module('aggregate_list', name)(
185 cloud_name=cloud_name)
186 aggregate_exists = [agg for agg in aggregates
187 if agg['name'] == name]
188 metadata = metadata or {}
189 hosts = hosts or []
190 if availability_zone_name:
191 metadata.update(availability_zone=availability_zone_name)
192 if not aggregate_exists:
193 aggregate = _call_nova_salt_module('aggregate_create', name)(
194 name, availability_zone_name, cloud_name=cloud_name)
195 if metadata:
196 _call_nova_salt_module('aggregate_set_metadata', name)(
197 cloud_name=cloud_name, **metadata)
198 aggregate['metadata'] = metadata
199 for host in hosts or []:
200 _call_nova_salt_module('aggregate_add_host', name)(
201 name, host, cloud_name=cloud_name)
202 aggregate['hosts'] = hosts
203 return _created(name, 'Host aggregate', aggregate)
204 else:
205 aggregate = aggregate_exists[0]
206 changes = {}
207 existing_meta = set(aggregate['metadata'].items())
208 requested_meta = set(metadata.items())
209 if existing_meta - requested_meta or requested_meta - existing_meta:
210 _call_nova_salt_module('aggregate_set_metadata', name)(
211 name, cloud_name=cloud_name, **metadata)
212 changes['metadata'] = metadata
213 hosts_to_add = set(hosts) - set(aggregate['hosts'])
214 hosts_to_remove = set(aggregate['hosts']) - set(hosts)
215 if hosts_to_remove or hosts_to_add:
216 for host in hosts_to_add:
217 _call_nova_salt_module('aggregate_add_host', name)(
218 name, host, cloud_name=cloud_name)
219 for host in hosts_to_remove:
220 _call_nova_salt_module('aggregate_remove_host', name)(
221 name, host, cloud_name=cloud_name)
222 changes['hosts'] = hosts
223 if changes:
224 return _updated(name, 'Host aggregate', changes)
225 else:
226 return _no_change(name, 'Host aggregate')
227
228
229@_error_handler
230def aggregate_absent(name, cloud_name):
231 """Ensure aggregate is absent"""
232 existing_aggregates = _call_nova_salt_module('aggregate_list', name)(
233 cloud_name=cloud_name)
234 matching_aggs = [agg for agg in existing_aggregates
235 if agg['name'] == name]
236 if matching_aggs:
237 _call_nova_salt_module('aggregate_delete', name)(
238 name, cloud_name=cloud_name)
239 return _deleted(name, 'Host Aggregate')
240 return _non_existent(name, 'Host Aggregate')
241
242
243@_error_handler
244def keypair_present(name, cloud_name, public_key_file=None, public_key=None):
245 """Ensures that the Nova key-pair exists"""
246 existing_keypairs = _call_nova_salt_module('keypair_list', name)(
247 cloud_name=cloud_name)
248 matching_kps = [kp for kp in existing_keypairs
249 if kp['keypair']['name'] == name]
250 if public_key_file and not public_key:
251 with salt.utils.fopen(public_key_file, 'r') as f:
252 public_key = f.read()
253 if not public_key:
254 ret = _create_failed(name, 'Keypair')
255 ret['comment'] += '\nPlease specify public key for keypair creation.'
256 return ret
257 if matching_kps:
258 # Keypair names are unique, there is either 1 or 0 with requested name
259 kp = matching_kps[0]['keypair']
260 if kp['public_key'] != public_key:
261 _call_nova_salt_module('keypair_delete', name)(
262 name, cloud_name=cloud_name)
263 else:
264 return _no_change(name, 'Keypair')
265 res = _call_nova_salt_module('keypair_create', name)(
266 name, cloud_name=cloud_name, public_key=public_key)
267 return _created(name, 'Keypair', res)
268
269
270@_error_handler
271def keypair_absent(name, cloud_name):
272 """Ensure keypair is absent"""
273 existing_keypairs = _call_nova_salt_module('keypair_list', name)(
274 cloud_name=cloud_name)
275 matching_kps = [kp for kp in existing_keypairs
276 if kp['keypair']['name'] == name]
277 if matching_kps:
278 _call_nova_salt_module('keypair_delete', name)(
279 name, cloud_name=cloud_name)
280 return _deleted(name, 'Keypair')
281 return _non_existent(name, 'Keypair')
282
283
284def cell_present(name='cell1', transport_url='none:///', db_engine='mysql',
285 db_name='nova_upgrade', db_user='nova', db_password=None,
286 db_address='0.0.0.0'):
287 """Ensure nova cell is present
288
289 For newly created cells this state also runs discover_hosts and
290 map_instances."""
291 cell_info = __salt__['cmd.shell'](
292 "nova-manage cell_v2 list_cells --verbose | "
293 "awk '/%s/ {print $4,$6,$8}'" % name).split()
294 db_connection = (
295 '%(db_engine)s+pymysql://%(db_user)s:%(db_password)s@'
296 '%(db_address)s/%(db_name)s?charset=utf8' % {
297 'db_engine': db_engine, 'db_user': db_user,
298 'db_password': db_password, 'db_address': db_address,
299 'db_name': db_name})
300 args = {'transport_url': transport_url, 'db_connection': db_connection}
301 # There should be at least 1 component printed to cell_info
302 if len(cell_info) >= 1:
303 cell_info = dict(zip_longest(
304 ('cell_uuid', 'existing_transport_url', 'existing_db_connection'),
305 cell_info))
306 cell_uuid, existing_transport_url, existing_db_connection = cell_info
307 command_string = ''
308 if existing_transport_url != transport_url:
309 command_string = (
310 '%s --transport-url %%(transport_url)s' % command_string)
311 if existing_db_connection != db_connection:
312 command_string = (
313 '%s --database_connection %%(db_connection)s' % command_string)
314 if not command_string:
315 return _no_change(name, 'Nova cell')
316 try:
317 __salt__['cmd.shell'](
318 ('nova-manage cell_v2 update_cell --cell_uuid %s %s' % (
319 cell_uuid, command_string)) % args)
320 LOG.warning("Updating the transport_url or database_connection "
321 "fields on a running system will NOT result in all "
322 "nodes immediately using the new values. Use caution "
323 "when changing these values.")
324 ret = _updated(name, 'Nova cell', args)
325 except Exception as e:
326 ret = _update_failed(name, 'Nova cell')
327 ret['comment'] += '\nException: %s' % e
328 return ret
329 args.update(name=name)
330 try:
331 cell_uuid = __salt__['cmd.shell'](
332 'nova-manage cell_v2 create_cell --name %(name)s '
333 '--transport-url %(transport_url)s '
334 '--database_connection %(db_connection)s --verbose' % args)
335 __salt__['cmd.shell']('nova-manage cell_v2 discover_hosts '
336 '--cell_uuid %s --verbose' % cell_uuid)
337 __salt__['cmd.shell']('nova-manage cell_v2 map_instances '
338 '--cell_uuid %s' % cell_uuid)
339 ret = _created(name, 'Nova cell', args)
340 except Exception as e:
341 ret = _create_failed(name, 'Nova cell')
342 ret['comment'] += '\nException: %s' % e
343 return ret
344
345
346def cell_absent(name, force=False):
347 """Ensure cell is absent"""
348 cell_uuid = __salt__['cmd.shell'](
349 "nova-manage cell_v2 list_cells | awk '/%s/ {print $4}'" % name)
350 if not cell_uuid:
351 return _non_existent(name, 'Nova cell')
352 try:
353 __salt__['cmd.shell'](
354 'nova-manage cell_v2 delete_cell --cell_uuid %s %s' % (
355 cell_uuid, '--force' if force else ''))
356 ret = _deleted(name, 'Nova cell')
357 except Exception as e:
358 ret = _delete_failed(name, 'Nova cell')
359 ret['comment'] += '\nException: %s' % e
360 return ret
361
362
363def _db_version_update(db, version, human_readable_resource_name):
364 existing_version = __salt__['cmd.shell'](
365 'nova-manage %s version 2>/dev/null' % db)
366 try:
367 existing_version = int(existing_version)
368 version = int(version)
369 except Exception as e:
370 ret = _update_failed(existing_version,
371 human_readable_resource_name)
372 ret['comment'] += ('\nCan not convert existing or requested version '
373 'to integer, exception: %s' % e)
374 LOG.error(ret['comment'])
375 return ret
376 if existing_version < version:
377 try:
378 __salt__['cmd.shell'](
379 'nova-manage %s sync --version %s' % (db, version))
380 ret = _updated(existing_version, human_readable_resource_name,
381 {db: '%s sync --version %s' % (db, version)})
382 except Exception as e:
383 ret = _update_failed(existing_version,
384 human_readable_resource_name)
385 ret['comment'] += '\nException: %s' % e
386 return ret
387 return _no_change(existing_version, human_readable_resource_name)
388
389
390def api_db_version_present(name=None, version="20"):
391 """Ensures that specific api_db version is present"""
392 return _db_version_update('api_db', version, 'Nova API database version')
393
394
395def db_version_present(name=None, version="334"):
396 """Ensures that specific db version is present"""
397 return _db_version_update('db', version, 'Nova database version')
398
399
400def online_data_migrations_present(name=None, api_db_version="20",
401 db_version="334"):
402 """Runs online_data_migrations if databases are of specific versions"""
403 ret = {'name': 'online_data_migrations', 'changes': {}, 'result': False,
404 'comment': 'Current nova api_db version != {0} or nova db version '
405 '!= {1}.'.format(api_db_version, db_version)}
406 cur_api_db_version = __salt__['cmd.shell'](
407 'nova-manage api_db version 2>/dev/null')
408 cur_db_version = __salt__['cmd.shell'](
409 'nova-manage db version 2>/dev/null')
410 try:
411 cur_api_db_version = int(cur_api_db_version)
412 cur_db_version = int(cur_db_version)
413 api_db_version = int(api_db_version)
414 db_version = int(db_version)
415 except Exception as e:
416 LOG.error(ret['comment'])
417 ret['comment'] = ('\nCan not convert existing or requested database '
418 'versions to integer, exception: %s' % e)
419 return ret
420 if cur_api_db_version == api_db_version and cur_db_version == db_version:
421 try:
422 __salt__['cmd.shell']('nova-manage db online_data_migrations')
423 ret['result'] = True
424 ret['comment'] = ('nova-manage db online_data_migrations was '
425 'executed successfuly')
426 ret['changes']['online_data_migrations'] = (
427 'online_data_migrations run on nova api_db version {0} and '
428 'nova db version {1}'.format(api_db_version, db_version))
429 except Exception as e:
430 ret['comment'] = (
431 'Failed to execute online_data_migrations on nova api_db '
432 'version %s and nova db version %s, exception: %s' % (
433 api_db_version, db_version, e))
434 return ret
435
436
437def _find_failed(name, resource):
438 return {
439 'name': name, 'changes': {}, 'result': False,
440 'comment': 'Failed to find {0}s with name {1}'.format(resource, name)}
441
442
443def _created(name, resource, changes):
444 return {
445 'name': name, 'changes': changes, 'result': True,
446 'comment': '{0} {1} created'.format(resource, name)}
447
448
449def _create_failed(name, resource):
450 return {
451 'name': name, 'changes': {}, 'result': False,
452 'comment': '{0} {1} creation failed'.format(resource, name)}
453
454
455def _no_change(name, resource):
456 return {
457 'name': name, 'changes': {}, 'result': True,
458 'comment': '{0} {1} already is in the desired state'.format(
459 resource, name)}
460
461
462def _updated(name, resource, changes):
463 return {
464 'name': name, 'changes': changes, 'result': True,
465 'comment': '{0} {1} was updated'.format(resource, name)}
466
467
468def _update_failed(name, resource):
469 return {
470 'name': name, 'changes': {}, 'result': False,
471 'comment': '{0} {1} update failed'.format(resource, name)}
472
473
474def _deleted(name, resource):
475 return {
476 'name': name, 'changes': {}, 'result': True,
477 'comment': '{0} {1} deleted'.format(resource, name)}
478
479
480def _delete_failed(name, resource):
481 return {
482 'name': name, 'changes': {}, 'result': False,
483 'comment': '{0} {1} deletion failed'.format(resource, name)}
484
485
486def _non_existent(name, resource):
487 return {
488 'name': name, 'changes': {}, 'result': True,
489 'comment': '{0} {1} does not exist'.format(resource, name)}