1# Licensed under the Apache License, Version 2.0 (the "License");
2# you may not use this file except in compliance with the License.
3# You may obtain 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,
9# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10# See the License for the specific language governing permissions and
11# limitations under the License.
12
13# import types so that we can reference ListType in sphinx param declarations.
14# We can't just use list, because sphinx gets confused by
15# openstack.resource.Resource.list and openstack.resource2.Resource.list
16import types  # noqa
17
18import munch
19
20from openstack.cloud import _normalize
21from openstack.cloud import _utils
22from openstack.cloud import exc
23from openstack import utils
24
25
26class IdentityCloudMixin(_normalize.Normalizer):
27
28    @property
29    def _identity_client(self):
30        if 'identity' not in self._raw_clients:
31            self._raw_clients['identity'] = self._get_versioned_client(
32                'identity', min_version=2, max_version='3.latest')
33        return self._raw_clients['identity']
34
35    @_utils.cache_on_arguments()
36    def list_projects(self, domain_id=None, name_or_id=None, filters=None):
37        """List projects.
38
39        With no parameters, returns a full listing of all visible projects.
40
41        :param domain_id: domain ID to scope the searched projects.
42        :param name_or_id: project name or ID.
43        :param filters: a dict containing additional filters to use
44            OR
45            A string containing a jmespath expression for further filtering.
46            Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
47
48        :returns: a list of ``munch.Munch`` containing the projects
49
50        :raises: ``OpenStackCloudException``: if something goes wrong during
51            the OpenStack API call.
52        """
53        kwargs = dict(
54            filters=filters,
55            domain_id=domain_id)
56        if self._is_client_version('identity', 3):
57            kwargs['obj_name'] = 'project'
58
59        pushdown, filters = _normalize._split_filters(**kwargs)
60
61        try:
62            if self._is_client_version('identity', 3):
63                key = 'projects'
64            else:
65                key = 'tenants'
66            data = self._identity_client.get(
67                '/{endpoint}'.format(endpoint=key), params=pushdown)
68            projects = self._normalize_projects(
69                self._get_and_munchify(key, data))
70        except Exception as e:
71            self.log.debug("Failed to list projects", exc_info=True)
72            raise exc.OpenStackCloudException(str(e))
73        return _utils._filter_list(projects, name_or_id, filters)
74
75    def search_projects(self, name_or_id=None, filters=None, domain_id=None):
76        '''Backwards compatibility method for search_projects
77
78        search_projects originally had a parameter list that was name_or_id,
79        filters and list had domain_id first. This method exists in this form
80        to allow code written with positional parameter to still work. But
81        really, use keyword arguments.
82        '''
83        return self.list_projects(
84            domain_id=domain_id, name_or_id=name_or_id, filters=filters)
85
86    def get_project(self, name_or_id, filters=None, domain_id=None):
87        """Get exactly one project.
88
89        :param name_or_id: project name or ID.
90        :param filters: a dict containing additional filters to use.
91        :param domain_id: domain ID (identity v3 only).
92
93        :returns: a list of ``munch.Munch`` containing the project description.
94
95        :raises: ``OpenStackCloudException``: if something goes wrong during
96            the OpenStack API call.
97        """
98        return _utils._get_entity(self, 'project', name_or_id, filters,
99                                  domain_id=domain_id)
100
101    def update_project(self, name_or_id, enabled=None, domain_id=None,
102                       **kwargs):
103        with _utils.shade_exceptions(
104                "Error in updating project {project}".format(
105                    project=name_or_id)):
106            proj = self.get_project(name_or_id, domain_id=domain_id)
107            if not proj:
108                raise exc.OpenStackCloudException(
109                    "Project %s not found." % name_or_id)
110            if enabled is not None:
111                kwargs.update({'enabled': enabled})
112            # NOTE(samueldmq): Current code only allow updates of description
113            # or enabled fields.
114            if self._is_client_version('identity', 3):
115                data = self._identity_client.patch(
116                    '/projects/' + proj['id'], json={'project': kwargs})
117                project = self._get_and_munchify('project', data)
118            else:
119                data = self._identity_client.post(
120                    '/tenants/' + proj['id'], json={'tenant': kwargs})
121                project = self._get_and_munchify('tenant', data)
122            project = self._normalize_project(project)
123        self.list_projects.invalidate(self)
124        return project
125
126    def create_project(
127            self, name, description=None, domain_id=None, enabled=True):
128        """Create a project."""
129        with _utils.shade_exceptions(
130                "Error in creating project {project}".format(project=name)):
131            project_ref = self._get_domain_id_param_dict(domain_id)
132            project_ref.update({'name': name,
133                                'description': description,
134                                'enabled': enabled})
135            endpoint, key = ('tenants', 'tenant')
136            if self._is_client_version('identity', 3):
137                endpoint, key = ('projects', 'project')
138            data = self._identity_client.post(
139                '/{endpoint}'.format(endpoint=endpoint),
140                json={key: project_ref})
141            project = self._normalize_project(
142                self._get_and_munchify(key, data))
143        self.list_projects.invalidate(self)
144        return project
145
146    def delete_project(self, name_or_id, domain_id=None):
147        """Delete a project.
148
149        :param string name_or_id: Project name or ID.
150        :param string domain_id: Domain ID containing the project(identity v3
151            only).
152
153        :returns: True if delete succeeded, False if the project was not found.
154
155        :raises: ``OpenStackCloudException`` if something goes wrong during
156            the OpenStack API call
157        """
158
159        with _utils.shade_exceptions(
160                "Error in deleting project {project}".format(
161                    project=name_or_id)):
162            project = self.get_project(name_or_id, domain_id=domain_id)
163            if project is None:
164                self.log.debug(
165                    "Project %s not found for deleting", name_or_id)
166                return False
167
168            if self._is_client_version('identity', 3):
169                self._identity_client.delete('/projects/' + project['id'])
170            else:
171                self._identity_client.delete('/tenants/' + project['id'])
172
173        return True
174
175    @_utils.valid_kwargs('domain_id', 'name')
176    @_utils.cache_on_arguments()
177    def list_users(self, **kwargs):
178        """List users.
179
180        :param domain_id: Domain ID. (v3)
181
182        :returns: a list of ``munch.Munch`` containing the user description.
183
184        :raises: ``OpenStackCloudException``: if something goes wrong during
185            the OpenStack API call.
186        """
187        data = self._identity_client.get('/users', params=kwargs)
188        return _utils.normalize_users(
189            self._get_and_munchify('users', data))
190
191    @_utils.valid_kwargs('domain_id', 'name')
192    def search_users(self, name_or_id=None, filters=None, **kwargs):
193        """Search users.
194
195        :param string name_or_id: user name or ID.
196        :param domain_id: Domain ID. (v3)
197        :param filters: a dict containing additional filters to use.
198            OR
199            A string containing a jmespath expression for further filtering.
200            Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
201
202        :returns: a list of ``munch.Munch`` containing the users
203
204        :raises: ``OpenStackCloudException``: if something goes wrong during
205            the OpenStack API call.
206        """
207        # NOTE(jdwidari) if name_or_id isn't UUID like then make use of server-
208        # side filter for user name https://bit.ly/2qh0Ijk
209        # especially important when using LDAP and using page to limit results
210        if name_or_id and not _utils._is_uuid_like(name_or_id):
211            kwargs['name'] = name_or_id
212        users = self.list_users(**kwargs)
213        return _utils._filter_list(users, name_or_id, filters)
214
215    @_utils.valid_kwargs('domain_id')
216    def get_user(self, name_or_id, filters=None, **kwargs):
217        """Get exactly one user.
218
219        :param string name_or_id: user name or ID.
220        :param domain_id: Domain ID. (v3)
221        :param filters: a dict containing additional filters to use.
222            OR
223            A string containing a jmespath expression for further filtering.
224            Example:: "[?last_name==`Smith`] | [?other.gender]==`Female`]"
225
226        :returns: a single ``munch.Munch`` containing the user description.
227
228        :raises: ``OpenStackCloudException``: if something goes wrong during
229            the OpenStack API call.
230        """
231        if not _utils._is_uuid_like(name_or_id):
232            kwargs['name'] = name_or_id
233
234        return _utils._get_entity(self, 'user', name_or_id, filters, **kwargs)
235
236    def get_user_by_id(self, user_id, normalize=True):
237        """Get a user by ID.
238
239        :param string user_id: user ID
240        :param bool normalize: Flag to control dict normalization
241
242        :returns: a single ``munch.Munch`` containing the user description
243        """
244        data = self._identity_client.get(
245            '/users/{user}'.format(user=user_id),
246            error_message="Error getting user with ID {user_id}".format(
247                user_id=user_id))
248
249        user = self._get_and_munchify('user', data)
250        if user and normalize:
251            user = _utils.normalize_users(user)
252        return user
253
254    # NOTE(Shrews): Keystone v2 supports updating only name, email and enabled.
255    @_utils.valid_kwargs('name', 'email', 'enabled', 'domain_id', 'password',
256                         'description', 'default_project')
257    def update_user(self, name_or_id, **kwargs):
258        self.list_users.invalidate(self)
259        user_kwargs = {}
260        if 'domain_id' in kwargs and kwargs['domain_id']:
261            user_kwargs['domain_id'] = kwargs['domain_id']
262        user = self.get_user(name_or_id, **user_kwargs)
263
264        # TODO(mordred) When this changes to REST, force interface=admin
265        # in the adapter call if it's an admin force call (and figure out how
266        # to make that disctinction)
267        if self._is_client_version('identity', 2):
268            # Do not pass v3 args to a v2 keystone.
269            kwargs.pop('domain_id', None)
270            kwargs.pop('description', None)
271            kwargs.pop('default_project', None)
272            password = kwargs.pop('password', None)
273            if password is not None:
274                with _utils.shade_exceptions(
275                        "Error updating password for {user}".format(
276                            user=name_or_id)):
277                    error_msg = "Error updating password for user {}".format(
278                        name_or_id)
279                    data = self._identity_client.put(
280                        '/users/{u}/OS-KSADM/password'.format(u=user['id']),
281                        json={'user': {'password': password}},
282                        error_message=error_msg)
283
284            # Identity v2.0 implements PUT. v3 PATCH. Both work as PATCH.
285            data = self._identity_client.put(
286                '/users/{user}'.format(user=user['id']), json={'user': kwargs},
287                error_message="Error in updating user {}".format(name_or_id))
288        else:
289            # NOTE(samueldmq): now this is a REST call and domain_id is dropped
290            # if None. keystoneclient drops keys with None values.
291            if 'domain_id' in kwargs and kwargs['domain_id'] is None:
292                del kwargs['domain_id']
293            data = self._identity_client.patch(
294                '/users/{user}'.format(user=user['id']), json={'user': kwargs},
295                error_message="Error in updating user {}".format(name_or_id))
296
297        user = self._get_and_munchify('user', data)
298        self.list_users.invalidate(self)
299        return _utils.normalize_users([user])[0]
300
301    def create_user(
302            self, name, password=None, email=None, default_project=None,
303            enabled=True, domain_id=None, description=None):
304        """Create a user."""
305        params = self._get_identity_params(domain_id, default_project)
306        params.update({'name': name, 'password': password, 'email': email,
307                       'enabled': enabled})
308        if self._is_client_version('identity', 3):
309            params['description'] = description
310        elif description is not None:
311            self.log.info(
312                "description parameter is not supported on Keystone v2")
313
314        error_msg = "Error in creating user {user}".format(user=name)
315        data = self._identity_client.post('/users', json={'user': params},
316                                          error_message=error_msg)
317        user = self._get_and_munchify('user', data)
318
319        self.list_users.invalidate(self)
320        return _utils.normalize_users([user])[0]
321
322    @_utils.valid_kwargs('domain_id')
323    def delete_user(self, name_or_id, **kwargs):
324        # TODO(mordred) Why are we invalidating at the TOP?
325        self.list_users.invalidate(self)
326        user = self.get_user(name_or_id, **kwargs)
327        if not user:
328            self.log.debug(
329                "User {0} not found for deleting".format(name_or_id))
330            return False
331
332        # TODO(mordred) Extra GET only needed to support keystoneclient.
333        #               Can be removed as a follow-on.
334        user = self.get_user_by_id(user['id'], normalize=False)
335        self._identity_client.delete(
336            '/users/{user}'.format(user=user['id']),
337            error_message="Error in deleting user {user}".format(
338                user=name_or_id))
339
340        self.list_users.invalidate(self)
341        return True
342
343    def _get_user_and_group(self, user_name_or_id, group_name_or_id):
344        user = self.get_user(user_name_or_id)
345        if not user:
346            raise exc.OpenStackCloudException(
347                'User {user} not found'.format(user=user_name_or_id))
348
349        group = self.get_group(group_name_or_id)
350        if not group:
351            raise exc.OpenStackCloudException(
352                'Group {user} not found'.format(user=group_name_or_id))
353
354        return (user, group)
355
356    def add_user_to_group(self, name_or_id, group_name_or_id):
357        """Add a user to a group.
358
359        :param string name_or_id: User name or ID
360        :param string group_name_or_id: Group name or ID
361
362        :raises: ``OpenStackCloudException`` if something goes wrong during
363            the OpenStack API call
364        """
365        user, group = self._get_user_and_group(name_or_id, group_name_or_id)
366
367        error_msg = "Error adding user {user} to group {group}".format(
368            user=name_or_id, group=group_name_or_id)
369        self._identity_client.put(
370            '/groups/{g}/users/{u}'.format(g=group['id'], u=user['id']),
371            error_message=error_msg)
372
373    def is_user_in_group(self, name_or_id, group_name_or_id):
374        """Check to see if a user is in a group.
375
376        :param string name_or_id: User name or ID
377        :param string group_name_or_id: Group name or ID
378
379        :returns: True if user is in the group, False otherwise
380
381        :raises: ``OpenStackCloudException`` if something goes wrong during
382            the OpenStack API call
383        """
384        user, group = self._get_user_and_group(name_or_id, group_name_or_id)
385
386        try:
387            self._identity_client.head(
388                '/groups/{g}/users/{u}'.format(g=group['id'], u=user['id']))
389            return True
390        except exc.OpenStackCloudURINotFound:
391            # NOTE(samueldmq): knowing this URI exists, let's interpret this as
392            # user not found in group rather than URI not found.
393            return False
394
395    def remove_user_from_group(self, name_or_id, group_name_or_id):
396        """Remove a user from a group.
397
398        :param string name_or_id: User name or ID
399        :param string group_name_or_id: Group name or ID
400
401        :raises: ``OpenStackCloudException`` if something goes wrong during
402            the OpenStack API call
403        """
404        user, group = self._get_user_and_group(name_or_id, group_name_or_id)
405
406        error_msg = "Error removing user {user} from group {group}".format(
407            user=name_or_id, group=group_name_or_id)
408        self._identity_client.delete(
409            '/groups/{g}/users/{u}'.format(g=group['id'], u=user['id']),
410            error_message=error_msg)
411
412    @_utils.valid_kwargs('type', 'service_type', 'description')
413    def create_service(self, name, enabled=True, **kwargs):
414        """Create a service.
415
416        :param name: Service name.
417        :param type: Service type. (type or service_type required.)
418        :param service_type: Service type. (type or service_type required.)
419        :param description: Service description (optional).
420        :param enabled: Whether the service is enabled (v3 only)
421
422        :returns: a ``munch.Munch`` containing the services description,
423            i.e. the following attributes::
424            - id: <service id>
425            - name: <service name>
426            - type: <service type>
427            - service_type: <service type>
428            - description: <service description>
429
430        :raises: ``OpenStackCloudException`` if something goes wrong during the
431            OpenStack API call.
432
433        """
434        type_ = kwargs.pop('type', None)
435        service_type = kwargs.pop('service_type', None)
436
437        # TODO(mordred) When this changes to REST, force interface=admin
438        # in the adapter call
439        if self._is_client_version('identity', 2):
440            url, key = '/OS-KSADM/services', 'OS-KSADM:service'
441            kwargs['type'] = type_ or service_type
442        else:
443            url, key = '/services', 'service'
444            kwargs['type'] = type_ or service_type
445            kwargs['enabled'] = enabled
446        kwargs['name'] = name
447
448        msg = 'Failed to create service {name}'.format(name=name)
449        data = self._identity_client.post(
450            url, json={key: kwargs}, error_message=msg)
451        service = self._get_and_munchify(key, data)
452        return _utils.normalize_keystone_services([service])[0]
453
454    @_utils.valid_kwargs('name', 'enabled', 'type', 'service_type',
455                         'description')
456    def update_service(self, name_or_id, **kwargs):
457        # NOTE(SamYaple): Service updates are only available on v3 api
458        if self._is_client_version('identity', 2):
459            raise exc.OpenStackCloudUnavailableFeature(
460                'Unavailable Feature: Service update requires Identity v3'
461            )
462
463        # NOTE(SamYaple): Keystone v3 only accepts 'type' but shade accepts
464        #                 both 'type' and 'service_type' with a preference
465        #                 towards 'type'
466        type_ = kwargs.pop('type', None)
467        service_type = kwargs.pop('service_type', None)
468        if type_ or service_type:
469            kwargs['type'] = type_ or service_type
470
471        if self._is_client_version('identity', 2):
472            url, key = '/OS-KSADM/services', 'OS-KSADM:service'
473        else:
474            url, key = '/services', 'service'
475
476        service = self.get_service(name_or_id)
477        msg = 'Error in updating service {service}'.format(service=name_or_id)
478        data = self._identity_client.patch(
479            '{url}/{id}'.format(url=url, id=service['id']), json={key: kwargs},
480            error_message=msg)
481        service = self._get_and_munchify(key, data)
482        return _utils.normalize_keystone_services([service])[0]
483
484    def list_services(self):
485        """List all Keystone services.
486
487        :returns: a list of ``munch.Munch`` containing the services description
488
489        :raises: ``OpenStackCloudException`` if something goes wrong during the
490            OpenStack API call.
491        """
492        if self._is_client_version('identity', 2):
493            url, key = '/OS-KSADM/services', 'OS-KSADM:services'
494            endpoint_filter = {'interface': 'admin'}
495        else:
496            url, key = '/services', 'services'
497            endpoint_filter = {}
498
499        data = self._identity_client.get(
500            url, endpoint_filter=endpoint_filter,
501            error_message="Failed to list services")
502        services = self._get_and_munchify(key, data)
503        return _utils.normalize_keystone_services(services)
504
505    def search_services(self, name_or_id=None, filters=None):
506        """Search Keystone services.
507
508        :param name_or_id: Name or id of the desired service.
509        :param filters: a dict containing additional filters to use. e.g.
510                        {'type': 'network'}.
511
512        :returns: a list of ``munch.Munch`` containing the services description
513
514        :raises: ``OpenStackCloudException`` if something goes wrong during the
515            OpenStack API call.
516        """
517        services = self.list_services()
518        return _utils._filter_list(services, name_or_id, filters)
519
520    def get_service(self, name_or_id, filters=None):
521        """Get exactly one Keystone service.
522
523        :param name_or_id: Name or id of the desired service.
524        :param filters: a dict containing additional filters to use. e.g.
525                {'type': 'network'}
526
527        :returns: a ``munch.Munch`` containing the services description,
528            i.e. the following attributes::
529            - id: <service id>
530            - name: <service name>
531            - type: <service type>
532            - description: <service description>
533
534        :raises: ``OpenStackCloudException`` if something goes wrong during the
535            OpenStack API call or if multiple matches are found.
536        """
537        return _utils._get_entity(self, 'service', name_or_id, filters)
538
539    def delete_service(self, name_or_id):
540        """Delete a Keystone service.
541
542        :param name_or_id: Service name or id.
543
544        :returns: True if delete succeeded, False otherwise.
545
546        :raises: ``OpenStackCloudException`` if something goes wrong during
547            the OpenStack API call
548        """
549        service = self.get_service(name_or_id=name_or_id)
550        if service is None:
551            self.log.debug("Service %s not found for deleting", name_or_id)
552            return False
553
554        if self._is_client_version('identity', 2):
555            url = '/OS-KSADM/services'
556            endpoint_filter = {'interface': 'admin'}
557        else:
558            url = '/services'
559            endpoint_filter = {}
560
561        error_msg = 'Failed to delete service {id}'.format(id=service['id'])
562        self._identity_client.delete(
563            '{url}/{id}'.format(url=url, id=service['id']),
564            endpoint_filter=endpoint_filter, error_message=error_msg)
565
566        return True
567
568    @_utils.valid_kwargs('public_url', 'internal_url', 'admin_url')
569    def create_endpoint(self, service_name_or_id, url=None, interface=None,
570                        region=None, enabled=True, **kwargs):
571        """Create a Keystone endpoint.
572
573        :param service_name_or_id: Service name or id for this endpoint.
574        :param url: URL of the endpoint
575        :param interface: Interface type of the endpoint
576        :param public_url: Endpoint public URL.
577        :param internal_url: Endpoint internal URL.
578        :param admin_url: Endpoint admin URL.
579        :param region: Endpoint region.
580        :param enabled: Whether the endpoint is enabled
581
582        NOTE: Both v2 (public_url, internal_url, admin_url) and v3
583              (url, interface) calling semantics are supported. But
584              you can only use one of them at a time.
585
586        :returns: a list of ``munch.Munch`` containing the endpoint description
587
588        :raises: OpenStackCloudException if the service cannot be found or if
589            something goes wrong during the OpenStack API call.
590        """
591        public_url = kwargs.pop('public_url', None)
592        internal_url = kwargs.pop('internal_url', None)
593        admin_url = kwargs.pop('admin_url', None)
594
595        if (url or interface) and (public_url or internal_url or admin_url):
596            raise exc.OpenStackCloudException(
597                "create_endpoint takes either url and interface OR"
598                " public_url, internal_url, admin_url")
599
600        service = self.get_service(name_or_id=service_name_or_id)
601        if service is None:
602            raise exc.OpenStackCloudException(
603                "service {service} not found".format(
604                    service=service_name_or_id))
605
606        if self._is_client_version('identity', 2):
607            if url:
608                # v2.0 in use, v3-like arguments, one endpoint created
609                if interface != 'public':
610                    raise exc.OpenStackCloudException(
611                        "Error adding endpoint for service {service}."
612                        " On a v2 cloud the url/interface API may only be"
613                        " used for public url. Try using the public_url,"
614                        " internal_url, admin_url parameters instead of"
615                        " url and interface".format(
616                            service=service_name_or_id))
617                endpoint_args = {'publicurl': url}
618            else:
619                # v2.0 in use, v2.0-like arguments, one endpoint created
620                endpoint_args = {}
621                if public_url:
622                    endpoint_args.update({'publicurl': public_url})
623                if internal_url:
624                    endpoint_args.update({'internalurl': internal_url})
625                if admin_url:
626                    endpoint_args.update({'adminurl': admin_url})
627
628            # keystone v2.0 requires 'region' arg even if it is None
629            endpoint_args.update(
630                {'service_id': service['id'], 'region': region})
631
632            data = self._identity_client.post(
633                '/endpoints', json={'endpoint': endpoint_args},
634                endpoint_filter={'interface': 'admin'},
635                error_message=("Failed to create endpoint for service"
636                               " {service}".format(service=service['name'])))
637            return [self._get_and_munchify('endpoint', data)]
638        else:
639            endpoints_args = []
640            if url:
641                # v3 in use, v3-like arguments, one endpoint created
642                endpoints_args.append(
643                    {'url': url, 'interface': interface,
644                     'service_id': service['id'], 'enabled': enabled,
645                     'region': region})
646            else:
647                # v3 in use, v2.0-like arguments, one endpoint created for each
648                # interface url provided
649                endpoint_args = {'region': region, 'enabled': enabled,
650                                 'service_id': service['id']}
651                if public_url:
652                    endpoint_args.update({'url': public_url,
653                                          'interface': 'public'})
654                    endpoints_args.append(endpoint_args.copy())
655                if internal_url:
656                    endpoint_args.update({'url': internal_url,
657                                          'interface': 'internal'})
658                    endpoints_args.append(endpoint_args.copy())
659                if admin_url:
660                    endpoint_args.update({'url': admin_url,
661                                          'interface': 'admin'})
662                    endpoints_args.append(endpoint_args.copy())
663
664            endpoints = []
665            error_msg = ("Failed to create endpoint for service"
666                         " {service}".format(service=service['name']))
667            for args in endpoints_args:
668                data = self._identity_client.post(
669                    '/endpoints', json={'endpoint': args},
670                    error_message=error_msg)
671                endpoints.append(self._get_and_munchify('endpoint', data))
672            return endpoints
673
674    @_utils.valid_kwargs('enabled', 'service_name_or_id', 'url', 'interface',
675                         'region')
676    def update_endpoint(self, endpoint_id, **kwargs):
677        # NOTE(SamYaple): Endpoint updates are only available on v3 api
678        if self._is_client_version('identity', 2):
679            raise exc.OpenStackCloudUnavailableFeature(
680                'Unavailable Feature: Endpoint update'
681            )
682
683        service_name_or_id = kwargs.pop('service_name_or_id', None)
684        if service_name_or_id is not None:
685            kwargs['service_id'] = service_name_or_id
686
687        data = self._identity_client.patch(
688            '/endpoints/{}'.format(endpoint_id), json={'endpoint': kwargs},
689            error_message="Failed to update endpoint {}".format(endpoint_id))
690        return self._get_and_munchify('endpoint', data)
691
692    def list_endpoints(self):
693        """List Keystone endpoints.
694
695        :returns: a list of ``munch.Munch`` containing the endpoint description
696
697        :raises: ``OpenStackCloudException``: if something goes wrong during
698            the OpenStack API call.
699        """
700        # Force admin interface if v2.0 is in use
701        v2 = self._is_client_version('identity', 2)
702        kwargs = {'endpoint_filter': {'interface': 'admin'}} if v2 else {}
703
704        data = self._identity_client.get(
705            '/endpoints', error_message="Failed to list endpoints", **kwargs)
706        endpoints = self._get_and_munchify('endpoints', data)
707
708        return endpoints
709
710    def search_endpoints(self, id=None, filters=None):
711        """List Keystone endpoints.
712
713        :param id: endpoint id.
714        :param filters: a dict containing additional filters to use. e.g.
715                {'region': 'region-a.geo-1'}
716
717        :returns: a list of ``munch.Munch`` containing the endpoint
718            description. Each dict contains the following attributes::
719            - id: <endpoint id>
720            - region: <endpoint region>
721            - public_url: <endpoint public url>
722            - internal_url: <endpoint internal url> (optional)
723            - admin_url: <endpoint admin url> (optional)
724
725        :raises: ``OpenStackCloudException``: if something goes wrong during
726            the OpenStack API call.
727        """
728        # NOTE(SamYaple): With keystone v3 we can filter directly via the
729        # the keystone api, but since the return of all the endpoints even in
730        # large environments is small, we can continue to filter in shade just
731        # like the v2 api.
732        endpoints = self.list_endpoints()
733        return _utils._filter_list(endpoints, id, filters)
734
735    def get_endpoint(self, id, filters=None):
736        """Get exactly one Keystone endpoint.
737
738        :param id: endpoint id.
739        :param filters: a dict containing additional filters to use. e.g.
740                {'region': 'region-a.geo-1'}
741
742        :returns: a ``munch.Munch`` containing the endpoint description.
743            i.e. a ``munch.Munch`` containing the following attributes::
744            - id: <endpoint id>
745            - region: <endpoint region>
746            - public_url: <endpoint public url>
747            - internal_url: <endpoint internal url> (optional)
748            - admin_url: <endpoint admin url> (optional)
749        """
750        return _utils._get_entity(self, 'endpoint', id, filters)
751
752    def delete_endpoint(self, id):
753        """Delete a Keystone endpoint.
754
755        :param id: Id of the endpoint to delete.
756
757        :returns: True if delete succeeded, False otherwise.
758
759        :raises: ``OpenStackCloudException`` if something goes wrong during
760            the OpenStack API call.
761        """
762        endpoint = self.get_endpoint(id=id)
763        if endpoint is None:
764            self.log.debug("Endpoint %s not found for deleting", id)
765            return False
766
767        # Force admin interface if v2.0 is in use
768        v2 = self._is_client_version('identity', 2)
769        kwargs = {'endpoint_filter': {'interface': 'admin'}} if v2 else {}
770
771        error_msg = "Failed to delete endpoint {id}".format(id=id)
772        self._identity_client.delete('/endpoints/{id}'.format(id=id),
773                                     error_message=error_msg, **kwargs)
774
775        return True
776
777    def create_domain(self, name, description=None, enabled=True):
778        """Create a domain.
779
780        :param name: The name of the domain.
781        :param description: A description of the domain.
782        :param enabled: Is the domain enabled or not (default True).
783
784        :returns: a ``munch.Munch`` containing the domain representation.
785
786        :raise OpenStackCloudException: if the domain cannot be created.
787        """
788        domain_ref = {'name': name, 'enabled': enabled}
789        if description is not None:
790            domain_ref['description'] = description
791        msg = 'Failed to create domain {name}'.format(name=name)
792        data = self._identity_client.post(
793            '/domains', json={'domain': domain_ref}, error_message=msg)
794        domain = self._get_and_munchify('domain', data)
795        return _utils.normalize_domains([domain])[0]
796
797    def update_domain(
798            self, domain_id=None, name=None, description=None,
799            enabled=None, name_or_id=None):
800        if domain_id is None:
801            if name_or_id is None:
802                raise exc.OpenStackCloudException(
803                    "You must pass either domain_id or name_or_id value"
804                )
805            dom = self.get_domain(None, name_or_id)
806            if dom is None:
807                raise exc.OpenStackCloudException(
808                    "Domain {0} not found for updating".format(name_or_id)
809                )
810            domain_id = dom['id']
811
812        domain_ref = {}
813        domain_ref.update({'name': name} if name else {})
814        domain_ref.update({'description': description} if description else {})
815        domain_ref.update({'enabled': enabled} if enabled is not None else {})
816
817        error_msg = "Error in updating domain {id}".format(id=domain_id)
818        data = self._identity_client.patch(
819            '/domains/{id}'.format(id=domain_id),
820            json={'domain': domain_ref}, error_message=error_msg)
821        domain = self._get_and_munchify('domain', data)
822        return _utils.normalize_domains([domain])[0]
823
824    def delete_domain(self, domain_id=None, name_or_id=None):
825        """Delete a domain.
826
827        :param domain_id: ID of the domain to delete.
828        :param name_or_id: Name or ID of the domain to delete.
829
830        :returns: True if delete succeeded, False otherwise.
831
832        :raises: ``OpenStackCloudException`` if something goes wrong during
833            the OpenStack API call.
834        """
835        if domain_id is None:
836            if name_or_id is None:
837                raise exc.OpenStackCloudException(
838                    "You must pass either domain_id or name_or_id value"
839                )
840            dom = self.get_domain(name_or_id=name_or_id)
841            if dom is None:
842                self.log.debug(
843                    "Domain %s not found for deleting", name_or_id)
844                return False
845            domain_id = dom['id']
846
847        # A domain must be disabled before deleting
848        self.update_domain(domain_id, enabled=False)
849        error_msg = "Failed to delete domain {id}".format(id=domain_id)
850        self._identity_client.delete('/domains/{id}'.format(id=domain_id),
851                                     error_message=error_msg)
852
853        return True
854
855    def list_domains(self, **filters):
856        """List Keystone domains.
857
858        :returns: a list of ``munch.Munch`` containing the domain description.
859
860        :raises: ``OpenStackCloudException``: if something goes wrong during
861            the OpenStack API call.
862        """
863        data = self._identity_client.get(
864            '/domains', params=filters, error_message="Failed to list domains")
865        domains = self._get_and_munchify('domains', data)
866        return _utils.normalize_domains(domains)
867
868    def search_domains(self, filters=None, name_or_id=None):
869        """Search Keystone domains.
870
871        :param name_or_id: domain name or id
872        :param dict filters: A dict containing additional filters to use.
873             Keys to search on are id, name, enabled and description.
874
875        :returns: a list of ``munch.Munch`` containing the domain description.
876            Each ``munch.Munch`` contains the following attributes::
877            - id: <domain id>
878            - name: <domain name>
879            - description: <domain description>
880
881        :raises: ``OpenStackCloudException``: if something goes wrong during
882            the OpenStack API call.
883        """
884        if filters is None:
885            filters = {}
886        if name_or_id is not None:
887            domains = self.list_domains()
888            return _utils._filter_list(domains, name_or_id, filters)
889        else:
890            return self.list_domains(**filters)
891
892    def get_domain(self, domain_id=None, name_or_id=None, filters=None):
893        """Get exactly one Keystone domain.
894
895        :param domain_id: domain id.
896        :param name_or_id: domain name or id.
897        :param dict filters: A dict containing additional filters to use.
898             Keys to search on are id, name, enabled and description.
899
900        :returns: a ``munch.Munch`` containing the domain description, or None
901            if not found. Each ``munch.Munch`` contains the following
902            attributes::
903            - id: <domain id>
904            - name: <domain name>
905            - description: <domain description>
906
907        :raises: ``OpenStackCloudException``: if something goes wrong during
908            the OpenStack API call.
909        """
910        if domain_id is None:
911            # NOTE(SamYaple): search_domains() has filters and name_or_id
912            # in the wrong positional order which prevents _get_entity from
913            # being able to return quickly if passing a domain object so we
914            # duplicate that logic here
915            if hasattr(name_or_id, 'id'):
916                return name_or_id
917            return _utils._get_entity(self, 'domain', filters, name_or_id)
918        else:
919            error_msg = 'Failed to get domain {id}'.format(id=domain_id)
920            data = self._identity_client.get(
921                '/domains/{id}'.format(id=domain_id),
922                error_message=error_msg)
923            domain = self._get_and_munchify('domain', data)
924            return _utils.normalize_domains([domain])[0]
925
926    @_utils.valid_kwargs('domain_id')
927    @_utils.cache_on_arguments()
928    def list_groups(self, **kwargs):
929        """List Keystone Groups.
930
931        :param domain_id: domain id.
932
933        :returns: A list of ``munch.Munch`` containing the group description.
934
935        :raises: ``OpenStackCloudException``: if something goes wrong during
936            the OpenStack API call.
937        """
938        data = self._identity_client.get(
939            '/groups', params=kwargs, error_message="Failed to list groups")
940        return _utils.normalize_groups(self._get_and_munchify('groups', data))
941
942    @_utils.valid_kwargs('domain_id')
943    def search_groups(self, name_or_id=None, filters=None, **kwargs):
944        """Search Keystone groups.
945
946        :param name: Group name or id.
947        :param filters: A dict containing additional filters to use.
948        :param domain_id: domain id.
949
950        :returns: A list of ``munch.Munch`` containing the group description.
951
952        :raises: ``OpenStackCloudException``: if something goes wrong during
953            the OpenStack API call.
954        """
955        groups = self.list_groups(**kwargs)
956        return _utils._filter_list(groups, name_or_id, filters)
957
958    @_utils.valid_kwargs('domain_id')
959    def get_group(self, name_or_id, filters=None, **kwargs):
960        """Get exactly one Keystone group.
961
962        :param id: Group name or id.
963        :param filters: A dict containing additional filters to use.
964        :param domain_id: domain id.
965
966        :returns: A ``munch.Munch`` containing the group description.
967
968        :raises: ``OpenStackCloudException``: if something goes wrong during
969            the OpenStack API call.
970        """
971        return _utils._get_entity(self, 'group', name_or_id, filters, **kwargs)
972
973    def create_group(self, name, description, domain=None):
974        """Create a group.
975
976        :param string name: Group name.
977        :param string description: Group description.
978        :param string domain: Domain name or ID for the group.
979
980        :returns: A ``munch.Munch`` containing the group description.
981
982        :raises: ``OpenStackCloudException``: if something goes wrong during
983            the OpenStack API call.
984        """
985        group_ref = {'name': name}
986        if description:
987            group_ref['description'] = description
988        if domain:
989            dom = self.get_domain(domain)
990            if not dom:
991                raise exc.OpenStackCloudException(
992                    "Creating group {group} failed: Invalid domain "
993                    "{domain}".format(group=name, domain=domain)
994                )
995            group_ref['domain_id'] = dom['id']
996
997        error_msg = "Error creating group {group}".format(group=name)
998        data = self._identity_client.post(
999            '/groups', json={'group': group_ref}, error_message=error_msg)
1000        group = self._get_and_munchify('group', data)
1001        self.list_groups.invalidate(self)
1002        return _utils.normalize_groups([group])[0]
1003
1004    @_utils.valid_kwargs('domain_id')
1005    def update_group(self, name_or_id, name=None, description=None,
1006                     **kwargs):
1007        """Update an existing group
1008
1009        :param string name: New group name.
1010        :param string description: New group description.
1011        :param domain_id: domain id.
1012
1013        :returns: A ``munch.Munch`` containing the group description.
1014
1015        :raises: ``OpenStackCloudException``: if something goes wrong during
1016            the OpenStack API call.
1017        """
1018        self.list_groups.invalidate(self)
1019        group = self.get_group(name_or_id, **kwargs)
1020        if group is None:
1021            raise exc.OpenStackCloudException(
1022                "Group {0} not found for updating".format(name_or_id)
1023            )
1024
1025        group_ref = {}
1026        if name:
1027            group_ref['name'] = name
1028        if description:
1029            group_ref['description'] = description
1030
1031        error_msg = "Unable to update group {name}".format(name=name_or_id)
1032        data = self._identity_client.patch(
1033            '/groups/{id}'.format(id=group['id']),
1034            json={'group': group_ref}, error_message=error_msg)
1035        group = self._get_and_munchify('group', data)
1036        self.list_groups.invalidate(self)
1037        return _utils.normalize_groups([group])[0]
1038
1039    @_utils.valid_kwargs('domain_id')
1040    def delete_group(self, name_or_id, **kwargs):
1041        """Delete a group
1042
1043        :param name_or_id: ID or name of the group to delete.
1044        :param domain_id: domain id.
1045
1046        :returns: True if delete succeeded, False otherwise.
1047
1048        :raises: ``OpenStackCloudException``: if something goes wrong during
1049            the OpenStack API call.
1050        """
1051        group = self.get_group(name_or_id, **kwargs)
1052        if group is None:
1053            self.log.debug(
1054                "Group %s not found for deleting", name_or_id)
1055            return False
1056
1057        error_msg = "Unable to delete group {name}".format(name=name_or_id)
1058        self._identity_client.delete('/groups/{id}'.format(id=group['id']),
1059                                     error_message=error_msg)
1060
1061        self.list_groups.invalidate(self)
1062        return True
1063
1064    @_utils.valid_kwargs('domain_id', 'name')
1065    def list_roles(self, **kwargs):
1066        """List Keystone roles.
1067
1068        :param domain_id: domain id for listing roles (v3)
1069
1070        :returns: a list of ``munch.Munch`` containing the role description.
1071
1072        :raises: ``OpenStackCloudException``: if something goes wrong during
1073            the OpenStack API call.
1074        """
1075        v2 = self._is_client_version('identity', 2)
1076        url = '/OS-KSADM/roles' if v2 else '/roles'
1077        data = self._identity_client.get(
1078            url, params=kwargs, error_message="Failed to list roles")
1079        return self._normalize_roles(self._get_and_munchify('roles', data))
1080
1081    @_utils.valid_kwargs('domain_id')
1082    def search_roles(self, name_or_id=None, filters=None, **kwargs):
1083        """Seach Keystone roles.
1084
1085        :param string name: role name or id.
1086        :param dict filters: a dict containing additional filters to use.
1087        :param domain_id: domain id (v3)
1088
1089        :returns: a list of ``munch.Munch`` containing the role description.
1090            Each ``munch.Munch`` contains the following attributes::
1091
1092                - id: <role id>
1093                - name: <role name>
1094                - description: <role description>
1095
1096        :raises: ``OpenStackCloudException``: if something goes wrong during
1097            the OpenStack API call.
1098        """
1099        roles = self.list_roles(**kwargs)
1100        return _utils._filter_list(roles, name_or_id, filters)
1101
1102    @_utils.valid_kwargs('domain_id')
1103    def get_role(self, name_or_id, filters=None, **kwargs):
1104        """Get exactly one Keystone role.
1105
1106        :param id: role name or id.
1107        :param filters: a dict containing additional filters to use.
1108        :param domain_id: domain id (v3)
1109
1110        :returns: a single ``munch.Munch`` containing the role description.
1111            Each ``munch.Munch`` contains the following attributes::
1112
1113                - id: <role id>
1114                - name: <role name>
1115                - description: <role description>
1116
1117        :raises: ``OpenStackCloudException``: if something goes wrong during
1118            the OpenStack API call.
1119        """
1120        return _utils._get_entity(self, 'role', name_or_id, filters, **kwargs)
1121
1122    def _keystone_v2_role_assignments(self, user, project=None,
1123                                      role=None, **kwargs):
1124        data = self._identity_client.get(
1125            "/tenants/{tenant}/users/{user}/roles".format(
1126                tenant=project, user=user),
1127            error_message="Failed to list role assignments")
1128
1129        roles = self._get_and_munchify('roles', data)
1130
1131        ret = []
1132        for tmprole in roles:
1133            if role is not None and role != tmprole.id:
1134                continue
1135            ret.append({
1136                'role': {
1137                    'id': tmprole.id
1138                },
1139                'scope': {
1140                    'project': {
1141                        'id': project,
1142                    }
1143                },
1144                'user': {
1145                    'id': user,
1146                }
1147            })
1148        return ret
1149
1150    def _keystone_v3_role_assignments(self, **filters):
1151        # NOTE(samueldmq): different parameters have different representation
1152        # patterns as query parameters in the call to the list role assignments
1153        # API. The code below handles each set of patterns separately and
1154        # renames the parameters names accordingly, ignoring 'effective',
1155        # 'include_names' and 'include_subtree' whose do not need any renaming.
1156        for k in ('group', 'role', 'user'):
1157            if k in filters:
1158                filters[k + '.id'] = filters[k]
1159                del filters[k]
1160        for k in ('project', 'domain'):
1161            if k in filters:
1162                filters['scope.' + k + '.id'] = filters[k]
1163                del filters[k]
1164        if 'os_inherit_extension_inherited_to' in filters:
1165            filters['scope.OS-INHERIT:inherited_to'] = (
1166                filters['os_inherit_extension_inherited_to'])
1167            del filters['os_inherit_extension_inherited_to']
1168
1169        data = self._identity_client.get(
1170            '/role_assignments', params=filters,
1171            error_message="Failed to list role assignments")
1172        return self._get_and_munchify('role_assignments', data)
1173
1174    def list_role_assignments(self, filters=None):
1175        """List Keystone role assignments
1176
1177        :param dict filters: Dict of filter conditions. Acceptable keys are:
1178
1179            * 'user' (string) - User ID to be used as query filter.
1180            * 'group' (string) - Group ID to be used as query filter.
1181            * 'project' (string) - Project ID to be used as query filter.
1182            * 'domain' (string) - Domain ID to be used as query filter.
1183            * 'role' (string) - Role ID to be used as query filter.
1184            * 'os_inherit_extension_inherited_to' (string) - Return inherited
1185              role assignments for either 'projects' or 'domains'
1186            * 'effective' (boolean) - Return effective role assignments.
1187            * 'include_subtree' (boolean) - Include subtree
1188
1189            'user' and 'group' are mutually exclusive, as are 'domain' and
1190            'project'.
1191
1192            NOTE: For keystone v2, only user, project, and role are used.
1193                  Project and user are both required in filters.
1194
1195        :returns: a list of ``munch.Munch`` containing the role assignment
1196            description. Contains the following attributes::
1197
1198                - id: <role id>
1199                - user|group: <user or group id>
1200                - project|domain: <project or domain id>
1201
1202        :raises: ``OpenStackCloudException``: if something goes wrong during
1203            the OpenStack API call.
1204        """
1205        # NOTE(samueldmq): although 'include_names' is a valid query parameter
1206        # in the keystone v3 list role assignments API, it would have NO effect
1207        # on shade due to normalization. It is not documented as an acceptable
1208        # filter in the docs above per design!
1209
1210        if not filters:
1211            filters = {}
1212
1213        # NOTE(samueldmq): the docs above say filters are *IDs*, though if
1214        # munch.Munch objects are passed, this still works for backwards
1215        # compatibility as keystoneclient allows either IDs or objects to be
1216        # passed in.
1217        # TODO(samueldmq): fix the docs above to advertise munch.Munch objects
1218        # can be provided as parameters too
1219        for k, v in filters.items():
1220            if isinstance(v, munch.Munch):
1221                filters[k] = v['id']
1222
1223        if self._is_client_version('identity', 2):
1224            if filters.get('project') is None or filters.get('user') is None:
1225                raise exc.OpenStackCloudException(
1226                    "Must provide project and user for keystone v2"
1227                )
1228            assignments = self._keystone_v2_role_assignments(**filters)
1229        else:
1230            assignments = self._keystone_v3_role_assignments(**filters)
1231
1232        return _utils.normalize_role_assignments(assignments)
1233
1234    @_utils.valid_kwargs('domain_id')
1235    def create_role(self, name, **kwargs):
1236        """Create a Keystone role.
1237
1238        :param string name: The name of the role.
1239        :param domain_id: domain id (v3)
1240
1241        :returns: a ``munch.Munch`` containing the role description
1242
1243        :raise OpenStackCloudException: if the role cannot be created
1244        """
1245        v2 = self._is_client_version('identity', 2)
1246        url = '/OS-KSADM/roles' if v2 else '/roles'
1247        kwargs['name'] = name
1248        msg = 'Failed to create role {name}'.format(name=name)
1249        data = self._identity_client.post(
1250            url, json={'role': kwargs}, error_message=msg)
1251        role = self._get_and_munchify('role', data)
1252        return self._normalize_role(role)
1253
1254    @_utils.valid_kwargs('domain_id')
1255    def update_role(self, name_or_id, name, **kwargs):
1256        """Update a Keystone role.
1257
1258        :param name_or_id: Name or id of the role to update
1259        :param string name: The new role name
1260        :param domain_id: domain id
1261
1262        :returns: a ``munch.Munch`` containing the role description
1263
1264        :raise OpenStackCloudException: if the role cannot be created
1265        """
1266        if self._is_client_version('identity', 2):
1267            raise exc.OpenStackCloudUnavailableFeature(
1268                'Unavailable Feature: Role update requires Identity v3'
1269            )
1270        kwargs['name_or_id'] = name_or_id
1271        role = self.get_role(**kwargs)
1272        if role is None:
1273            self.log.debug(
1274                "Role %s not found for updating", name_or_id)
1275            return False
1276        msg = 'Failed to update role {name}'.format(name=name_or_id)
1277        json_kwargs = {'role_id': role.id, 'role': {'name': name}}
1278        data = self._identity_client.patch('/roles', error_message=msg,
1279                                           json=json_kwargs)
1280        role = self._get_and_munchify('role', data)
1281        return self._normalize_role(role)
1282
1283    @_utils.valid_kwargs('domain_id')
1284    def delete_role(self, name_or_id, **kwargs):
1285        """Delete a Keystone role.
1286
1287        :param string id: Name or id of the role to delete.
1288        :param domain_id: domain id (v3)
1289
1290        :returns: True if delete succeeded, False otherwise.
1291
1292        :raises: ``OpenStackCloudException`` if something goes wrong during
1293            the OpenStack API call.
1294        """
1295        role = self.get_role(name_or_id, **kwargs)
1296        if role is None:
1297            self.log.debug(
1298                "Role %s not found for deleting", name_or_id)
1299            return False
1300
1301        v2 = self._is_client_version('identity', 2)
1302        url = '{preffix}/{id}'.format(
1303            preffix='/OS-KSADM/roles' if v2 else '/roles', id=role['id'])
1304        error_msg = "Unable to delete role {name}".format(name=name_or_id)
1305        self._identity_client.delete(url, error_message=error_msg)
1306
1307        return True
1308
1309    def _get_grant_revoke_params(self, role, user=None, group=None,
1310                                 project=None, domain=None):
1311        role = self.get_role(role)
1312        if role is None:
1313            return {}
1314        data = {'role': role.id}
1315
1316        # domain and group not available in keystone v2.0
1317        is_keystone_v2 = self._is_client_version('identity', 2)
1318
1319        filters = {}
1320        if not is_keystone_v2 and domain:
1321            filters['domain_id'] = data['domain'] = \
1322                self.get_domain(domain)['id']
1323
1324        if user:
1325            if domain:
1326                data['user'] = self.get_user(user,
1327                                             domain_id=filters['domain_id'],
1328                                             filters=filters)
1329            else:
1330                data['user'] = self.get_user(user, filters=filters)
1331
1332        if project:
1333            # drop domain in favor of project
1334            data.pop('domain', None)
1335            data['project'] = self.get_project(project, filters=filters)
1336
1337        if not is_keystone_v2 and group:
1338            data['group'] = self.get_group(group, filters=filters)
1339
1340        return data
1341
1342    def grant_role(self, name_or_id, user=None, group=None,
1343                   project=None, domain=None, wait=False, timeout=60):
1344        """Grant a role to a user.
1345
1346        :param string name_or_id: The name or id of the role.
1347        :param string user: The name or id of the user.
1348        :param string group: The name or id of the group. (v3)
1349        :param string project: The name or id of the project.
1350        :param string domain: The id of the domain. (v3)
1351        :param bool wait: Wait for role to be granted
1352        :param int timeout: Timeout to wait for role to be granted
1353
1354        NOTE: domain is a required argument when the grant is on a project,
1355            user or group specified by name. In that situation, they are all
1356            considered to be in that domain. If different domains are in use
1357            in the same role grant, it is required to specify those by ID.
1358
1359        NOTE: for wait and timeout, sometimes granting roles is not
1360              instantaneous.
1361
1362        NOTE: project is required for keystone v2
1363
1364        :returns: True if the role is assigned, otherwise False
1365
1366        :raise OpenStackCloudException: if the role cannot be granted
1367        """
1368        data = self._get_grant_revoke_params(name_or_id, user, group,
1369                                             project, domain)
1370        filters = data.copy()
1371        if not data:
1372            raise exc.OpenStackCloudException(
1373                'Role {0} not found.'.format(name_or_id))
1374
1375        if data.get('user') is not None and data.get('group') is not None:
1376            raise exc.OpenStackCloudException(
1377                'Specify either a group or a user, not both')
1378        if data.get('user') is None and data.get('group') is None:
1379            raise exc.OpenStackCloudException(
1380                'Must specify either a user or a group')
1381        if self._is_client_version('identity', 2) and \
1382                data.get('project') is None:
1383            raise exc.OpenStackCloudException(
1384                'Must specify project for keystone v2')
1385
1386        if self.list_role_assignments(filters=filters):
1387            self.log.debug('Assignment already exists')
1388            return False
1389
1390        error_msg = "Error granting access to role: {0}".format(data)
1391        if self._is_client_version('identity', 2):
1392            # For v2.0, only tenant/project assignment is supported
1393            url = "/tenants/{t}/users/{u}/roles/OS-KSADM/{r}".format(
1394                t=data['project']['id'], u=data['user']['id'], r=data['role'])
1395
1396            self._identity_client.put(url, error_message=error_msg,
1397                                      endpoint_filter={'interface': 'admin'})
1398        else:
1399            if data.get('project') is None and data.get('domain') is None:
1400                raise exc.OpenStackCloudException(
1401                    'Must specify either a domain or project')
1402
1403            # For v3, figure out the assignment type and build the URL
1404            if data.get('domain'):
1405                url = "/domains/{}".format(data['domain'])
1406            else:
1407                url = "/projects/{}".format(data['project']['id'])
1408            if data.get('group'):
1409                url += "/groups/{}".format(data['group']['id'])
1410            else:
1411                url += "/users/{}".format(data['user']['id'])
1412            url += "/roles/{}".format(data.get('role'))
1413
1414            self._identity_client.put(url, error_message=error_msg)
1415
1416        if wait:
1417            for count in utils.iterate_timeout(
1418                    timeout,
1419                    "Timeout waiting for role to be granted"):
1420                if self.list_role_assignments(filters=filters):
1421                    break
1422        return True
1423
1424    def revoke_role(self, name_or_id, user=None, group=None,
1425                    project=None, domain=None, wait=False, timeout=60):
1426        """Revoke a role from a user.
1427
1428        :param string name_or_id: The name or id of the role.
1429        :param string user: The name or id of the user.
1430        :param string group: The name or id of the group. (v3)
1431        :param string project: The name or id of the project.
1432        :param string domain: The id of the domain. (v3)
1433        :param bool wait: Wait for role to be revoked
1434        :param int timeout: Timeout to wait for role to be revoked
1435
1436        NOTE: for wait and timeout, sometimes revoking roles is not
1437              instantaneous.
1438
1439        NOTE: project is required for keystone v2
1440
1441        :returns: True if the role is revoke, otherwise False
1442
1443        :raise OpenStackCloudException: if the role cannot be removed
1444        """
1445        data = self._get_grant_revoke_params(name_or_id, user, group,
1446                                             project, domain)
1447        filters = data.copy()
1448
1449        if not data:
1450            raise exc.OpenStackCloudException(
1451                'Role {0} not found.'.format(name_or_id))
1452
1453        if data.get('user') is not None and data.get('group') is not None:
1454            raise exc.OpenStackCloudException(
1455                'Specify either a group or a user, not both')
1456        if data.get('user') is None and data.get('group') is None:
1457            raise exc.OpenStackCloudException(
1458                'Must specify either a user or a group')
1459        if self._is_client_version('identity', 2) and \
1460                data.get('project') is None:
1461            raise exc.OpenStackCloudException(
1462                'Must specify project for keystone v2')
1463
1464        if not self.list_role_assignments(filters=filters):
1465            self.log.debug('Assignment does not exist')
1466            return False
1467
1468        error_msg = "Error revoking access to role: {0}".format(data)
1469        if self._is_client_version('identity', 2):
1470            # For v2.0, only tenant/project assignment is supported
1471            url = "/tenants/{t}/users/{u}/roles/OS-KSADM/{r}".format(
1472                t=data['project']['id'], u=data['user']['id'], r=data['role'])
1473
1474            self._identity_client.delete(
1475                url, error_message=error_msg,
1476                endpoint_filter={'interface': 'admin'})
1477        else:
1478            if data.get('project') is None and data.get('domain') is None:
1479                raise exc.OpenStackCloudException(
1480                    'Must specify either a domain or project')
1481
1482            # For v3, figure out the assignment type and build the URL
1483            if data.get('domain'):
1484                url = "/domains/{}".format(data['domain'])
1485            else:
1486                url = "/projects/{}".format(data['project']['id'])
1487            if data.get('group'):
1488                url += "/groups/{}".format(data['group']['id'])
1489            else:
1490                url += "/users/{}".format(data['user']['id'])
1491            url += "/roles/{}".format(data.get('role'))
1492
1493            self._identity_client.delete(url, error_message=error_msg)
1494
1495        if wait:
1496            for count in utils.iterate_timeout(
1497                    timeout,
1498                    "Timeout waiting for role to be revoked"):
1499                if not self.list_role_assignments(filters=filters):
1500                    break
1501        return True
1502
1503    def _get_project_id_param_dict(self, name_or_id):
1504        if name_or_id:
1505            project = self.get_project(name_or_id)
1506            if not project:
1507                return {}
1508            if self._is_client_version('identity', 3):
1509                return {'default_project_id': project['id']}
1510            else:
1511                return {'tenant_id': project['id']}
1512        else:
1513            return {}
1514
1515    def _get_domain_id_param_dict(self, domain_id):
1516        """Get a useable domain."""
1517
1518        # Keystone v3 requires domains for user and project creation. v2 does
1519        # not. However, keystone v2 does not allow user creation by non-admin
1520        # users, so we can throw an error to the user that does not need to
1521        # mention api versions
1522        if self._is_client_version('identity', 3):
1523            if not domain_id:
1524                raise exc.OpenStackCloudException(
1525                    "User or project creation requires an explicit"
1526                    " domain_id argument.")
1527            else:
1528                return {'domain_id': domain_id}
1529        else:
1530            return {}
1531
1532    def _get_identity_params(self, domain_id=None, project=None):
1533        """Get the domain and project/tenant parameters if needed.
1534
1535        keystone v2 and v3 are divergent enough that we need to pass or not
1536        pass project or tenant_id or domain or nothing in a sane manner.
1537        """
1538        ret = {}
1539        ret.update(self._get_domain_id_param_dict(domain_id))
1540        ret.update(self._get_project_id_param_dict(project))
1541        return ret
1542