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