Files
distcloud/distributedcloud/dccommon/endpoint_cache.py
Hugo Brito c2c7ab93ef Update endpoint caches post network reconfig
After switching the management network to the admin network
we need to update the services endpoints cache of all
dcmanager, dcorch and cert-mon workers to the new admin value.

The fanout parameter is being added to the cast calls of the
RPC clients (dcorch and audit), this parameter makes the method
cast to all servers listening on a topic rather than just one
of them.

Test Plan:
PASS: dcmanager subcloud update (using admin network parameters)
1. Endpoints for the subcloud updated with admin ip value
2. subcloud availability = online
PASS: Verify that the subcloud is online shortly after succesful
completion of subcloud_update playbook
PASS: Verify that the service endpoints are updated in all workers'
endpoint caches for the subcloud
PASS: Manage the subcloud and verify that both dcmanager and dcorch
audits are working as expected
PASS: Perform a Identity sync:
1. openstack --os-region-name SystemController user create <new_user>
--domain <domain> --project <project> --password <password>
2. Log in into subcloud and verify the new user: openstack user list
PASS: Verify that the master token is refreshed successfully after an hour

Story: 2010319
Task: 47556

Depends-On: https://review.opendev.org/c/starlingx/config/+/877323

Signed-off-by: Hugo Brito <hugo.brito@windriver.com>
Change-Id: I149c864382b7c63d424f736bdb4eaac2a787b709
2023-03-14 11:44:15 -03:00

264 lines
11 KiB
Python

# Copyright 2015 Huawei Technologies Co., Ltd.
# Copyright (c) 2018-2023 Wind River Systems, Inc.
# All Rights Reserved
#
# 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 collections
import threading
from keystoneauth1 import loading
from keystoneauth1 import session
from keystoneclient.v3 import client as ks_client
from oslo_concurrency import lockutils
from oslo_config import cfg
from oslo_log import log as logging
from dccommon import consts
from dccommon.utils import is_token_expiring_soon
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
LOCK_NAME = 'dc-keystone-endpoint-cache'
class EndpointCache(object):
plugin_loader = None
plugin_lock = threading.Lock()
master_keystone_client = None
master_token = {}
master_services_list = None
master_service_endpoint_map = collections.defaultdict(dict)
def __init__(self, region_name=None, auth_url=None):
# Region specific service endpoint map
self.service_endpoint_map = collections.defaultdict(dict)
self.admin_session = None
self.keystone_client = None
# if auth_url is provided use that otherwise use the one
# defined in the config
if auth_url:
self.external_auth_url = auth_url
else:
self.external_auth_url = CONF.endpoint_cache.auth_uri
self._initialize_keystone_client(region_name, auth_url)
def _initialize_keystone_client(self, region_name=None, auth_url=None):
self.admin_session = EndpointCache.get_admin_session(
self.external_auth_url,
CONF.endpoint_cache.username,
CONF.endpoint_cache.user_domain_name,
CONF.endpoint_cache.password,
CONF.endpoint_cache.project_name,
CONF.endpoint_cache.project_domain_name)
self.keystone_client, self.service_endpoint_map = \
self.get_cached_master_keystone_client_and_region_endpoint_map(region_name)
# if Endpoint cache is intended for a subcloud then
# we need to retrieve the subcloud token and session.
# Skip this if auth_url was provided as its assumed that the
# auth_url would correspond to a subcloud so session was
# set up above
if (not auth_url and region_name and
region_name not in
[consts.CLOUD_0, consts.VIRTUAL_MASTER_CLOUD]):
try:
sc_auth_url = self.service_endpoint_map['keystone']
except KeyError:
# Should not be here...
LOG.exception("Endpoint not found for region_name = %s. "
"Refreshing cached data..." % region_name)
self.re_initialize_master_keystone_client()
raise
# We assume that the dcmanager user names and passwords are the
# same on this subcloud since this is an audited resource
self.admin_session = EndpointCache.get_admin_session(
sc_auth_url,
CONF.endpoint_cache.username,
CONF.endpoint_cache.user_domain_name,
CONF.endpoint_cache.password,
CONF.endpoint_cache.project_name,
CONF.endpoint_cache.project_domain_name)
try:
self.keystone_client = ks_client.Client(
session=self.admin_session,
region_name=region_name)
except Exception:
LOG.error("Retrying keystone client creation for %s" % region_name)
self.keystone_client = ks_client.Client(
session=self.admin_session,
region_name=region_name)
self.external_auth_url = sc_auth_url
@classmethod
def get_admin_session(cls, auth_url, user_name, user_domain_name,
user_password, user_project, user_project_domain,
timeout=None):
with EndpointCache.plugin_lock:
if EndpointCache.plugin_loader is None:
EndpointCache.plugin_loader = loading.get_plugin_loader(
CONF.endpoint_cache.auth_plugin)
user_auth = EndpointCache.plugin_loader.load_from_options(
auth_url=auth_url,
username=user_name,
user_domain_name=user_domain_name,
password=user_password,
project_name=user_project,
project_domain_name=user_project_domain,
)
timeout = (CONF.endpoint_cache.http_connect_timeout if timeout is None
else timeout)
return session.Session(
auth=user_auth, additional_headers=consts.USER_HEADER,
timeout=timeout)
@classmethod
def get_master_services_list(cls):
return EndpointCache.master_services_list
@staticmethod
def _is_central_cloud(region_id):
central_cloud_regions = [consts.CLOUD_0, consts.VIRTUAL_MASTER_CLOUD]
return region_id in central_cloud_regions
@staticmethod
def _generate_master_service_endpoint_map(self):
master_endpoints_list = EndpointCache.master_keystone_client.endpoints.list()
service_id_name_map = {}
for service in EndpointCache.master_services_list: # pylint: disable=not-an-iterable
service_dict = service.to_dict()
service_id_name_map[service_dict['id']] = service_dict['name']
service_endpoint_map = {}
for endpoint in master_endpoints_list:
endpoint_dict = endpoint.to_dict()
region_id = endpoint_dict['region']
# within central cloud, use internal endpoints
if EndpointCache._is_central_cloud(region_id):
if endpoint_dict['interface'] != consts.KS_ENDPOINT_INTERNAL:
continue
# Otherwise should always use admin endpoints
elif endpoint_dict['interface'] != consts.KS_ENDPOINT_ADMIN:
continue
service_id = endpoint_dict['service_id']
url = endpoint_dict['url']
service_name = service_id_name_map[service_id]
if region_id not in service_endpoint_map:
service_endpoint_map[region_id] = {}
service_endpoint_map[region_id][service_name] = url
return service_endpoint_map
def get_endpoint(self, service):
"""Get the endpoint for the specified service.
return: service url or None
"""
try:
endpoint = self.service_endpoint_map[service]
except KeyError:
LOG.error("Unknown service: %s" % service)
endpoint = None
return endpoint
@lockutils.synchronized(LOCK_NAME)
def get_all_regions(self):
"""Get region list.
return: List of regions
"""
return list(EndpointCache.master_service_endpoint_map.keys())
def get_session_from_token(self, token, project_id):
"""Get session based on token to communicate with openstack services.
:param token: token with which the request is triggered.
:param project_id: UUID of the project.
:return: session object.
"""
loader = loading.get_plugin_loader('token')
auth = loader.load_from_options(auth_url=self.external_auth_url,
token=token, project_id=project_id)
sess = session.Session(auth=auth)
return sess
@lockutils.synchronized(LOCK_NAME)
def update_master_service_endpoint_region(self, region_name, endpoint_values):
EndpointCache.master_service_endpoint_map[region_name] = endpoint_values
@lockutils.synchronized(LOCK_NAME)
def get_cached_master_keystone_client_and_region_endpoint_map(self, region_name):
if (EndpointCache.master_keystone_client is None):
self._create_master_cached_data()
LOG.info("Generated Master keystone client and master token the "
"very first time")
else:
token_expiring_soon = is_token_expiring_soon(token=EndpointCache.master_token)
# If token is expiring soon, initialize a new master keystone
# client
if token_expiring_soon:
LOG.info("The cached keystone token for %s "
"will expire soon %s" %
(consts.CLOUD_0, EndpointCache.master_token['expires_at']))
self._create_master_cached_data()
LOG.info("Generated Master keystone client and master token as they are expiring soon")
else:
# Check if the cached master service endpoint map needs to be refreshed
if region_name not in self.master_service_endpoint_map:
previous_size = len(EndpointCache.master_service_endpoint_map)
EndpointCache.master_service_endpoint_map = self._generate_master_service_endpoint_map(self)
current_size = len(EndpointCache.master_service_endpoint_map)
LOG.info("Master endpoints list refreshed to include region %s: "
"prev_size=%d, current_size=%d" % (region_name, previous_size, current_size))
# TODO(clientsession)
if region_name is not None:
region_service_endpoint_map = EndpointCache.master_service_endpoint_map[region_name]
else:
region_service_endpoint_map = collections.defaultdict(dict)
return (EndpointCache.master_keystone_client, region_service_endpoint_map)
@lockutils.synchronized(LOCK_NAME)
def re_initialize_master_keystone_client(self):
self._create_master_cached_data()
LOG.info("Generated Master keystone client and master token upon exception")
def _create_master_cached_data(self):
EndpointCache.master_keystone_client = ks_client.Client(
session=self.admin_session,
region_name=consts.CLOUD_0)
EndpointCache.master_token = EndpointCache.master_keystone_client.tokens.validate(
EndpointCache.master_keystone_client.session.get_token(),
include_catalog=False)
if EndpointCache.master_services_list is None:
EndpointCache.master_services_list = EndpointCache.master_keystone_client.services.list()
EndpointCache.master_service_endpoint_map = self._generate_master_service_endpoint_map(self)