blob: 8c8978ffa0ad502d354a62f7a69cf43e436ba3a2 [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,
Dzmitry Stremkouski6c9c79f2020-09-11 13:13:58 +020075 extra_specs=None, is_public=False):
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +030076 """Ensures that the flavor exists"""
77 extra_specs = extra_specs or {}
Dzmitry Stremkouski6c9c79f2020-09-11 13:13:58 +020078 # There is no way to query flavors by name. And we always list both
79 # public and private flavors
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +030080 flavors = _call_nova_salt_module('flavor_list', name)(
Dzmitry Stremkouski6c9c79f2020-09-11 13:13:58 +020081 detail=True, is_public=None, cloud_name=cloud_name)
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +030082 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 Stremkouski6c9c79f2020-09-11 13:13:58 +020084 # 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 Drokcb8d0fb2018-06-27 19:28:14 +030087 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 Stremkouski6c9c79f2020-09-11 13:13:58 +0200105 name, vcpus, ram, disk, id=flavor_id, cloud_name=cloud_name,
106 **{"os-flavor-access:is_public": is_public})
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300107 _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
115def flavor_absent(name, cloud_name):
116 """Ensure flavor is absent"""
Vladyslav Drok657827a2020-10-27 17:20:32 +0100117 # There is no way to query flavors by name. And we always list both
118 # public and private flavors
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300119 flavors = _call_nova_salt_module('flavor_list', name)(
Vladyslav Drok657827a2020-10-27 17:20:32 +0100120 detail=True, is_public=None, cloud_name=cloud_name)
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300121 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
130def _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
143def 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
170def 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
188def 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 Stremkouski6d57e8c2020-04-15 18:40:47 +0200204 name, cloud_name=cloud_name, **metadata)
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300205 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 Stremkouski6d57e8c2020-04-15 18:40:47 +0200216 if existing_meta - requested_meta or requested_meta - existing_meta:
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300217 _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 Stremkouski7f5c9242020-02-16 17:02:44 +0100222 if hosts and (hosts_to_remove or hosts_to_add):
Vladyslav Drokcb8d0fb2018-06-27 19:28:14 +0300223 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
237def 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
251def 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
278def 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
291def 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
353def 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 Drok8c124c32019-12-10 12:01:19 +0100370def instances_mapped_to_cell(name, max_count=None, timeout=60, runas='nova'):
Vladyslav Drok9adb24d2019-02-06 15:34:31 +0100371 """Ensure that all instances in the cell are mapped
372
373 :param name: cell name.
Vladyslav Drok8c124c32019-12-10 12:01:19 +0100374 :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 Drok9adb24d2019-02-06 15:34:31 +0100377 :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 Drok8c124c32019-12-10 12:01:19 +0100390 command = 'nova-manage cell_v2 map_instances --cell_uuid %s' % cell_uuid
391 if max_count:
392 command += ' --max-count %s' % max_count
Vladyslav Drok9adb24d2019-02-06 15:34:31 +0100393 start_time = time.time()
394 if not test:
395 while True:
Vladyslav Drok8c124c32019-12-10 12:01:19 +0100396 rc = __salt__['cmd.retcode'](command, runas=runas)
Vladyslav Drok9adb24d2019-02-06 15:34:31 +0100397 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 Drokcb8d0fb2018-06-27 19:28:14 +0300411def _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
438def 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
443def 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
448def 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 Hryhorov5cfb9d32018-09-11 16:55:24 +0000485@_error_handler
486def 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 Shyshkoc74c4772018-11-29 15:17:34 +0000495 name, binary=binary, cloud_name=cloud_name)
Oleh Hryhorov5cfb9d32018-09-11 16:55:24 +0000496 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
508def 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 Shyshkoc74c4772018-11-29 15:17:34 +0000522 name, binary=binary, cloud_name=cloud_name)
Oleh Hryhorov5cfb9d32018-09-11 16:55:24 +0000523 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 Drokcb8d0fb2018-06-27 19:28:14 +0300535def _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
541def _created(name, resource, changes):
542 return {
543 'name': name, 'changes': changes, 'result': True,
544 'comment': '{0} {1} created'.format(resource, name)}
545
546
547def _create_failed(name, resource):
548 return {
549 'name': name, 'changes': {}, 'result': False,
550 'comment': '{0} {1} creation failed'.format(resource, name)}
551
552
553def _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
560def _updated(name, resource, changes):
561 return {
562 'name': name, 'changes': changes, 'result': True,
563 'comment': '{0} {1} was updated'.format(resource, name)}
564
565
566def _update_failed(name, resource):
567 return {
568 'name': name, 'changes': {}, 'result': False,
569 'comment': '{0} {1} update failed'.format(resource, name)}
570
571
572def _deleted(name, resource):
573 return {
574 'name': name, 'changes': {}, 'result': True,
575 'comment': '{0} {1} deleted'.format(resource, name)}
576
577
578def _delete_failed(name, resource):
579 return {
580 'name': name, 'changes': {}, 'result': False,
581 'comment': '{0} {1} deletion failed'.format(resource, name)}
582
583
584def _non_existent(name, resource):
585 return {
586 'name': name, 'changes': {}, 'result': True,
587 'comment': '{0} {1} does not exist'.format(resource, name)}