95da742946
There is a typo error in role deletion that prevents deleting the role in subcloud during role synchronization. This update fixed this typo error and made role synchronization work. Closes-Bug: 1797960 Change-Id: Iff78ceffdd95b2676854d986126c6c2d001866de Signed-off-by: Andy Ning <andy.ning@windriver.com>
863 lines
38 KiB
Python
863 lines
38 KiB
Python
# 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.roles.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 role {}:{} [{}] 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
|