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