blob: 060ccfb07a0e29184a0301cb1930248425aaa7b9 [file] [log] [blame]
Oleg Iurchenko5b1e5322017-10-20 00:29:20 +03001# -*- coding: utf-8 -*-
2'''
3Management of Keystone users
4============================
5
6:depends: - keystoneclient Python module
7:configuration: See :py:mod:`salt.modules.keystone` for setup instructions.
8
9.. code-block:: yaml
10
11 Keystone tenants:
12 keystoneng.tenant_present:
13 - names:
14 - admin
15 - demo
16 - service
17
18 Keystone roles:
19 keystoneng.role_present:
20 - names:
21 - admin
22 - Member
23
24 admin:
25 keystoneng.user_present:
26 - password: R00T_4CC3SS
27 - email: admin@domain.com
28 - roles:
29 admin: # tenants
30 - admin # roles
31 service:
32 - admin
33 - Member
34 - require:
35 - keystone: Keystone tenants
36 - keystone: Keystone roles
37
38 nova:
39 keystoneng.user_present:
40 - password: '$up3rn0v4'
41 - email: nova@domain.com
42 - tenant: service
43 - roles:
44 service:
45 - admin
46 - require:
47 - keystone: Keystone tenants
48 - keystone: Keystone roles
49
50 demo:
51 keystoneng.user_present:
52 - password: 'd3m0n$trati0n'
53 - email: demo@domain.com
54 - tenant: demo
55 - roles:
56 demo:
57 - Member
58 - require:
59 - keystone: Keystone tenants
60 - keystone: Keystone roles
61
62 nova service:
63 keystoneng.service_present:
64 - name: nova
65 - service_type: compute
66 - description: OpenStack Compute Service
67
68'''
69
70
71def __virtual__():
72 '''
73 Only load if the keystone module is in __salt__
74 '''
75 return 'keystoneng' if 'keystoneng.auth' in __salt__ else False
76
77
78_OS_IDENTITY_API_VERSION = 2
79_TENANT_ID = 'tenant_id'
80
81
82def _api_version(profile=None, **connection_args):
83 '''
84 Sets global variables _OS_IDENTITY_API_VERSION and _TENANT_ID
85 depending on API version.
86 '''
87 global _TENANT_ID
88 global _OS_IDENTITY_API_VERSION
89 try:
90 if float(__salt__['keystoneng.api_version'](profile=profile, **connection_args)) >= 3:
91 _TENANT_ID = 'project_id'
92 _OS_IDENTITY_API_VERSION = 3
93 except KeyError:
94 pass
95
96
97def user_present(name,
98 password,
99 email,
100 tenant=None,
101 enabled=True,
102 roles=None,
103 profile=None,
104 password_reset=True,
105 project=None,
106 **connection_args):
107 '''
108 Ensure that the keystone user is present with the specified properties.
109
110 name
111 The name of the user to manage
112
113 password
114 The password to use for this user.
115
116 .. note::
117
118 If the user already exists and a different password was set for
119 the user than the one specified here, the password for the user
120 will be updated. Please set the ``password_reset`` option to
121 ``False`` if this is not the desired behavior.
122
123 password_reset
124 Whether or not to reset password after initial set. Defaults to
125 ``True``.
126
127 email
128 The email address for this user
129
130 tenant
131 The tenant (name) for this user
132
133 project
134 The project (name) for this user (overrides tenant in api v3)
135
136 enabled
137 Availability state for this user
138
139 roles
140 The roles the user should have under given tenants.
141 Passed as a dictionary mapping tenant names to a list
142 of roles in this tenant, i.e.::
143
144 roles:
145 admin: # tenant
146 - admin # role
147 service:
148 - admin
149 - Member
150 '''
151 ret = {'name': name,
152 'changes': {},
153 'result': True,
154 'comment': 'User "{0}" will be updated'.format(name)}
155
156 _api_version(profile=profile, **connection_args)
157
158 if project and not tenant:
159 tenant = project
160
161 # Validate tenant if set
162 if tenant is not None:
163 tenantdata = __salt__['keystoneng.tenant_get'](name=tenant,
164 profile=profile,
165 **connection_args)
166 if 'Error' in tenantdata:
167 ret['result'] = False
168 ret['comment'] = 'Tenant / project "{0}" does not exist'.format(tenant)
169 return ret
170 tenant_id = tenantdata[tenant]['id']
171 else:
172 tenant_id = None
173
174 # Check if user is already present
175 user = __salt__['keystoneng.user_get'](name=name, profile=profile,
176 **connection_args)
177 if 'Error' not in user:
178
179 change_email = False
180 change_enabled = False
181 change_tenant = False
182 change_password = False
183
184 if user[name].get('email', None) != email:
185 change_email = True
186
187 if user[name].get('enabled', None) != enabled:
188 change_enabled = True
189
190 if tenant and (_TENANT_ID not in user[name] or
191 user[name].get(_TENANT_ID, None) != tenant_id):
192 change_tenant = True
193
194 if (password_reset is True and
195 not __salt__['keystoneng.user_verify_password'](name=name,
196 password=password,
197 profile=profile,
198 **connection_args)):
199 change_password = True
200
201 if __opts__.get('test') and (change_email or change_enabled or change_tenant or change_password):
202 ret['result'] = None
203 ret['comment'] = 'User "{0}" will be updated'.format(name)
204 if change_email is True:
205 ret['changes']['Email'] = 'Will be updated'
206 if change_enabled is True:
207 ret['changes']['Enabled'] = 'Will be True'
208 if change_tenant is True:
209 ret['changes']['Tenant'] = 'Will be added to "{0}" tenant'.format(tenant)
210 if change_password is True:
211 ret['changes']['Password'] = 'Will be updated'
212 return ret
213
214 ret['comment'] = 'User "{0}" is already present'.format(name)
215
216 if change_email:
217 __salt__['keystoneng.user_update'](name=name, email=email, profile=profile, **connection_args)
218 ret['comment'] = 'User "{0}" has been updated'.format(name)
219 ret['changes']['Email'] = 'Updated'
220
221 if change_enabled:
222 __salt__['keystoneng.user_update'](name=name, enabled=enabled, profile=profile, **connection_args)
223 ret['comment'] = 'User "{0}" has been updated'.format(name)
224 ret['changes']['Enabled'] = 'Now {0}'.format(enabled)
225
226 if change_tenant:
227 __salt__['keystoneng.user_update'](name=name, tenant=tenant, profile=profile, **connection_args)
228 ret['comment'] = 'User "{0}" has been updated'.format(name)
229 ret['changes']['Tenant'] = 'Added to "{0}" tenant'.format(tenant)
230
231 if change_password:
232 __salt__['keystoneng.user_password_update'](name=name, password=password, profile=profile,
233 **connection_args)
234 ret['comment'] = 'User "{0}" has been updated'.format(name)
235 ret['changes']['Password'] = 'Updated'
236
237 if roles:
238 for tenant in roles:
239 args = dict({'user_name': name, 'tenant_name':
240 tenant, 'profile': profile}, **connection_args)
241 tenant_roles = __salt__['keystoneng.user_role_list'](**args)
242 for role in roles[tenant]:
243 if role not in tenant_roles:
244 if __opts__.get('test'):
245 ret['result'] = None
246 ret['comment'] = 'User roles "{0}" will been updated'.format(name)
247 return ret
248 addargs = dict({'user': name, 'role': role,
249 'tenant': tenant,
250 'profile': profile},
251 **connection_args)
252 newrole = __salt__['keystoneng.user_role_add'](**addargs)
253 if 'roles' in ret['changes']:
254 ret['changes']['roles'].append(newrole)
255 else:
256 ret['changes']['roles'] = [newrole]
257 roles_to_remove = list(set(tenant_roles) - set(roles[tenant]))
258 for role in roles_to_remove:
259 if __opts__.get('test'):
260 ret['result'] = None
261 ret['comment'] = 'User roles "{0}" will been updated'.format(name)
262 return ret
263 addargs = dict({'user': name, 'role': role,
264 'tenant': tenant,
265 'profile': profile},
266 **connection_args)
267 oldrole = __salt__['keystoneng.user_role_remove'](**addargs)
268 if 'roles' in ret['changes']:
269 ret['changes']['roles'].append(oldrole)
270 else:
271 ret['changes']['roles'] = [oldrole]
272 else:
273 # Create that user!
274 if __opts__.get('test'):
275 ret['result'] = None
276 ret['comment'] = 'Keystone user "{0}" will be added'.format(name)
277 ret['changes']['User'] = 'Will be created'
278 return ret
279 __salt__['keystoneng.user_create'](name=name,
280 password=password,
281 email=email,
282 tenant_id=tenant_id,
283 enabled=enabled,
284 profile=profile,
285 **connection_args)
286 if roles:
287 for tenant in roles:
288 for role in roles[tenant]:
289 __salt__['keystoneng.user_role_add'](user=name,
290 role=role,
291 tenant=tenant,
292 profile=profile,
293 **connection_args)
294 ret['comment'] = 'Keystone user {0} has been added'.format(name)
295 ret['changes']['User'] = 'Created'
296
297 return ret
298
299
300def user_absent(name, profile=None, **connection_args):
301 '''
302 Ensure that the keystone user is absent.
303
304 name
305 The name of the user that should not exist
306 '''
307 ret = {'name': name,
308 'changes': {},
309 'result': True,
310 'comment': 'User "{0}" is already absent'.format(name)}
311
312 # Check if user is present
313 user = __salt__['keystoneng.user_get'](name=name, profile=profile,
314 **connection_args)
315 if 'Error' not in user:
316 if __opts__.get('test'):
317 ret['result'] = None
318 ret['comment'] = 'User "{0}" will be deleted'.format(name)
319 return ret
320 # Delete that user!
321 __salt__['keystoneng.user_delete'](name=name, profile=profile,
322 **connection_args)
323 ret['comment'] = 'User "{0}" has been deleted'.format(name)
324 ret['changes']['User'] = 'Deleted'
325
326 return ret
327
328
329def tenant_present(name, description=None, enabled=True, profile=None,
330 **connection_args):
331 '''
332 Ensures that the keystone tenant exists
333
334 name
335 The name of the tenant to manage
336
337 description
338 The description to use for this tenant
339
340 enabled
341 Availability state for this tenant
342 '''
343 ret = {'name': name,
344 'changes': {},
345 'result': True,
346 'comment': 'Tenant / project "{0}" already exists'.format(name)}
347
348 _api_version(profile=profile, **connection_args)
349
350 # Check if tenant is already present
351 tenant = __salt__['keystoneng.tenant_get'](name=name,
352 profile=profile,
353 **connection_args)
354
355 if 'Error' not in tenant:
356 if tenant[name].get('description', None) != description:
357 if __opts__.get('test'):
358 ret['result'] = None
359 ret['comment'] = 'Tenant / project "{0}" will be updated'.format(name)
360 ret['changes']['Description'] = 'Will be updated'
361 return ret
362 __salt__['keystoneng.tenant_update'](name=name,
363 description=description,
364 enabled=enabled,
365 profile=profile,
366 **connection_args)
367 ret['comment'] = 'Tenant / project "{0}" has been updated'.format(name)
368 ret['changes']['Description'] = 'Updated'
369 if tenant[name].get('enabled', None) != enabled:
370 if __opts__.get('test'):
371 ret['result'] = None
372 ret['comment'] = 'Tenant / project "{0}" will be updated'.format(name)
373 ret['changes']['Enabled'] = 'Will be {0}'.format(enabled)
374 return ret
375 __salt__['keystoneng.tenant_update'](name=name,
376 description=description,
377 enabled=enabled,
378 profile=profile,
379 **connection_args)
380 ret['comment'] = 'Tenant / project "{0}" has been updated'.format(name)
381 ret['changes']['Enabled'] = 'Now {0}'.format(enabled)
382 else:
383 if __opts__.get('test'):
384 ret['result'] = None
385 ret['comment'] = 'Tenant / project "{0}" will be added'.format(name)
386 ret['changes']['Tenant'] = 'Will be created'
387 return ret
388 # Create tenant
389 if _OS_IDENTITY_API_VERSION > 2:
390 created = __salt__['keystoneng.project_create'](name=name, domain='default', description=description,
391 enabled=enabled, profile=profile, **connection_args)
392 else:
393 created = __salt__['keystoneng.tenant_create'](name=name, description=description, enabled=enabled,
394 profile=profile, **connection_args)
395 ret['changes']['Tenant'] = 'Created' if created is True else 'Failed'
396 ret['result'] = created
397 ret['comment'] = 'Tenant / project "{0}" has been added'.format(name)
398 return ret
399
400
401def tenant_absent(name, profile=None, **connection_args):
402 '''
403 Ensure that the keystone tenant is absent.
404
405 name
406 The name of the tenant that should not exist
407 '''
408 ret = {'name': name,
409 'changes': {},
410 'result': True,
411 'comment': 'Tenant / project "{0}" is already absent'.format(name)}
412
413 # Check if tenant is present
414 tenant = __salt__['keystoneng.tenant_get'](name=name,
415 profile=profile,
416 **connection_args)
417 if 'Error' not in tenant:
418 if __opts__.get('test'):
419 ret['result'] = None
420 ret['comment'] = 'Tenant / project "{0}" will be deleted'.format(name)
421 return ret
422 # Delete tenant
423 __salt__['keystoneng.tenant_delete'](name=name, profile=profile,
424 **connection_args)
425 ret['comment'] = 'Tenant / project "{0}" has been deleted'.format(name)
426 ret['changes']['Tenant/Project'] = 'Deleted'
427
428 return ret
429
430
431def project_present(name, description=None, enabled=True, profile=None,
432 **connection_args):
433 '''
434 Ensures that the keystone project exists
435 Alias for tenant_present from V2 API to fulfill
436 V3 API naming convention.
437
438 .. versionadded:: 2016.11.0
439
440 name
441 The name of the project to manage
442
443 description
444 The description to use for this project
445
446 enabled
447 Availability state for this project
448
449 .. code-block:: yaml
450
451 nova:
452 keystoneng.project_present:
453 - enabled: True
454 - description: 'Nova Compute Service'
455
456 '''
457
458 return tenant_present(name, description=description, enabled=enabled, profile=profile,
459 **connection_args)
460
461
462def project_absent(name, profile=None, **connection_args):
463 '''
464 Ensure that the keystone project is absent.
465 Alias for tenant_absent from V2 API to fulfill
466 V3 API naming convention.
467
468 .. versionadded:: 2016.11.0
469
470 name
471 The name of the project that should not exist
472
473 .. code-block:: yaml
474
475 delete_nova:
476 keystoneng.project_absent:
477 - name: nova
478 '''
479
480 return tenant_absent(name, profile=profile, **connection_args)
481
482
483def role_present(name, profile=None, **connection_args):
484 ''''
485 Ensures that the keystone role exists
486
487 name
488 The name of the role that should be present
489 '''
490 ret = {'name': name,
491 'changes': {},
492 'result': True,
493 'comment': 'Role "{0}" already exists'.format(name)}
494
495 # Check if role is already present
496 role = __salt__['keystoneng.role_get'](name=name, profile=profile,
497 **connection_args)
498
499 if 'Error' not in role:
500 return ret
501 else:
502 if __opts__.get('test'):
503 ret['result'] = None
504 ret['comment'] = 'Role "{0}" will be added'.format(name)
505 return ret
506 # Create role
507 __salt__['keystoneng.role_create'](name, profile=profile,
508 **connection_args)
509 ret['comment'] = 'Role "{0}" has been added'.format(name)
510 ret['changes']['Role'] = 'Created'
511 return ret
512
513
514def role_absent(name, profile=None, **connection_args):
515 '''
516 Ensure that the keystone role is absent.
517
518 name
519 The name of the role that should not exist
520 '''
521 ret = {'name': name,
522 'changes': {},
523 'result': True,
524 'comment': 'Role "{0}" is already absent'.format(name)}
525
526 # Check if role is present
527 role = __salt__['keystoneng.role_get'](name=name, profile=profile,
528 **connection_args)
529 if 'Error' not in role:
530 if __opts__.get('test'):
531 ret['result'] = None
532 ret['comment'] = 'Role "{0}" will be deleted'.format(name)
533 return ret
534 # Delete role
535 __salt__['keystoneng.role_delete'](name=name, profile=profile,
536 **connection_args)
537 ret['comment'] = 'Role "{0}" has been deleted'.format(name)
538 ret['changes']['Role'] = 'Deleted'
539
540 return ret
541
542
543def service_present(name, service_type, description=None,
544 profile=None, **connection_args):
545 '''
546 Ensure service present in Keystone catalog
547
548 name
549 The name of the service
550
551 service_type
552 The type of Openstack Service
553
554 description (optional)
555 Description of the service
556 '''
557 ret = {'name': name,
558 'changes': {},
559 'result': True,
560 'comment': 'Service "{0}" already exists'.format(name)}
561
562 # Check if service is already present
563 role = __salt__['keystoneng.service_get'](name=name,
564 profile=profile,
565 **connection_args)
566
567 if 'Error' not in role:
568 return ret
569 else:
570 if __opts__.get('test'):
571 ret['result'] = None
572 ret['comment'] = 'Service "{0}" will be added'.format(name)
573 return ret
574 # Create service
575 __salt__['keystoneng.service_create'](name, service_type,
576 description,
577 profile=profile,
578 **connection_args)
579 ret['comment'] = 'Service "{0}" has been added'.format(name)
580 ret['changes']['Service'] = 'Created'
581
582 return ret
583
584
585def service_absent(name, profile=None, **connection_args):
586 '''
587 Ensure that the service doesn't exist in Keystone catalog
588
589 name
590 The name of the service that should not exist
591 '''
592 ret = {'name': name,
593 'changes': {},
594 'result': True,
595 'comment': 'Service "{0}" is already absent'.format(name)}
596
597 # Check if service is present
598 role = __salt__['keystoneng.service_get'](name=name,
599 profile=profile,
600 **connection_args)
601 if 'Error' not in role:
602 if __opts__.get('test'):
603 ret['result'] = None
604 ret['comment'] = 'Service "{0}" will be deleted'.format(name)
605 return ret
606 # Delete service
607 __salt__['keystoneng.service_delete'](name=name,
608 profile=profile,
609 **connection_args)
610 ret['comment'] = 'Service "{0}" has been deleted'.format(name)
611 ret['changes']['Service'] = 'Deleted'
612
613 return ret
614
615
616def endpoint_present(name,
617 publicurl=None,
618 internalurl=None,
619 adminurl=None,
620 region=None,
621 profile=None,
622 url=None,
623 interface=None, **connection_args):
624 '''
625 Ensure the specified endpoints exists for service
626
627 name
628 The Service name
629
630 publicurl
631 The public url of service endpoint (for V2 API)
632
633 internalurl
634 The internal url of service endpoint (for V2 API)
635
636 adminurl
637 The admin url of the service endpoint (for V2 API)
638
639 region
640 The region of the endpoint
641
642 url
643 The endpoint URL (for V3 API)
644
645 interface
646 The interface type, which describes the visibility
647 of the endpoint. (for V3 API)
648
649 '''
650 ret = {'name': name,
651 'changes': {},
652 'result': True,
653 'comment': ''}
654 endpoint = __salt__['keystoneng.endpoint_get'](name, region,
655 profile=profile,
656 interface=interface,
657 **connection_args)
658
659 def _changes(desc):
660 return ret.get('comment', '') + desc + '\n'
661
662 def _create_endpoint():
663 if _OS_IDENTITY_API_VERSION > 2:
664 ret['changes'] = __salt__['keystoneng.endpoint_create'](
665 name,
666 region=region,
667 url=url,
668 interface=interface,
669 profile=profile,
670 **connection_args)
671 else:
672 ret['changes'] = __salt__['keystoneng.endpoint_create'](
673 name,
674 region=region,
675 publicurl=publicurl,
676 adminurl=adminurl,
677 internalurl=internalurl,
678 profile=profile,
679 **connection_args)
680
681 if endpoint and 'Error' not in endpoint and endpoint.get('region') == region:
682
683 if _OS_IDENTITY_API_VERSION > 2:
684
685 change_url = False
686 change_interface = False
687
688 if endpoint.get('url', None) != url:
689 ret['comment'] = _changes('URL changes from "{0}" to "{1}"'.format(endpoint.get('url', None), url))
690 change_url = True
691
692 if endpoint.get('interface', None) != interface:
693 ret['comment'] = _changes('Interface changes from "{0}" to "{1}"'.format(endpoint.get('interface', None), interface))
694 change_interface = True
695
696 if __opts__.get('test') and (change_url or change_interface):
697 ret['result'] = None
698 ret['changes']['Endpoint'] = 'Will be updated'
699 ret['comment'] += 'Endpoint for service "{0}" will be updated'.format(name)
700 return ret
701
702 if change_url:
703 ret['changes']['url'] = url
704
705 if change_interface:
706 ret['changes']['interface'] = interface
707
708 else:
709 change_publicurl = False
710 change_adminurl = False
711 change_internalurl = False
712
713 if endpoint.get('publicurl', None) != publicurl:
714 change_publicurl = True
715
716 ret['comment'] = _changes('Public URL changes from "{0}" to "{1}"'.format(
717 endpoint.get('publicurl', None), publicurl)
718 )
719
720 if endpoint.get('adminurl', None) != adminurl:
721 change_adminurl = True
722 ret['comment'] = _changes('Admin URL changes from "{0}" to "{1}"'.format(
723 endpoint.get('adminurl', None), adminurl)
724 )
725
726 if endpoint.get('internalurl', None) != internalurl:
727 change_internalurl = True
728 ret['comment'] = _changes(
729 'Internal URL changes from "{0}" to "{1}"'.format(
730 endpoint.get('internalurl', None),
731 internalurl
732 )
733 )
734
735 if __opts__.get('test') and (change_publicurl or change_adminurl or change_internalurl):
736 ret['result'] = None
737 ret['comment'] += 'Endpoint for service "{0}" will be updated'.format(name)
738 ret['changes']['Endpoint'] = 'Will be updated'
739 return ret
740
741 if change_publicurl:
742 ret['changes']['publicurl'] = publicurl
743
744 if change_adminurl:
745 ret['changes']['adminurl'] = adminurl
746
747 if change_internalurl:
748 ret['changes']['internalurl'] = internalurl
749
750 if ret['comment']: # changed
751 __salt__['keystoneng.endpoint_delete'](name, region, profile=profile, interface=interface, **connection_args)
752 _create_endpoint()
753 ret['comment'] += 'Endpoint for service "{0}" has been updated'.format(name)
754
755 else:
756 # Add new endpoint
757 if __opts__.get('test'):
758 ret['result'] = None
759 ret['changes']['Endpoint'] = 'Will be created'
760 ret['comment'] = 'Endpoint for service "{0}" will be added'.format(name)
761 return ret
762 _create_endpoint()
763 ret['comment'] = 'Endpoint for service "{0}" has been added'.format(name)
764
765 if ret['comment'] == '': # => no changes
766 ret['comment'] = 'Endpoint for service "{0}" already exists'.format(name)
767 return ret
768
769
770def endpoint_absent(name, region=None, profile=None, interface=None, **connection_args):
771 '''
772 Ensure that the endpoint for a service doesn't exist in Keystone catalog
773
774 name
775 The name of the service whose endpoints should not exist
776
777 region (optional)
778 The region of the endpoint. Defaults to ``RegionOne``.
779
780 interface
781 The interface type, which describes the visibility
782 of the endpoint. (for V3 API)
783 '''
784 ret = {'name': name,
785 'changes': {},
786 'result': True,
787 'comment': 'Endpoint for service "{0}"{1} is already absent'.format(name,
788 ', interface "{0}",'.format(interface) if interface is not None else '')}
789
790 # Check if service is present
791 endpoint = __salt__['keystoneng.endpoint_get'](name, region,
792 profile=profile,
793 interface=interface,
794 **connection_args)
795 if not endpoint:
796 return ret
797 else:
798 if __opts__.get('test'):
799 ret['result'] = None
800 ret['comment'] = 'Endpoint for service "{0}" will be deleted'.format(name)
801 return ret
802 # Delete service
803 __salt__['keystoneng.endpoint_delete'](name, region,
804 profile=profile,
805 interface=interface,
806 **connection_args)
807 ret['comment'] = 'Endpoint for service "{0}"{1} has been deleted'.format(name,
808 ', interface "{0}",'.format(interface) if interface is not None else '')
809 ret['changes']['endpoint'] = 'Deleted'
810 return ret