# Copyright 2018 Wind River # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import keyring from collections import namedtuple from keystoneauth1 import exceptions as keystone_exceptions from keystoneclient import client as keystoneclient from oslo_log import log as logging from oslo_serialization import jsonutils from dcorch.common import consts from dcorch.common import exceptions from dcorch.engine.sync_thread import SyncThread LOG = logging.getLogger(__name__) class IdentitySyncThread(SyncThread): """Manages tasks related to resource management for keystone.""" def __init__(self, subcloud_engine): super(IdentitySyncThread, self).__init__(subcloud_engine) self.endpoint_type = consts.ENDPOINT_TYPE_IDENTITY self.sync_handler_map = { consts.RESOURCE_TYPE_IDENTITY_USERS: self.sync_identity_resource, consts.RESOURCE_TYPE_IDENTITY_USERS_PASSWORD: self.sync_identity_resource, consts.RESOURCE_TYPE_IDENTITY_ROLES: self.sync_identity_resource, consts.RESOURCE_TYPE_IDENTITY_PROJECTS: self.sync_identity_resource, consts.RESOURCE_TYPE_IDENTITY_PROJECT_ROLE_ASSIGNMENTS: self.sync_identity_resource, } # Since services may use unscoped tokens, it is essential to ensure # that users are replicated prior to assignment data (roles/projects) self.audit_resources = [ consts.RESOURCE_TYPE_IDENTITY_USERS, consts.RESOURCE_TYPE_IDENTITY_ROLES, consts.RESOURCE_TYPE_IDENTITY_PROJECTS, consts.RESOURCE_TYPE_IDENTITY_PROJECT_ROLE_ASSIGNMENTS ] # For all the resource types, we need to filter out certain # resources self.filtered_audit_resources = { consts.RESOURCE_TYPE_IDENTITY_USERS: ['admin', 'mtce', 'heat_admin', 'cinder' + self.subcloud_engine.subcloud.region_name], consts.RESOURCE_TYPE_IDENTITY_ROLES: ['heat_stack_owner', 'heat_stack_user', 'ResellerAdmin'], consts.RESOURCE_TYPE_IDENTITY_PROJECTS: ['admin', 'services'] } self.log_extra = {"instance": "{}/{}: ".format( self.subcloud_engine.subcloud.region_name, self.endpoint_type)} self.sc_ks_client = None self.initialize() LOG.info("IdentitySyncThread initialized", extra=self.log_extra) def initialize_sc_clients(self): super(IdentitySyncThread, self).initialize_sc_clients() if (not self.sc_ks_client and self.sc_admin_session): self.sc_ks_client = keystoneclient.Client( session=self.sc_admin_session, endpoint_type=consts.KS_ENDPOINT_INTERNAL, region_name=self.subcloud_engine.subcloud.region_name) def initialize(self): # Subcloud may be enabled a while after being added. # Keystone endpoints for the subcloud could be added in # between these 2 steps. Reinitialize the session to # get the most up-to-date service catalog. super(IdentitySyncThread, self).initialize() # We initialize a master version of the keystone client, and a # subcloud specific version self.m_ks_client = self.ks_client LOG.info("Identity session and clients initialized", extra=self.log_extra) def sync_identity_resource(self, request, rsrc): self.initialize_sc_clients() # Invoke function with name format "operationtype_resourcetype" # For example: post_users() try: # If this sync is triggered by an audit, then the default # audit action is a CREATE instead of a POST Operation Type. # We therefore recognize those triggers and convert them to # POST operations operation_type = request.orch_job.operation_type rtype_role_assignments = \ consts.RESOURCE_TYPE_IDENTITY_PROJECT_ROLE_ASSIGNMENTS if operation_type == consts.OPERATION_TYPE_CREATE: if (rsrc.resource_type == rtype_role_assignments): operation_type = consts.OPERATION_TYPE_PUT else: operation_type = consts.OPERATION_TYPE_POST func_name = operation_type + \ "_" + rsrc.resource_type getattr(self, func_name)(request, rsrc) except AttributeError: LOG.error("{} not implemented for {}" .format(operation_type, rsrc.resource_type)) raise exceptions.SyncRequestFailed except (keystone_exceptions.connection.ConnectTimeout, keystone_exceptions.ConnectFailure) as e: LOG.error("sync_identity_resource: {} is not reachable [{}]" .format(self.subcloud_engine.subcloud.region_name, str(e)), extra=self.log_extra) raise exceptions.SyncRequestTimeout except exceptions.SyncRequestFailed: raise except Exception as e: LOG.exception(e) raise exceptions.SyncRequestFailedRetry def post_users(self, request, rsrc): # Create this user on this subcloud user_dict = jsonutils.loads(request.orch_job.resource_info) if 'user' in user_dict.keys(): user_dict = user_dict['user'] # (NOTE: knasim-wrs): If the user create request contains # "default_project_id" or "domain_id" then we need to remove # both these fields, since it is highly unlikely that these # IDs would exist on the subcloud, i.e. the ID for the "services" # project on subcloud-X will be different to the ID for the # project on Central Region. # These fields are optional anyways since a subsequent role # assignment will give the same scoping # # If these do need to be synced in the future then # procure the project / domain list for this subcloud first # and use IDs from that. user_dict.pop('default_project_id', None) user_dict.pop('domain_id', None) username = user_dict.pop('name', None) # compulsory if not username: LOG.error("Received user create request without required " "'name' field", extra=self.log_extra) raise exceptions.SyncRequestFailed password = user_dict.pop('password', None) # compulsory if not password: # this user creation request may have been generated # from the Identity Audit, in which case this password # would not be present in the resource info. We will # attempt to retrieve it from Keyring, failing which # we cannot proceed. # TODO(knasim-wrs): Set Service as constant password = keyring.get_password('CGCS', username) if not password: LOG.error("Received user create request without required " "'password' field and cannot retrieve from " "Keyring either", extra=self.log_extra) raise exceptions.SyncRequestFailed # Create the user in the subcloud user_ref = self.sc_ks_client.users.create( name=username, domain=user_dict.pop('domain', None), password=password, email=user_dict.pop('email', None), description=user_dict.pop('description', None), enabled=user_dict.pop('enabled', True), project=user_dict.pop('project', None), default_project=user_dict.pop('default_project', None)) user_ref_id = user_ref.id # Persist the subcloud resource. subcloud_rsrc_id = self.persist_db_subcloud_resource(rsrc.id, user_ref_id) LOG.info("Created Keystone user {}:{} [{}]" .format(rsrc.id, subcloud_rsrc_id, username), extra=self.log_extra) def post_users_password(self, request, rsrc): # Update this user's password on this subcloud user_dict = jsonutils.loads(request.orch_job.resource_info) oldpw = user_dict.pop('original_password', None) newpw = user_dict.pop('password', None) if (not oldpw or not newpw): LOG.error("Received users password change request without " "required original password or new password field", extra=self.log_extra) raise exceptions.SyncRequestFailed # NOTE (knasim-wrs): We can only update the password of the ADMIN # user, that is the one used to establish this subcloud session, # since the default behavior within the keystone client is to # take the user_id from within the client context (client.user_id) # user_id for this resource was passed in via URL and extracted # into the resource_id if (self.sc_ks_client.user_id == rsrc.id): self.sc_ks_client.users.update_password(oldpw, newpw) LOG.info("Updated password for user {}".format(rsrc.id), extra=self.log_extra) else: LOG.error("User {} requested a modification to its password. " "Can only self-modify for user {}. Consider updating " "the password for {} using the Admin user" .format(rsrc.id, self.sc_ks_client.user_id, rsrc.id)) raise exceptions.SyncRequestFailed def patch_users(self, request, rsrc): # Update user reference on this subcloud user_update_dict = jsonutils.loads(request.orch_job.resource_info) if not user_update_dict.keys(): LOG.error("Received user update request " "without any update fields", extra=self.log_extra) raise exceptions.SyncRequestFailed user_update_dict = user_update_dict['user'] user_subcloud_rsrc = self.get_db_subcloud_resource(rsrc.id) if not user_subcloud_rsrc: LOG.error("Unable to update user reference {}:{}, " "cannot find equivalent Keystone user in subcloud." .format(rsrc, user_update_dict), extra=self.log_extra) return # instead of stowing the entire user reference or # retrieving it, we build an opaque wrapper for the # v3 User Manager, containing the ID field which is # needed to update this user reference UserReferenceWrapper = namedtuple('UserReferenceWrapper', 'id') user_id = user_subcloud_rsrc.subcloud_resource_id original_user_ref = UserReferenceWrapper(id=user_id) # Update the user in the subcloud user_ref = self.sc_ks_client.users.update( original_user_ref, name=user_update_dict.pop('name', None), domain=user_update_dict.pop('domain', None), project=user_update_dict.pop('project', None), password=user_update_dict.pop('password', None), email=user_update_dict.pop('email', None), description=user_update_dict.pop('description', None), enabled=user_update_dict.pop('enabled', None), default_project=user_update_dict.pop('default_project', None)) if (user_ref.id == user_id): LOG.info("Updated Keystone user: {}:{}" .format(rsrc.id, user_ref.id), extra=self.log_extra) else: LOG.error("Unable to update Keystone user {}:{} for subcloud" .format(rsrc.id, user_id), extra=self.log_extra) def delete_users(self, request, rsrc): # Delete user reference on this subcloud user_subcloud_rsrc = self.get_db_subcloud_resource(rsrc.id) if not user_subcloud_rsrc: LOG.error("Unable to delete user reference {}, " "cannot find equivalent Keystone user in subcloud." .format(rsrc), extra=self.log_extra) return # instead of stowing the entire user reference or # retrieving it, we build an opaque wrapper for the # v3 User Manager, containing the ID field which is # needed to delete this user reference UserReferenceWrapper = namedtuple('UserReferenceWrapper', 'id') user_id = user_subcloud_rsrc.subcloud_resource_id original_user_ref = UserReferenceWrapper(id=user_id) # Delete the user in the subcloud self.sc_ks_client.users.delete(original_user_ref) # Master Resource can be deleted only when all subcloud resources # are deleted along with corresponding orch_job and orch_requests. LOG.info("Keystone user {}:{} [{}] deleted" .format(rsrc.id, user_subcloud_rsrc.id, user_subcloud_rsrc.subcloud_resource_id), extra=self.log_extra) user_subcloud_rsrc.delete() def post_projects(self, request, rsrc): # Create this project on this subcloud project_dict = jsonutils.loads(request.orch_job.resource_info) if 'project' in project_dict.keys(): project_dict = project_dict['project'] projectname = project_dict.pop('name', None) # compulsory projectdomain = project_dict.pop('domain_id', 'default') # compulsory if not projectname: LOG.error("Received project create request without required " "'name' field", extra=self.log_extra) raise exceptions.SyncRequestFailed # Create the project in the subcloud project_ref = self.sc_ks_client.projects.create( name=projectname, domain=projectdomain, description=project_dict.pop('description', None), enabled=project_dict.pop('enabled', True), parent=project_dict.pop('parent_id', None)) project_ref_id = project_ref.id # Persist the subcloud resource. subcloud_rsrc_id = self.persist_db_subcloud_resource(rsrc.id, project_ref_id) LOG.info("Created Keystone project {}:{} [{}]" .format(rsrc.id, subcloud_rsrc_id, projectname), extra=self.log_extra) def patch_projects(self, request, rsrc): # Update project on this subcloud project_update_dict = jsonutils.loads(request.orch_job.resource_info) if not project_update_dict.keys(): LOG.error("Received project update request " "without any update fields", extra=self.log_extra) raise exceptions.SyncRequestFailed project_update_dict = project_update_dict['project'] project_subcloud_rsrc = self.get_db_subcloud_resource(rsrc.id) if not project_subcloud_rsrc: LOG.error("Unable to update project reference {}:{}, " "cannot find equivalent Keystone project in subcloud." .format(rsrc, project_update_dict), extra=self.log_extra) return # instead of stowing the entire project reference or # retrieving it, we build an opaque wrapper for the # v3 ProjectManager, containing the ID field which is # needed to update this user reference ProjectReferenceWrapper = namedtuple('ProjectReferenceWrapper', 'id') proj_id = project_subcloud_rsrc.subcloud_resource_id original_proj_ref = ProjectReferenceWrapper(id=proj_id) # Update the project in the subcloud project_ref = self.sc_ks_client.projects.update( original_proj_ref, name=project_update_dict.pop('name', None), domain=project_update_dict.pop('domain_id', None), description=project_update_dict.pop('description', None), enabled=project_update_dict.pop('enabled', None)) if (project_ref.id == proj_id): LOG.info("Updated Keystone project: {}:{}" .format(rsrc.id, project_ref.id), extra=self.log_extra) else: LOG.error("Unable to update Keystone project {}:{} for subcloud" .format(rsrc.id, proj_id), extra=self.log_extra) def delete_projects(self, request, rsrc): # Delete this project on this subcloud project_subcloud_rsrc = self.get_db_subcloud_resource(rsrc.id) if not project_subcloud_rsrc: LOG.error("Unable to delete project reference {}, " "cannot find equivalent Keystone project in subcloud." .format(rsrc), extra=self.log_extra) return # instead of stowing the entire project reference or # retrieving it, we build an opaque wrapper for the # v3 ProjectManager, containing the ID field which is # needed to delete this project reference ProjectReferenceWrapper = namedtuple('ProjectReferenceWrapper', 'id') proj_id = project_subcloud_rsrc.subcloud_resource_id original_proj_ref = ProjectReferenceWrapper(id=proj_id) # Delete the project in the subcloud self.sc_ks_client.projects.delete(original_proj_ref) # Master Resource can be deleted only when all subcloud resources # are deleted along with corresponding orch_job and orch_requests. LOG.info("Keystone project {}:{} [{}] deleted" .format(rsrc.id, project_subcloud_rsrc.id, project_subcloud_rsrc.subcloud_resource_id), extra=self.log_extra) project_subcloud_rsrc.delete() def post_roles(self, request, rsrc): # Create this role on this subcloud role_dict = jsonutils.loads(request.orch_job.resource_info) if 'role' in role_dict.keys(): role_dict = role_dict['role'] rolename = role_dict.pop('name', None) # compulsory if not rolename: LOG.error("Received role create request without required " "'name' field", extra=self.log_extra) raise exceptions.SyncRequestFailed # Create the role in the subcloud role_ref = self.sc_ks_client.roles.create( name=rolename, domain=role_dict.pop('domain_id', None)) role_ref_id = role_ref.id # Persist the subcloud resource. subcloud_rsrc_id = self.persist_db_subcloud_resource(rsrc.id, role_ref_id) LOG.info("Created Keystone role {}:{} [{}]" .format(rsrc.id, subcloud_rsrc_id, rolename), extra=self.log_extra) def patch_roles(self, request, rsrc): # Update this role on this subcloud role_update_dict = jsonutils.loads(request.orch_job.resource_info) if not role_update_dict.keys(): LOG.error("Received role update request " "without any update fields", extra=self.log_extra) raise exceptions.SyncRequestFailed role_update_dict = role_update_dict['role'] role_subcloud_rsrc = self.get_db_subcloud_resource(rsrc.id) if not role_subcloud_rsrc: LOG.error("Unable to update role reference {}:{}, " "cannot find equivalent Keystone role in subcloud." .format(rsrc, role_update_dict), extra=self.log_extra) return # instead of stowing the entire role reference or # retrieving it, we build an opaque wrapper for the # v3 RoleManager, containing the ID field which is # needed to update this user reference RoleReferenceWrapper = namedtuple('RoleReferenceWrapper', 'id') role_id = role_subcloud_rsrc.subcloud_resource_id original_role_ref = RoleReferenceWrapper(id=role_id) # Update the role in the subcloud role_ref = self.sc_ks_client.roles.update( original_role_ref, name=role_update_dict.pop('name', None)) if (role_ref.id == role_id): LOG.info("Updated Keystone role: {}:{}" .format(rsrc.id, role_ref.id), extra=self.log_extra) else: LOG.error("Unable to update Keystone role {}:{} for subcloud" .format(rsrc.id, role_id), extra=self.log_extra) def delete_roles(self, request, rsrc): # Delete this role on this subcloud role_subcloud_rsrc = self.get_db_subcloud_resource(rsrc.id) if not role_subcloud_rsrc: LOG.error("Unable to delete role reference {}, " "cannot find equivalent Keystone role in subcloud." .format(rsrc), extra=self.log_extra) return # instead of stowing the entire role reference or # retrieving it, we build an opaque wrapper for the # v3 RoleManager, containing the ID field which is # needed to delete this role reference RoleReferenceWrapper = namedtuple('RoleReferenceWrapper', 'id') role_id = role_subcloud_rsrc.subcloud_resource_id original_role_ref = RoleReferenceWrapper(id=role_id) # Delete the role in the subcloud self.sc_ks_client.projects.delete(original_role_ref) # Master Resource can be deleted only when all subcloud resources # are deleted along with corresponding orch_job and orch_requests. LOG.info("Keystone project {}:{} [{}] deleted" .format(rsrc.id, role_subcloud_rsrc.id, role_subcloud_rsrc.subcloud_resource_id), extra=self.log_extra) role_subcloud_rsrc.delete() def put_project_role_assignments(self, request, rsrc): # Assign this role to user on project on this subcloud resource_tags = rsrc.master_id.split('_') if len(resource_tags) < 3: LOG.error("Malformed resource tag {} expected to be in " "format: ProjectID_UserID_RoleID." .format(rsrc.id), extra=self.log_extra) raise exceptions.SyncRequestFailed project_id = resource_tags[0] user_id = resource_tags[1] role_id = resource_tags[2] project_name = self.m_ks_client.projects.get(project_id).name user_name = self.m_ks_client.users.get(user_id).name role_name = self.m_ks_client.roles.get(role_id).name # Ensure that we have already synced the project, user and role # prior to syncing the assignment sc_role = None sc_role_list = self.sc_ks_client.roles.list() for role in sc_role_list: if role.name == role_name: sc_role = role break if not sc_role: LOG.error("Unable to assign role to user on project reference {}:" "{}, cannot find equivalent Keystone Role in subcloud." .format(rsrc, role_name), extra=self.log_extra) raise exceptions.SyncRequestFailed sc_proj = None sc_proj_list = self.sc_ks_client.projects.list() for proj in sc_proj_list: if proj.name == project_name: sc_proj = proj break if not sc_proj: LOG.error("Unable to assign role to user on project reference {}:" "{}, cannot find equivalent Keystone Project in subcloud" .format(rsrc, project_name), extra=self.log_extra) raise exceptions.SyncRequestFailed sc_user = None sc_user_list = self.sc_ks_client.users.list() for user in sc_user_list: if user.name == user_name: sc_user = user break if not sc_user: LOG.error("Unable to assign role to user on project reference {}:" "{}, cannot find equivalent Keystone User in subcloud." .format(rsrc, user_name), extra=self.log_extra) raise exceptions.SyncRequestFailed # Create role assignment self.sc_ks_client.roles.grant( sc_role, user=sc_user, project=sc_proj) role_ref = self.sc_ks_client.role_assignments.list( user=sc_user, project=sc_proj, role=sc_role) if role_ref: LOG.info("Added Keystone role assignment: {}:{}" .format(rsrc.id, role_ref), extra=self.log_extra) # Persist the subcloud resource. sc_rid = sc_proj.id + '_' + sc_user.id + '_' + sc_role.id subcloud_rsrc_id = self.persist_db_subcloud_resource(rsrc.id, sc_rid) LOG.info("Created Keystone role assignment {}:{} [{}]" .format(rsrc.id, subcloud_rsrc_id, sc_rid), extra=self.log_extra) else: LOG.error("Unable to update Keystone role assignment {}:{} " .format(rsrc.id, sc_role), extra=self.log_extra) def delete_project_role_assignments(self, request, rsrc): # Revoke this role for user on project on this subcloud # Ensure that we have already synced the project, user and role # prior to syncing the assignment assignment_subcloud_rsrc = self.get_db_subcloud_resource(rsrc.id) if not assignment_subcloud_rsrc: LOG.error("Unable to delete assignment {}, " "cannot find Keystone Role Assignment in subcloud." .format(rsrc), extra=self.log_extra) return # resource_id is in format: # projectId_userId_roleId subcloud_rid = assignment_subcloud_rsrc.subcloud_resource_id resource_tags = subcloud_rid.split('_') if len(resource_tags) < 3: LOG.error("Malformed subcloud resource tag {} expected to be in " "format: ProjectID_UserID_RoleID." .format(assignment_subcloud_rsrc), extra=self.log_extra) assignment_subcloud_rsrc.delete() return project_id = resource_tags[0] user_id = resource_tags[1] role_id = resource_tags[2] # Revoke role assignment self.sc_ks_client.roles.revoke( role_id, user=user_id, project=project_id) role_ref = self.sc_ks_client.role_assignments.list( user=user_id, project=project_id, role=role_id) if (not role_ref): LOG.info("Deleted Keystone role assignment: {}:{}" .format(rsrc.id, assignment_subcloud_rsrc), extra=self.log_extra) else: LOG.error("Unable to delete Keystone role assignment {}:{} " .format(rsrc.id, role_id), extra=self.log_extra) assignment_subcloud_rsrc.delete() # ---- Override common audit functions ---- def _get_resource_audit_handler(self, resource_type, client): if resource_type == consts.RESOURCE_TYPE_IDENTITY_USERS: return self._get_users_resource(client) elif resource_type == consts.RESOURCE_TYPE_IDENTITY_ROLES: return self._get_roles_resource(client) elif resource_type == consts.RESOURCE_TYPE_IDENTITY_PROJECTS: return self._get_projects_resource(client) elif (resource_type == consts.RESOURCE_TYPE_IDENTITY_PROJECT_ROLE_ASSIGNMENTS): return self._get_assignments_resource(client) else: LOG.error("Wrong resource type {}".format(resource_type), extra=self.log_extra) return None def _get_users_resource(self, client): try: users = client.users.list() # NOTE (knasim-wrs): We need to filter out services users, # as some of these users may be for optional services # (such as Magnum, Murano etc) which will be picked up by # the Sync Audit and created on subclouds, later when these # optional services are enabled on the subcloud services = client.services.list() filtered_list = self.filtered_audit_resources[ consts.RESOURCE_TYPE_IDENTITY_USERS] filtered_users = [user for user in users if (all(user.name != service.name for service in services) and all(user.name != filtered for filtered in filtered_list))] return filtered_users except (keystone_exceptions.connection.ConnectTimeout, keystone_exceptions.ConnectFailure) as e: LOG.info("User Audit: subcloud {} is not reachable [{}]" .format(self.subcloud_engine.subcloud.region_name, str(e)), extra=self.log_extra) # None will force skip of audit return None except Exception as e: LOG.exception(e) return None def _get_roles_resource(self, client): try: roles = client.roles.list() # Filter out system roles filtered_list = self.filtered_audit_resources[ consts.RESOURCE_TYPE_IDENTITY_ROLES] filtered_roles = [role for role in roles if (all(role.name != filtered for filtered in filtered_list))] return filtered_roles except (keystone_exceptions.connection.ConnectTimeout, keystone_exceptions.ConnectFailure) as e: LOG.info("Role Audit: subcloud {} is not reachable [{}]" .format(self.subcloud_engine.subcloud.region_name, str(e)), extra=self.log_extra) # None will force skip of audit return None except Exception as e: LOG.exception(e) return None def _get_projects_resource(self, client): try: projects = client.projects.list() # Filter out admin or services projects filtered_list = self.filtered_audit_resources[ consts.RESOURCE_TYPE_IDENTITY_PROJECTS] filtered_projects = [project for project in projects if all(project.name != filtered for filtered in filtered_list)] return filtered_projects except (keystone_exceptions.connection.ConnectTimeout, keystone_exceptions.ConnectFailure) as e: LOG.info("Project Audit: subcloud {} is not reachable [{}]" .format(self.subcloud_engine.subcloud.region_name, str(e)), extra=self.log_extra) # None will force skip of audit return None except Exception as e: LOG.exception(e) return None def _get_assignments_resource(self, client): try: refactored_assignments = [] # An assignment will only contain scope information, # i.e. the IDs for the Role, the User and the Project. # We need to furnish additional information such a # role, project and user names assignments = client.role_assignments.list() roles = self._get_roles_resource(client) projects = self._get_projects_resource(client) users = self._get_users_resource(client) for assignment in assignments: if 'project' not in assignment.scope: # this is a domain scoped role, we don't care # about syncing or auditing them for now continue role_id = assignment.role['id'] user_id = assignment.user['id'] project_id = assignment.scope['project']['id'] assignment_dict = {} for user in users: if user.id == user_id: assignment_dict['user'] = user break else: continue for role in roles: if role.id == role_id: assignment_dict['role'] = role break else: continue for project in projects: if project.id == project_id: assignment_dict['project'] = project break else: continue # The id of a Role Assigment is: # projectID_userID_roleID assignment_dict['id'] = "{}_{}_{}".format( project_id, user_id, role_id) # Build an opaque object wrapper for this RoleAssignment refactored_assignment = namedtuple( 'RoleAssignmentWrapper', assignment_dict.keys())(*assignment_dict.values()) refactored_assignments.append(refactored_assignment) return refactored_assignments except (keystone_exceptions.connection.ConnectTimeout, keystone_exceptions.ConnectFailure) as e: LOG.info("Assignment Audit: subcloud {} is not reachable [{}]" .format(self.subcloud_engine.subcloud.region_name, str(e)), extra=self.log_extra) # None will force skip of audit return None except Exception as e: LOG.exception(e) return None def _same_identity_resource(self, m, sc): LOG.debug("master={}, subcloud={}".format(m, sc), extra=self.log_extra) # Any Keystone resource can be system wide or domain scoped, # If the domains are different then these resources # are instantly unique since the same resource name can be # mapped in different domains return (m.name == sc.name and m.domain_id == sc.domain_id) def _same_assignment_resource(self, m, sc): LOG.debug("same_assignment master={}, subcloud={}".format(m, sc), extra=self.log_extra) # For an assignment to be the same, all 3 of its role, project and # user information must match up is_same = (self._same_identity_resource(m.user, sc.user) and self._same_identity_resource(m.role, sc.role) and self._same_identity_resource(m.project, sc.project)) return is_same def get_master_resources(self, resource_type): return self._get_resource_audit_handler(resource_type, self.m_ks_client) def get_subcloud_resources(self, resource_type): self.initialize_sc_clients() return self._get_resource_audit_handler(resource_type, self.sc_ks_client) def same_resource(self, resource_type, m_resource, sc_resource): if (resource_type == consts.RESOURCE_TYPE_IDENTITY_PROJECT_ROLE_ASSIGNMENTS): return self._same_assignment_resource(m_resource, sc_resource) else: return self._same_identity_resource(m_resource, sc_resource) def get_resource_id(self, resource_type, resource): if hasattr(resource, 'master_id'): # If resource from DB, return master resource id # from master cloud return resource.master_id # Else, it is OpenStack resource retrieved from master cloud return resource.id def get_resource_info(self, resource_type, resource, operation_type=None): rtype = consts.RESOURCE_TYPE_IDENTITY_PROJECT_ROLE_ASSIGNMENTS if (operation_type == consts.OPERATION_TYPE_CREATE and resource_type != rtype): # With the exception of role assignments, for all create # requests the resource_info needs to be extracted # from the master resource return jsonutils.dumps(resource._info) else: super(IdentitySyncThread, self).get_resource_info( resource_type, resource, operation_type) def audit_discrepancy(self, resource_type, m_resource, sc_resources): # It could be that the details are different # between master cloud and subcloud now. # Thus, delete the resource before creating it again. self.schedule_work(self.endpoint_type, resource_type, self.get_resource_id(resource_type, m_resource), consts.OPERATION_TYPE_DELETE) # Return true to try creating the resource again return True def map_subcloud_resource(self, resource_type, m_r, m_rsrc_db, sc_resources): # Map an existing subcloud resource to an existing master resource. # If a mapping is created the function should return True. # Need to do this for all Identity resources (users, roles, projects) # as common resources would be created by application of the Puppet # manifest on the Subclouds and the Central Region should not try # to create these on the subclouds for sc_r in sc_resources: if self.same_resource(resource_type, m_r, sc_r): LOG.info( "Mapping resource {} to existing subcloud resource {}" .format(m_r, sc_r), extra=self.log_extra) self.persist_db_subcloud_resource(m_rsrc_db.id, self.get_resource_id( resource_type, sc_r)) return True return False