Skip to content

Commit b104137

Browse files
authored
Merge pull request #266 from stackhpc/upstream/master-2026-05-04
Synchronise master with upstream
2 parents 4d12190 + cb11e56 commit b104137

14 files changed

Lines changed: 255 additions & 31 deletions

File tree

doc/source/admin/troubleshooting-guide.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ management, therefore if it fails to start, these other components
458458
will not be running correctly either.
459459
Check that etcd is running on the master nodes by::
460460

461-
sudo service etcd status -l
461+
sudo systemctl status etcd
462462

463463
If it is running correctly, you should see that the service is
464464
successfully deployed::
@@ -484,7 +484,7 @@ something like::
484484

485485
In this case, try restarting etcd by::
486486

487-
sudo service etcd start
487+
sudo systemctl start etcd
488488

489489
If etcd continues to fail, check the following:
490490

@@ -534,7 +534,7 @@ confirmed by running *ping* or *curl* from one container to another.
534534
The Flannel daemon is run as a systemd service on each node of the cluster.
535535
To check Flannel, run on each node::
536536

537-
sudo service flanneld status
537+
sudo systemctl status flanneld
538538

539539
If the daemon is running, you should see that the service is successfully
540540
deployed::
@@ -562,7 +562,7 @@ Check the following:
562562
If the etcd service failed, once it has been restored successfully, the
563563
Flannel service can be restarted by::
564564

565-
sudo service flanneld restart
565+
sudo systemctl restart flanneld
566566

567567
- Magnum writes the configuration for Flannel in a local file on each master
568568
node. Check for this file on the master nodes by::

magnum/api/controllers/v1/cluster.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,24 @@ def __init__(self, **kwargs):
292292
def convert_with_links(rpc_clusters, limit, url=None, expand=False,
293293
**kwargs):
294294
collection = ClusterCollection()
295+
# Pre-fetch all unique ClusterTemplates needed for this page in one
296+
# batch to avoid N separate ClusterTemplate.get_by_uuid() RPC calls
297+
# (one per cluster). Clusters in a project commonly share the same
298+
# template, so this usually collapses to a single RPC regardless of
299+
# page size.
300+
template_cache = {}
301+
for rpc_cluster in rpc_clusters:
302+
tid = rpc_cluster.cluster_template_id
303+
if tid and tid not in template_cache:
304+
template_cache[tid] = objects.ClusterTemplate.get_by_uuid(
305+
pecan.request.context, tid)
306+
# Inject the pre-fetched template so obj_load_attr is never triggered
307+
# during Cluster.convert_with_links below.
308+
for rpc_cluster in rpc_clusters:
309+
tid = rpc_cluster.cluster_template_id
310+
if tid and tid in template_cache:
311+
rpc_cluster.cluster_template = template_cache[tid]
312+
295313
collection.clusters = [Cluster.convert_with_links(p, expand)
296314
for p in rpc_clusters]
297315
collection.next = collection.get_next(limit, url=url, **kwargs)
@@ -443,7 +461,9 @@ def get_one(self, cluster_ident):
443461
context.all_tenants = True
444462

445463
cluster = api_utils.get_resource('Cluster', cluster_ident)
446-
policy.enforce(context, 'cluster:get', cluster.as_dict(),
464+
# Compute as_dict() once and reuse it for policy enforcement.
465+
cluster_dict = cluster.as_dict()
466+
policy.enforce(context, 'cluster:get', cluster_dict,
447467
action='cluster:get')
448468

449469
api_cluster = Cluster.convert_with_links(cluster)

magnum/api/validation.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import pecan
1919

20+
from glanceclient import exc as glance_exception
2021
from keystoneauth1 import exceptions as ka_exception
2122

2223
from magnum.api import utils as api_utils
@@ -78,6 +79,10 @@ def wrapper(func, *args, **kwargs):
7879
'images')
7980
cluster_distro = image.get('os_distro')
8081
driver_name = image.get('magnum_driver')
82+
except (glance_exception.NotFound, exception.ResourceNotFound):
83+
raise exception.ImageNotFound(image_id=image_id)
84+
except glance_exception.HTTPForbidden:
85+
raise exception.ImageNotAuthorized(image_id=image_id)
8186
except Exception:
8287
pass
8388
cluster_type = (cluster_template.server_type,

magnum/cmd/api.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from magnum.common import profiler
2828
from magnum.common import service
2929
import magnum.conf
30+
from magnum.drivers.common import driver as driver_module
3031
from magnum.i18n import _
3132
from magnum.objects import base
3233
from magnum import version
@@ -78,6 +79,9 @@ def main():
7879
LOG.debug("Configuration:")
7980
CONF.log_opt_values(LOG, logging.DEBUG)
8081

82+
drivers = [ep.name for ep, _ in driver_module.Driver.load_entry_points()]
83+
LOG.debug('Loaded drivers: %s', drivers)
84+
8185
LOG.info('Serving on %(proto)s://%(host)s:%(port)s',
8286
dict(proto="https" if use_ssl else "http", host=host, port=port))
8387

magnum/cmd/conductor.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from magnum.conductor.handlers import indirection_api
3535
from magnum.conductor.handlers import nodegroup_conductor
3636
import magnum.conf
37+
from magnum.drivers.common import driver as driver_module
3738
from magnum import version
3839

3940
CONF = magnum.conf.CONF
@@ -50,6 +51,9 @@ def main():
5051
LOG.debug("Configuration:")
5152
CONF.log_opt_values(LOG, logging.DEBUG)
5253

54+
drivers = [ep.name for ep, _ in driver_module.Driver.load_entry_points()]
55+
LOG.debug('Loaded drivers: %s', drivers)
56+
5357
conductor_id = short_id.generate_id()
5458
endpoints = [
5559
indirection_api.Handler(),

magnum/common/policy.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@
3030
LOG = logging.getLogger(__name__)
3131
_ENFORCER = None
3232
CONF = cfg.CONF
33+
_TRUSTEE_DOMAIN_ID_CACHE = None
34+
35+
36+
def _reset_trustee_domain_id_cache():
37+
"""Reset the trustee_domain_id Keystone lookup cache.
38+
39+
Intended for use in test teardown only. In production the value is
40+
either read from CONF (free) or fetched once and cached for the process
41+
lifetime.
42+
"""
43+
global _TRUSTEE_DOMAIN_ID_CACHE
44+
_TRUSTEE_DOMAIN_ID_CACHE = None
3345

3446

3547
# we can get a policy enforcer by this init.
@@ -112,10 +124,21 @@ def enforce(context, rule=None, target=None,
112124

113125
def add_policy_attributes(target):
114126
"""Adds extra information for policy enforcement to raw target object"""
115-
context = importutils.import_module('magnum.common.context')
116-
admin_context = context.make_admin_context()
117-
admin_osc = clients.OpenStackClients(admin_context)
118-
trustee_domain_id = admin_osc.keystone().trustee_domain_id
127+
global _TRUSTEE_DOMAIN_ID_CACHE
128+
129+
# When trustee_domain_id is set - we don't need to do any operations
130+
trustee_domain_id = CONF.trust.trustee_domain_id
131+
if not trustee_domain_id:
132+
# Fallback for deployments that rely on auto-discovery via Keystone.
133+
# Cache the result for the process lifetime so the call happens
134+
# at most once.
135+
if _TRUSTEE_DOMAIN_ID_CACHE is None:
136+
ctx = importutils.import_module('magnum.common.context')
137+
admin_context = ctx.make_admin_context()
138+
admin_osc = clients.OpenStackClients(admin_context)
139+
_TRUSTEE_DOMAIN_ID_CACHE = admin_osc.keystone().trustee_domain_id
140+
trustee_domain_id = _TRUSTEE_DOMAIN_ID_CACHE
141+
119142
target['trustee_domain_id'] = trustee_domain_id
120143
return target
121144

magnum/common/rpc_service.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,46 @@ def create(cls, topic, server, handlers, binary):
6262
return service_obj
6363

6464

65+
# Share a single RPCClient per unique (topic, server, timeout) tuple
66+
# for the lifetime of the worker process. The per-request context is injected
67+
# via RPCClient.prepare(), which returns a lightweight _CallContext that reuses
68+
# the same underlying transport connections.
69+
_RPC_CLIENT_CACHE = {}
70+
71+
72+
def _get_cached_client(topic, server, timeout):
73+
"""Return a process-level cached RPCClient for the given target parameters.
74+
75+
The client is created once per (topic, server, timeout) combination and
76+
reused across all requests. This keeps the RabbitMQ connection pool warm
77+
and avoids per-request TCP connect/disconnect cycles.
78+
"""
79+
80+
key = (topic, server, timeout)
81+
client = _RPC_CLIENT_CACHE.get(key)
82+
if client is None:
83+
target = messaging.Target(topic=topic, server=server)
84+
client = rpc.get_client(
85+
target,
86+
serializer=objects_base.MagnumObjectSerializer(),
87+
timeout=timeout,
88+
)
89+
_RPC_CLIENT_CACHE[key] = client
90+
return client
91+
92+
6593
class API(object):
6694
def __init__(self, context=None, topic=None, server=None,
6795
timeout=None):
6896
self._context = context
6997
if topic is None:
7098
topic = ''
71-
target = messaging.Target(topic=topic, server=server)
72-
self._client = rpc.get_client(
73-
target,
74-
serializer=objects_base.MagnumObjectSerializer(),
75-
timeout=timeout
76-
)
99+
# Fetch (or create) the shared RPCClient from the process-level cache.
100+
# Storing it as self._client keeps the interface identical to the
101+
# original code; subclasses (conductor_api.API) that access
102+
# self._client directly for OVO indirection calls continue to work
103+
# without any changes.
104+
self._client = _get_cached_client(topic, server, timeout)
77105

78106
def _call(self, method, *args, **kwargs):
79107
return self._client.call(self._context, method, *args, **kwargs)

magnum/db/sqlalchemy/api.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,21 +113,36 @@ def _add_tenant_filters(self, context, query):
113113
if context.is_admin and context.all_tenants:
114114
return query
115115

116-
admin_context = request_context.make_admin_context(all_tenants=True)
117-
osc = clients.OpenStackClients(admin_context)
118-
kst = osc.keystone()
116+
# Read the trustee domain ID directly from configuration rather than
117+
# authenticating to Keystone on every DB query. The value is
118+
# operator-configured (CONF.trust.trustee_domain_id) and is stable for
119+
# the lifetime of the service.
120+
trustee_domain_id = CONF.trust.trustee_domain_id
121+
122+
# Fall back to a live Keystone lookup when trustee_domain_id is not
123+
# set in configuration so that deployments which rely on auto-discovery
124+
# continue to work correctly.
125+
if not trustee_domain_id:
126+
admin_context = request_context.make_admin_context(
127+
all_tenants=True)
128+
osc = clients.OpenStackClients(admin_context)
129+
trustee_domain_id = osc.keystone().trustee_domain_id
119130

120131
# User in a regular project (not in the trustee domain)
121132
if (
122133
context.project_id
123-
and context.user_domain_id != kst.trustee_domain_id
134+
and context.user_domain_id != trustee_domain_id
124135
):
125136
query = query.filter_by(project_id=context.project_id)
126137
# Match project ID component in trustee user's user name against
127138
# cluster's project_id to associate per-cluster trustee users who have
128139
# no project information with the project their clusters/cluster models
129140
# reside in. This is equivalent to the project filtering above.
130-
elif context.user_domain_id == kst.trustee_domain_id:
141+
elif context.user_domain_id == trustee_domain_id:
142+
admin_context = request_context.make_admin_context(
143+
all_tenants=True)
144+
osc = clients.OpenStackClients(admin_context)
145+
kst = osc.keystone()
131146
user_name = kst.client.users.get(context.user_id).name
132147
user_project = user_name.split('_', 2)[1]
133148
query = query.filter_by(project_id=user_project)

magnum/objects/cluster.py

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,40 @@ def _from_db_object(cluster, db_cluster):
111111
cluster.obj_reset_changes()
112112
return cluster
113113

114+
# Call-scoped nodegroups cache: None means inactive (no as_dict() in
115+
# progress). Set to a list by as_dict() before computing derived fields
116+
# and cleared afterwards. Outside of as_dict() _get_nodegroups() always
117+
# goes directly to the DB, so nodegroup mutations (create/delete) in
118+
# conductor code and tests are always visible.
119+
_nodegroups_cache = None
120+
121+
def _get_nodegroups(self):
122+
"""Fetch nodegroups, using a call-scoped cache when active.
123+
124+
as_dict() accesses four derived properties (node_count, master_count,
125+
node_addresses, master_addresses) that each call self.nodegroups.
126+
Without caching, a single as_dict() triggers four identical
127+
NodeGroup.list() RPC calls.
128+
129+
Rather than caching for the object lifetime (which would return stale
130+
data if nodegroups are created/deleted after first access, as happens
131+
in conductor and test code), the cache is scoped to a single
132+
as_dict() call: as_dict() populates _nodegroups_cache before
133+
computing derived fields and clears it on exit. At all other times
134+
_nodegroups_cache is None and this method goes directly to the DB.
135+
"""
136+
if self._nodegroups_cache is not None:
137+
return self._nodegroups_cache
138+
return NodeGroup.list(self._context, self.uuid)
139+
140+
def _invalidate_nodegroups_cache(self):
141+
"""Clear the call-scoped nodegroups cache."""
142+
self._nodegroups_cache = None
143+
114144
@property
115145
def nodegroups(self):
116146
# Returns all nodegroups that belong to the cluster.
117-
return NodeGroup.list(self._context, self.uuid)
147+
return self._get_nodegroups()
118148

119149
@property
120150
def default_ng_worker(self):
@@ -342,12 +372,20 @@ def obj_load_attr(self, attrname):
342372

343373
def as_dict(self):
344374
dict_ = super(Cluster, self).as_dict()
345-
# Update the dict with the attributes coming form
346-
# the cluster's nodegroups.
347-
dict_.update({
348-
'node_count': self.node_count,
349-
'master_count': self.master_count,
350-
'node_addresses': self.node_addresses,
351-
'master_addresses': self.master_addresses
352-
})
375+
# Populate the call-scoped nodegroups cache so that the four derived
376+
# properties below (node_count, master_count, node_addresses,
377+
# master_addresses) all share a single NodeGroup.list() fetch instead
378+
# of each issuing their own RPC call. The cache is cleared on exit
379+
# so that any subsequent access (e.g. from conductor code that has
380+
# mutated nodegroups) sees a fresh DB result.
381+
self._nodegroups_cache = NodeGroup.list(self._context, self.uuid)
382+
try:
383+
dict_.update({
384+
'node_count': self.node_count,
385+
'master_count': self.master_count,
386+
'node_addresses': self.node_addresses,
387+
'master_addresses': self.master_addresses
388+
})
389+
finally:
390+
self._nodegroups_cache = None
353391
return dict_

magnum/tests/base.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
from magnum.common import context as magnum_context
2929
from magnum.common import keystone as magnum_keystone
30+
from magnum.common import policy as magnum_policy
3031
from magnum.objects import base as objects_base
3132
from magnum.tests import conf_fixture
3233
from magnum.tests import fake_notifier
@@ -119,6 +120,12 @@ def make_context(*args, **kwargs):
119120
self.mock_make_trustee_domain_id = q.start()
120121
self.addCleanup(q.stop)
121122

123+
# Reset the module-level trustee_domain_id cache in policy.py so that
124+
# it is re-resolved on the first policy.enforce() call of the next
125+
# test. Without this, a test that exercises the Keystone-discovery
126+
# path could leave a stale value that bypasses mocks in later tests.
127+
self.addCleanup(magnum_policy._reset_trustee_domain_id_cache)
128+
122129
self.useFixture(conf_fixture.ConfFixture())
123130
self.useFixture(fixtures.NestedTempfile())
124131

0 commit comments

Comments
 (0)