Files
distcloud/distributedcloud/dccommon/drivers/openstack/sdk_platform.py
Gustavo Herzmann 6435d6c357 Dynamically retrieve the region one name from configuration
This commit removes the hardcoded "RegionOne" region name and instead
retrieves the region name dynamically from the service configuration.

This change prepares for a future update where DC services will be
deployed on a standalone system that uses a UUID as the default region
name.

Test Plan:
01. PASS - Add a subcloud.
02. PASS - Manage and unmanage a subcloud.
03. PASS - List and show subcloud details using subcloud list and
    subcloud show --detail.
04. PASS - Delete a subcloud.
05. PASS - Run 'dcmanager strategy-config update' using different
    region names: "RegionOne", "SystemController", and without
    specifying a region name. Verify that the default options are
    modified accordingly.
06. PASS - Run the previous test but using 'dcmanager strategy-config
    show' instead.
07. PASS - Upload a patch using the dcorch proxy (--os-region-name
    SystemController).
08. PASS - Run prestage orchestration.
09. PASS - Apply a patch to the system controller and then to the
    subclouds
10. PASS - Review all dcmanager and dcorch logs to ensure no
    exceptions are raised.

Story: 2011312
Task: 51861

Change-Id: I85c93c865c40418a351dab28aac56fc08464af72
Signed-off-by: Gustavo Herzmann <gustavo.herzmann@windriver.com>
2025-03-31 12:53:15 -03:00

438 lines
16 KiB
Python

# Copyright 2017-2025 Wind River Inc
# 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.
"""
OpenStack Driver
"""
import random
import time
import collections
from typing import Callable
from typing import List
from keystoneauth1 import exceptions as keystone_exceptions
from oslo_concurrency import lockutils
from oslo_log import log
from dccommon import consts
from dccommon.drivers.openstack.barbican import BarbicanClient
from dccommon.drivers.openstack.fm import FmClient
from dccommon.drivers.openstack.keystone_v3 import KeystoneClient
from dccommon.drivers.openstack.sysinv_v1 import SysinvClient
from dccommon.endpoint_cache import EndpointCache
from dccommon import exceptions
from dccommon import utils
from dcdbsync.dbsyncclient.client import Client as dbsyncclient
KEYSTONE_CLIENT_NAME = "keystone"
SYSINV_CLIENT_NAME = "sysinv"
FM_CLIENT_NAME = "fm"
BARBICAN_CLIENT_NAME = "barbican"
DBSYNC_CLIENT_NAME = "dbsync"
LOG = log.getLogger(__name__)
LOCK_NAME = "dc-openstackdriver-platform"
SUPPORTED_REGION_CLIENTS = (
SYSINV_CLIENT_NAME,
FM_CLIENT_NAME,
BARBICAN_CLIENT_NAME,
DBSYNC_CLIENT_NAME,
)
# Region client type and class mappings
region_client_class_map = {
SYSINV_CLIENT_NAME: SysinvClient,
FM_CLIENT_NAME: FmClient,
BARBICAN_CLIENT_NAME: BarbicanClient,
DBSYNC_CLIENT_NAME: dbsyncclient,
}
class OpenStackDriver(object):
"""An OpenStack driver for managing external services clients.
:param region_name: The name of the region. Defaults to the local region
:type region_name: str
:param thread_name: The name of the thread. Defaults to "dc".
:type thread_name: str
:param auth_url: The authentication URL.
:type auth_url: str
:param region_clients: The list of region clients to initialize.
:type region_clients: list
:param endpoint_type: The type of endpoint. Defaults to "admin".
:type endpoint_type: str
:param fetch_subcloud_ips: A function to fetch subcloud management IPs.
:type fetch_subcloud_ips: Callable
:param subcloud_management_ip: The subcloud management IP. If passed and
the region_name is associated with a subcloud, updates the cache with the
provided IP.
:type subcloud_management_ip: str
:param attempts: The maximum number of times allowed when trying to initialize
the Keystone client. It controls how many times the client
will attempt to reconnect or reauthenticate in case of
a failure, before aborting the process.
:type attempts: int
"""
os_clients_dict = collections.defaultdict(dict)
_identity_tokens = {}
def __init__(
self,
region_name: str = None,
thread_name: str = "dc",
auth_url: str = None,
region_clients: List[str] = SUPPORTED_REGION_CLIENTS,
endpoint_type: str = consts.KS_ENDPOINT_DEFAULT,
fetch_subcloud_ips: Callable = None,
subcloud_management_ip: str = None,
attempts: int = 3,
):
if not region_name:
region_name = utils.get_region_one_name()
self.region_name = region_name
self.keystone_client = None
# These clients are created dynamically by initialize_region_clients
self.sysinv_client = None
self.fm_client = None
self.barbican_client = None
self.dbsync_client = None
# Update the endpoint cache for the subcloud with the specified IP
if subcloud_management_ip and region_name != utils.get_region_one_name():
# Check if the IP is different from the one already cached
endpoint_map = EndpointCache.master_service_endpoint_map.get(region_name)
if endpoint_map:
endpoint = next(iter(endpoint_map.values()))
if subcloud_management_ip not in endpoint:
EndpointCache.update_subcloud_endpoint_cache_by_ip(
region_name, subcloud_management_ip
)
self.get_cached_keystone_client(
region_name, auth_url, fetch_subcloud_ips, attempts
)
if self.keystone_client is None:
self.initialize_keystone_client(auth_url, fetch_subcloud_ips, attempts)
OpenStackDriver.update_region_clients_cache(
region_name, KEYSTONE_CLIENT_NAME, self.keystone_client
)
# Clear client object cache
if region_name != utils.get_region_one_name():
OpenStackDriver.os_clients_dict[region_name] = collections.defaultdict(
dict
)
if region_clients:
self.initialize_region_clients(region_clients, thread_name, endpoint_type)
def initialize_region_clients(
self, region_clients: List[str], thread_name: str, endpoint_type: str
) -> None:
"""Initialize region clients dynamically setting them as attributes
:param region_clients: The list of region clients to initialize.
:type region_clients: list
:param thread_name: The name of the thread.
:type thread_name: str
:param endpoint_type: The type of endpoint.
:type endpoint_type: str
"""
self.get_cached_region_clients_for_thread(
self.region_name, thread_name, region_clients
)
for client_name in region_clients:
client_obj_name = f"{client_name}_client"
# If the client object already exists, do nothing
if getattr(self, client_obj_name, None) is not None:
continue
# Create new client object and cache it
try:
try:
client_class = region_client_class_map[client_name]
except KeyError as e:
msg = f"Requested region client is not supported: {client_name}"
LOG.error(msg)
raise exceptions.InvalidInputError from e
args = {
"region": self.region_name,
"session": self.keystone_client.session,
"endpoint_type": endpoint_type,
}
# Since SysinvClient (cgtsclient) does not support session,
# also pass the cached endpoint so it does not need to
# retrieve it from keystone.
if client_name == "sysinv":
args["endpoint"] = self.keystone_client.endpoint_cache.get_endpoint(
"sysinv"
)
client_object = client_class(**args)
# Store the new client
setattr(self, client_obj_name, client_object)
OpenStackDriver.update_region_clients_cache(
self.region_name, client_name, client_object, thread_name
)
except Exception as exception:
LOG.error(
f"Region {self.region_name} client {client_name} "
f"thread {thread_name} error: {str(exception)}"
)
raise exception
def initialize_keystone_client(
self, auth_url: str, fetch_subcloud_ips: Callable, attempts: int = 3
) -> None:
"""Initialize a new Keystone client.
:param auth_url: The authentication URL.
:type auth_url: str
:param fetch_subcloud_ips: A function to fetch subcloud management IPs.
:type fetch_subcloud_ips: Callable
:param attempts: The maximum number of times allowed when trying to
initialize the Keystone client. It controls how many
times the client will attempt to reconnect or
reauthenticate in case of a failure, before aborting
the process.
:type attempts: int
"""
LOG.debug(f"get new keystone client for region {self.region_name}")
for attempt in range(attempts):
try:
self.keystone_client = KeystoneClient(
self.region_name, auth_url, fetch_subcloud_ips
)
# Exit loop if no exception occurs
break
except (
keystone_exceptions.NotFound,
keystone_exceptions.ConnectTimeout,
) as exception:
if attempt < attempts - 1:
# The interval between each attempt will be 1 to 4 seconds,
# with a random delay varying by at least 10 milliseconds.
delay = random.randint(100, 400) / 100.0
LOG.info(
f"Retry {attempt + 1}/{attempts - 1} for ConnectTimeout "
f"or NotFound in region {self.region_name} in {delay} "
f"seconds"
)
time.sleep(delay)
else:
LOG.debug(
f"keystone_client region {self.region_name} error: "
f"{str(exception)}"
)
raise exception
except (
keystone_exceptions.ConnectFailure,
keystone_exceptions.ServiceUnavailable,
) as exception:
LOG.error(
f"keystone_client region {self.region_name} error: "
f"{str(exception)}"
)
raise exception
except Exception as exception:
LOG.exception(
f"Unable to get a new keystone client for region: "
f"{self.region_name}"
)
raise exception
@lockutils.synchronized(LOCK_NAME)
def get_cached_keystone_client(
self,
region_name: str,
auth_url: str,
fetch_subcloud_ips: Callable,
attempts: int = 3,
) -> None:
"""Get the cached Keystone client if it exists
:param region_name: The name of the region.
:type region_name: str
:param auth_url: The authentication URL.
:type auth_url: str
:param fetch_subcloud_ips: A function to fetch subcloud management IPs.
:type fetch_subcloud_ips: Callable
:param attempts: The maximum number of times allowed when trying to
initialize the Keystone client. It controls how many times
the client will attempt to reconnect or reauthenticate
in case of a failure, before aborting the process.
:type attempts: int
"""
os_clients_dict = OpenStackDriver.os_clients_dict
keystone_client = os_clients_dict.get(region_name, {}).get(KEYSTONE_CLIENT_NAME)
# If there's a cached keystone client and the token is valid, use it
if keystone_client and self._is_token_valid(region_name):
self.keystone_client = keystone_client
# Else if master region, create a new keystone client
elif region_name in utils.get_system_controller_region_names():
self.initialize_keystone_client(auth_url, fetch_subcloud_ips, attempts)
os_clients_dict[region_name][KEYSTONE_CLIENT_NAME] = self.keystone_client
@lockutils.synchronized(LOCK_NAME)
def get_cached_region_clients_for_thread(
self, region_name: str, thread_name: str, clients: List[str]
) -> None:
"""Get and assign the cached region clients as object attributes.
Also initializes the os_clients_dict region and
thread dictionaries if they don't already exist.
:param region_name: The name of the region.
:type region_name: str
:param thread_name: The name of the thread.
:type thread_name: str
:param clients: The list of client names.
:type clients: list
"""
os_clients = OpenStackDriver.os_clients_dict
for client in clients:
client_obj = (
os_clients.setdefault(region_name, {})
.setdefault(thread_name, {})
.get(client)
)
if client_obj is not None:
LOG.debug(
f"Using cached OS {client} client objects "
f"{region_name} {thread_name}"
)
setattr(self, f"{client}_client", client_obj)
@classmethod
@lockutils.synchronized(LOCK_NAME)
def update_region_clients_cache(
cls,
region_name: str,
client_name: str,
client_object: object,
thread_name: str = None,
) -> None:
"""Update the region clients cache.
:param region_name: The name of the region.
:type region_name: str
:param client_name: The name of the client.
:type client_name: str
:param client_object: The client object.
:param thread_name: The name of the thread. Defaults to None.
:type thread_name: str
"""
region_dict = cls.os_clients_dict[region_name]
if thread_name is None:
region_dict[client_name] = client_object
else:
region_dict[thread_name][client_name] = client_object
@classmethod
@lockutils.synchronized(LOCK_NAME)
def delete_region_clients(cls, region_name: str, clear_token: bool = False) -> None:
"""Delete region clients from cache.
:param region_name: The name of the region.
:type region_name: str
:param clear_token: Whether to clear the token cache. Defaults to False.
:type clear_token: bool
"""
LOG.warn(f"delete_region_clients={region_name}, clear_token={clear_token}")
try:
del cls.os_clients_dict[region_name]
except KeyError:
pass
if clear_token:
cls._identity_tokens[region_name] = None
@classmethod
@lockutils.synchronized(LOCK_NAME)
def delete_region_clients_for_thread(
cls, region_name: str, thread_name: str
) -> None:
"""Delete region clients for a specific thread from cache.
:param region_name: The name of the region.
:type region_name: str
:param thread_name: The name of the thread.
:type thread_name: str
"""
LOG.debug(f"delete_region_clients={region_name}, thread_name={thread_name}")
try:
del cls.os_clients_dict[region_name][thread_name]
except KeyError:
pass
@staticmethod
def _reset_cached_clients_and_token(region_name: str) -> None:
OpenStackDriver.os_clients_dict[region_name] = collections.defaultdict(dict)
OpenStackDriver._identity_tokens[region_name] = None
def _is_token_valid(self, region_name: str) -> bool:
"""Check if the cached token is valid.
:param region_name: The name of the region.
:type region_name: str
"""
cached_os_clients = OpenStackDriver.os_clients_dict
# If the token is not cached, validate the session token and cache it
try:
keystone = cached_os_clients[region_name]["keystone"].keystone_client
cached_tokens = OpenStackDriver._identity_tokens
if not cached_tokens.get(region_name):
cached_tokens[region_name] = keystone.tokens.validate(
keystone.session.get_token(), include_catalog=False
)
LOG.info(
f"Token for subcloud {region_name} expires_at="
f"{cached_tokens[region_name]['expires_at']}"
)
except Exception as exception:
LOG.info(
f"_is_token_valid handle: region: {region_name} "
f"error: {str(exception)}"
)
self._reset_cached_clients_and_token(region_name)
return False
# If token is expiring soon, reset cached data and return False.
if utils.is_token_expiring_soon(token=cached_tokens[region_name]):
LOG.info(
f"The cached keystone token for subcloud {region_name} will "
f"expire soon {cached_tokens[region_name]['expires_at']}"
)
# Reset the cached dictionary
self._reset_cached_clients_and_token(region_name)
return False
return True