Add oslo.config-based cluster label registry with auto-docs

Introduces a [capi_helm_cluster_labels] oslo.config group that serves
as the authoritative registry for all supported cluster labels. Each
label is defined as a typed oslo.config option (BoolOpt/IntOpt/StrOpt)
with a default value and help text, which operators can override
site-wide in magnum.conf and users can override per-cluster via
Magnum labels.

The five csi_cinder_* options previously scattered in [capi_helm] are
migrated to the new group with deprecated_opts so existing magnum.conf
files remain compatible without changes.

_label(), _get_label_bool(), _get_label_int() now raise ValueError for
any key not registered in the new group. A new _get_label_csv() method
handles the comma-separated api_master_lb_allowed_cidrs label.

A new AST-based unit test (TestClusterLabelRegistry) walks driver.py
at test time and asserts that every string-literal key passed to any
_label* method is present in capi_helm_cluster_labels_opts, preventing
unregistered labels from being silently introduced.

Documentation is updated to drop the hand-maintained label table in
favour of a cross-reference to the Configuration Reference page, which
oslo_config.sphinxext now auto-generates from the new group alongside
the existing [capi_helm] section.

Generated-By: Claude Sonnet 4.6
Change-Id: I2eb36d4c9bddc9d378b05bce1640d8a6d479552b
Signed-off-by: Michal Nasiadka <mnasiadka@gmail.com>
This commit is contained in:
Michal Nasiadka
2026-06-01 15:34:50 +02:00
parent 2557fb543a
commit ac402eb8f8
9 changed files with 598 additions and 172 deletions
+119 -21
View File
@@ -41,31 +41,10 @@
# Default StorageClass volume type for persistent volumes. (string value)
#csi_cinder_default_volume_type = <None>
# Policy for reclaiming dynamically created persistent volumes. Can be 'Retain'
# or 'Delete'. (string value)
#csi_cinder_reclaim_policy = Retain
# Allows the users to resize the volume by editing the corresponding PVC object.
# (boolean value)
#csi_cinder_allow_volume_expansion = true
# Select the Nodes where the application Pods may be scheduled based on Node
# labels. (list value)
#csi_cinder_allowed_topologies =
# Filesystem type for persistent volumes. (string value)
#csi_cinder_fstype = ext4
# The volumeBindingMode field controls when volume binding and dynamic
# provisioning should occur. (string value)
# Possible values:
# WaitForFirstConsumer - <No description provided>
# Immediate - <No description provided>
#csi_cinder_volume_binding_mode = WaitForFirstConsumer
# The default availability zone to use for Cinder volumes. (string value)
#csi_cinder_availability_zone = nova
# The value to use in the interface field of generated application credentials.
# (string value)
#app_cred_interface_type = public
@@ -96,3 +75,122 @@
# List of conditions to check for kubernetes control plane resource to consider
# as ready. (list value)
#k8s_control_plane_resource_conditions = MachinesReady,Ready,EtcdClusterHealthy,ControlPlaneComponentsHealthy
[capi_helm_cluster_labels]
#
# From capi_helm_cluster_labels
#
# Size of the etcd block device in GB. 0 means use the default ephemeral disk.
# (integer value)
#etcd_blockdevice_size = 0
# Storage type for the etcd block device. 'volume' uses a Cinder volume; 'local'
# uses an ephemeral disk. (string value)
# Possible values:
# volume - <No description provided>
# local - <No description provided>
#etcd_blockdevice_type = volume
# Cinder volume type for the etcd block device. (string value)
#etcd_blockdevice_volume_type =
# Availability zone for the etcd Cinder volume. (string value)
#etcd_blockdevice_volume_az =
# Deprecated: use etcd_blockdevice_size instead. (integer value)
# Deprecated group/name - [capi_helm]/etcd_volume_size
#etcd_volume_size = 0
# Deprecated: use etcd_blockdevice_volume_type instead. (string value)
# Deprecated group/name - [capi_helm]/etcd_volume_type
#etcd_volume_type =
# Enable the monitoring addon on the cluster. (boolean value)
#monitoring_enabled = false
# Enable the Kubernetes Dashboard addon. (boolean value)
#kube_dashboard_enabled = true
# Enable auto-healing for cluster nodes. (boolean value)
#auto_healing_enabled = true
# Enable the cluster autoscaler. (boolean value)
#auto_scaling_enabled = false
# Minimum node count for autoscaling. Defaults to the nodegroup node_count when
# unset. (integer value)
#min_node_count = <None>
# Maximum node count for autoscaling. Defaults to the nodegroup node_count when
# unset. (integer value)
#max_node_count = <None>
# Enable the Keystone authentication webhook. (boolean value)
#keystone_auth_enabled = false
# Default availability zone for Cinder volumes. (string value)
# Deprecated group/name - [capi_helm]/csi_cinder_availability_zone
#csi_cinder_availability_zone = nova
# Reclaim policy for dynamically provisioned persistent volumes. Can be 'Retain'
# or 'Delete'. (string value)
# Possible values:
# Retain - <No description provided>
# Delete - <No description provided>
# Deprecated group/name - [capi_helm]/csi_cinder_reclaim_policy
#csi_cinder_reclaim_policy = Retain
# Controls when volume binding and dynamic provisioning occur. Can be
# 'WaitForFirstConsumer' or 'Immediate'. (string value)
# Possible values:
# WaitForFirstConsumer - <No description provided>
# Immediate - <No description provided>
# Deprecated group/name - [capi_helm]/csi_cinder_volume_binding_mode
#csi_cinder_volume_binding_mode = WaitForFirstConsumer
# Filesystem type for persistent volumes. (string value)
# Deprecated group/name - [capi_helm]/csi_cinder_fstype
#csi_cinder_fstype = ext4
# Allow users to resize persistent volumes by editing the PVC. (boolean value)
# Deprecated group/name - [capi_helm]/csi_cinder_allow_volume_expansion
#csi_cinder_allow_volume_expansion = true
# Octavia load balancer provider (e.g. 'amphora' or 'ovn'). (string value)
#octavia_provider = amphora
# Load balancer algorithm. When unset, defaults to SOURCE_IP_PORT for the ovn
# provider and ROUND_ROBIN for all others. (string value)
#octavia_lb_algorithm =
# Enable health checks on the load balancer. (boolean value)
#octavia_lb_healthcheck = true
# Associate a floating IP with the API load balancer. (boolean value)
#master_lb_floating_ip_enabled = true
# CIDR for the node network when no fixed_subnet is specified. (string value)
#fixed_subnet_cidr = 10.0.0.0/24
# Comma-separated list of CIDRs allowed to reach the API load balancer. Empty
# means all CIDRs are allowed. (string value)
#api_master_lb_allowed_cidrs =
# Name of an additional Neutron network to attach to each node. (string value)
#extra_network_name =
# Root volume type. Falls back to the cinder config option
# default_boot_volume_type when unset. (string value)
#boot_volume_type = <None>
# Root volume size in GB. Falls back to the cinder config option
# default_boot_volume_size when unset. (integer value)
#boot_volume_size = <None>
# Helm chart version to use. Can only be set via the cluster template label, not
# overridden per cluster. Falls back to capi_helm.default_helm_chart_version
# when unset. (string value)
#capi_helm_chart_version =
@@ -2,8 +2,12 @@
Configuration reference
=======================
All configuration options listed bellow are used in
[capi_helm] stanza in magnum.conf file.
The ``[capi_helm]`` section controls driver-level settings (Helm chart
location, flavour constraints, etc.).
The ``[capi_helm_cluster_labels]`` section controls the **default values**
for all supported cluster labels. Every option here can be overridden
per-cluster by the user via Magnum cluster (or template) labels.
.. show-options::
:config-file: etc/oslo-config-generator/capi_helm.conf
+11 -69
View File
@@ -20,78 +20,20 @@ as updates to node groups and their sizes.
The Kubernetes versions against which the CAPI Helm charts are currently being
tested can be found `here <https://github.com/azimuth-cloud/capi-helm-charts/blob/main/.github/workflows/ensure-capi-images.yaml#L9>`__.
The driver respects the following cluster and template properties:
The driver supports cluster labels to customise cluster behaviour. The full
list of supported labels, their types, defaults, and descriptions is
maintained in the :doc:`Configuration Reference <config-reference>` under
the ``[capi_helm_cluster_labels]`` section.
* image_id
* keypair
* fixed_network, fixed_subnet (if missing, a new one is created
with CIDR: 10.0.0.0/24)
* external_network_id
* dns_nameserver
Default values for all labels can be set operator-wide in ``magnum.conf``
under ``[capi_helm_cluster_labels]`` and overridden per cluster or template
by the user via Magnum labels.
The driver supports the following labels:
.. note::
+-----------------------------------+---------------------+
| Label | Default |
+===================================+=====================+
| monitoring_enabled | false |
| | |
+-----------------------------------+---------------------+
| kube_dashboard_enabled | true |
| | |
+-----------------------------------+---------------------+
| fixed_subnet_cidr | 10.0.0.0/24 |
+-----------------------------------+---------------------+
| extra_network_name | empty |
+-----------------------------------+---------------------+
| capi_helm_chart_version | 0.10.1 |
| (see bellow for additional info) | |
+-----------------------------------+---------------------+
| etcd_blockdevice_size | 0 |
+-----------------------------------+---------------------+
| etcd_blockdevice_type | volume |
| | |
+-----------------------------------+---------------------+
| etcd_blockdevice_volume_az | "" |
+-----------------------------------+---------------------+
| etcd_volume_size | 0 |
+-----------------------------------+---------------------+
| etcd_volume_type | "" |
+-----------------------------------+---------------------+
| csi_cinder_availability_zone | "" |
+-----------------------------------+---------------------+
| csi_cinder_reclaim_policy | Delete |
| | |
+-----------------------------------+---------------------+
| csi_cinder_fstype | ext4 |
+-----------------------------------+---------------------+
| csi_cinder_allow_volume_expansion | True |
| | |
+-----------------------------------+---------------------+
| octavia_provider | amphora |
| | ovn |
+-----------------------------------+---------------------+
| octavia_lb_algorithm | ROUND_ROBIN, |
| | SOURCE_IP_PORT if |
| | octavia_provider is |
| | set to "ovn". |
+-----------------------------------+---------------------+
| boot_volume_type | "" |
+-----------------------------------+---------------------+
| extra_network_name | "" |
| | |
+-----------------------------------+---------------------+
One config option requires a little bit more explanation:
* capi_helm_chart_version: can only be set via template property
and CAN'T be overridden by cli options. If not set in cluster template,
value is taken from magnum.config option default_helm_chart_version.
``capi_helm_chart_version`` can only be set via a cluster template label
and cannot be overridden per cluster. When unset it falls back to
``[capi_helm] default_helm_chart_version``.
+2 -1
View File
@@ -1,4 +1,5 @@
[DEFAULT]
wrap_width = 80
output_file = etc/capi_helm.conf.sample
namespace = capi_helm
namespace = capi_helm
namespace = capi_helm_cluster_labels
+234 -35
View File
@@ -80,22 +80,6 @@ capi_helm_opts = [
"csi_cinder_default_volume_type",
help=("Default StorageClass volume type for persistent volumes."),
),
cfg.StrOpt(
"csi_cinder_reclaim_policy",
default="Retain",
help=(
"Policy for reclaiming dynamically created "
"persistent volumes. Can be 'Retain' or 'Delete'."
),
),
cfg.BoolOpt(
"csi_cinder_allow_volume_expansion",
default=True,
help=(
"Allows the users to resize the volume by "
"editing the corresponding PVC object."
),
),
cfg.ListOpt(
"csi_cinder_allowed_topologies",
default=[],
@@ -104,25 +88,6 @@ capi_helm_opts = [
"Pods may be scheduled based on Node labels."
),
),
cfg.StrOpt(
"csi_cinder_fstype",
default="ext4",
help=("Filesystem type for persistent volumes."),
),
cfg.StrOpt(
"csi_cinder_volume_binding_mode",
default="WaitForFirstConsumer",
choices=["WaitForFirstConsumer", "Immediate"],
help=(
"The volumeBindingMode field controls when "
"volume binding and dynamic provisioning should occur."
),
),
cfg.StrOpt(
"csi_cinder_availability_zone",
default="nova",
help=("The default availability zone to use for Cinder volumes."),
),
cfg.StrOpt(
"app_cred_interface_type",
default="public",
@@ -172,10 +137,244 @@ capi_helm_opts = [
),
]
capi_helm_cluster_labels_group = cfg.OptGroup(
name="capi_helm_cluster_labels",
title="Default values for cluster labels",
)
capi_helm_cluster_labels_opts = [
# etcd
cfg.IntOpt(
"etcd_blockdevice_size",
default=0,
help=(
"Size of the etcd block device in GB. "
"0 means use the default ephemeral disk."
),
),
cfg.StrOpt(
"etcd_blockdevice_type",
default="volume",
choices=["volume", "local"],
help=(
"Storage type for the etcd block device. "
"'volume' uses a Cinder volume; 'local' uses an ephemeral disk."
),
),
cfg.StrOpt(
"etcd_blockdevice_volume_type",
default="",
help="Cinder volume type for the etcd block device.",
),
cfg.StrOpt(
"etcd_blockdevice_volume_az",
default="",
help="Availability zone for the etcd Cinder volume.",
),
cfg.IntOpt(
"etcd_volume_size",
default=0,
deprecated_opts=[
cfg.DeprecatedOpt("etcd_volume_size", group="capi_helm")
],
help="Deprecated: use etcd_blockdevice_size instead.",
),
cfg.StrOpt(
"etcd_volume_type",
default="",
deprecated_opts=[
cfg.DeprecatedOpt("etcd_volume_type", group="capi_helm")
],
help="Deprecated: use etcd_blockdevice_volume_type instead.",
),
# addons
cfg.BoolOpt(
"monitoring_enabled",
default=False,
help="Enable the monitoring addon on the cluster.",
),
cfg.BoolOpt(
"kube_dashboard_enabled",
default=True,
help="Enable the Kubernetes Dashboard addon.",
),
cfg.BoolOpt(
"auto_healing_enabled",
default=True,
help="Enable auto-healing for cluster nodes.",
),
cfg.BoolOpt(
"auto_scaling_enabled",
default=False,
help="Enable the cluster autoscaler.",
),
cfg.IntOpt(
"min_node_count",
default=None,
help=(
"Minimum node count for autoscaling. "
"Defaults to the nodegroup node_count when unset."
),
),
cfg.IntOpt(
"max_node_count",
default=None,
help=(
"Maximum node count for autoscaling. "
"Defaults to the nodegroup node_count when unset."
),
),
# auth
cfg.BoolOpt(
"keystone_auth_enabled",
default=False,
help="Enable the Keystone authentication webhook.",
),
# CSI Cinder — moved from [capi_helm], old location still accepted
cfg.StrOpt(
"csi_cinder_availability_zone",
default="nova",
deprecated_opts=[
cfg.DeprecatedOpt(
"csi_cinder_availability_zone", group="capi_helm"
)
],
help="Default availability zone for Cinder volumes.",
),
cfg.StrOpt(
"csi_cinder_reclaim_policy",
default="Retain",
choices=["Retain", "Delete"],
deprecated_opts=[
cfg.DeprecatedOpt("csi_cinder_reclaim_policy", group="capi_helm")
],
help=(
"Reclaim policy for dynamically provisioned persistent volumes. "
"Can be 'Retain' or 'Delete'."
),
),
cfg.StrOpt(
"csi_cinder_volume_binding_mode",
default="WaitForFirstConsumer",
choices=["WaitForFirstConsumer", "Immediate"],
deprecated_opts=[
cfg.DeprecatedOpt(
"csi_cinder_volume_binding_mode", group="capi_helm"
)
],
help=(
"Controls when volume binding and dynamic provisioning occur. "
"Can be 'WaitForFirstConsumer' or 'Immediate'."
),
),
cfg.StrOpt(
"csi_cinder_fstype",
default="ext4",
deprecated_opts=[
cfg.DeprecatedOpt("csi_cinder_fstype", group="capi_helm")
],
help="Filesystem type for persistent volumes.",
),
cfg.BoolOpt(
"csi_cinder_allow_volume_expansion",
default=True,
deprecated_opts=[
cfg.DeprecatedOpt(
"csi_cinder_allow_volume_expansion", group="capi_helm"
)
],
help="Allow users to resize persistent volumes by editing the PVC.",
),
# Octavia load balancer
cfg.StrOpt(
"octavia_provider",
default="amphora",
help="Octavia load balancer provider (e.g. 'amphora' or 'ovn').",
),
cfg.StrOpt(
"octavia_lb_algorithm",
default="",
help=(
"Load balancer algorithm. "
"When unset, defaults to SOURCE_IP_PORT for the ovn provider "
"and ROUND_ROBIN for all others."
),
),
cfg.BoolOpt(
"octavia_lb_healthcheck",
default=True,
help="Enable health checks on the load balancer.",
),
# networking
cfg.BoolOpt(
"master_lb_floating_ip_enabled",
default=True,
help="Associate a floating IP with the API load balancer.",
),
cfg.StrOpt(
"fixed_subnet_cidr",
default="10.0.0.0/24",
help="CIDR for the node network when no fixed_subnet is specified.",
),
cfg.StrOpt(
"api_master_lb_allowed_cidrs",
default="",
help=(
"Comma-separated list of CIDRs allowed to reach the API load "
"balancer. Empty means all CIDRs are allowed."
),
),
cfg.StrOpt(
"extra_network_name",
default="",
help="Name of an additional Neutron network to attach to each node.",
),
# boot volume
cfg.StrOpt(
"boot_volume_type",
default=None,
help=(
"Root volume type. "
"Falls back to the cinder config option "
"default_boot_volume_type when unset."
),
),
cfg.IntOpt(
"boot_volume_size",
default=None,
help=(
"Root volume size in GB. "
"Falls back to the cinder config option "
"default_boot_volume_size when unset."
),
),
# chart version (template-only — cannot be overridden per cluster)
cfg.StrOpt(
"capi_helm_chart_version",
default="",
help=(
"Helm chart version to use. Can only be set via the cluster "
"template label, not overridden per cluster. Falls back to "
"capi_helm.default_helm_chart_version when unset."
),
),
]
CONF = cfg.CONF
CONF.register_group(capi_helm_group)
CONF.register_opts(capi_helm_opts, group=capi_helm_group)
CONF.register_group(capi_helm_cluster_labels_group)
CONF.register_opts(
capi_helm_cluster_labels_opts, group=capi_helm_cluster_labels_group
)
def list_capi_opts():
return [(capi_helm_group, [o]) for o in capi_helm_opts]
def list_capi_cluster_label_opts():
return [
(capi_helm_cluster_labels_group, [o])
for o in capi_helm_cluster_labels_opts
]
+124 -32
View File
@@ -38,6 +38,10 @@ LOG = logging.getLogger(__name__)
CONF = conf.CONF
NODE_GROUP_ROLE_CONTROLLER = "master"
_REGISTERED_LABEL_NAMES = frozenset(
opt.name for opt in conf.capi_helm_cluster_labels_opts
)
class NodeGroupState(enum.Enum):
NOT_PRESENT = 1
@@ -487,6 +491,11 @@ class Driver(driver.Driver):
)
def _label(self, cluster, key, default):
if key not in _REGISTERED_LABEL_NAMES:
raise ValueError(
f"Cluster label '{key}' is not registered in "
"capi_helm_cluster_labels. Add it to conf.py first."
)
all_labels = helm.mergeconcat(
cluster.cluster_template.labels, cluster.labels
)
@@ -509,6 +518,25 @@ class Driver(driver.Driver):
except ValueError:
return default
def _get_label_csv(self, cluster, label, default):
"""Return a cluster label as a filtered list split on commas."""
if label not in _REGISTERED_LABEL_NAMES:
raise ValueError(
f"Cluster label '{label}' is not registered in "
"capi_helm_cluster_labels. Add it to conf.py first."
)
all_labels = helm.mergeconcat(
cluster.cluster_template.labels, cluster.labels
)
raw = (all_labels or {}).get(label, "") or default
if not raw:
return []
return [
re.sub(r"[^a-zA-Z0-9\.\-\/\:]+", "", entry.strip())
for entry in str(raw).split(",")
if entry.strip()
]
def _get_chart_version(self, cluster):
version = cluster.cluster_template.labels.get(
"capi_helm_chart_version",
@@ -592,18 +620,25 @@ class Driver(driver.Driver):
)
def _get_etcd_config(self, cluster):
lconf = CONF.capi_helm_cluster_labels
# Support new-style and legacy labels for volume size and type, with
# new-style labels taking precedence
etcd_size = self._get_label_int(
cluster,
"etcd_blockdevice_size",
self._get_label_int(cluster, "etcd_volume_size", 0),
self._get_label_int(
cluster,
"etcd_volume_size",
lconf.etcd_blockdevice_size,
),
)
if etcd_size > 0:
etcd_block_device = {"size": etcd_size}
# The block device type can be either local or volume
etcd_bd_type = self._label(
cluster, "etcd_blockdevice_type", "volume"
cluster,
"etcd_blockdevice_type",
lconf.etcd_blockdevice_type,
)
if etcd_bd_type == "local":
etcd_block_device["type"] = "Local"
@@ -613,13 +648,19 @@ class Driver(driver.Driver):
etcd_volume_type = self._label(
cluster,
"etcd_blockdevice_volume_type",
self._label(cluster, "etcd_volume_type", ""),
self._label(
cluster,
"etcd_volume_type",
lconf.etcd_blockdevice_volume_type,
),
)
if etcd_volume_type:
etcd_block_device["volumeType"] = etcd_volume_type
etcd_volume_az = self._label(
cluster, "etcd_blockdevice_volume_az", ""
cluster,
"etcd_blockdevice_volume_az",
lconf.etcd_blockdevice_volume_az,
)
if etcd_volume_az:
etcd_block_device["availabilityZone"] = etcd_volume_az
@@ -635,19 +676,32 @@ class Driver(driver.Driver):
return None
def _get_monitoring_enabled(self, cluster):
# NOTE(mkjpryor) default off, like heat driver,
# as requires cinder and takes a while
return self._get_label_bool(cluster, "monitoring_enabled", False)
return self._get_label_bool(
cluster,
"monitoring_enabled",
CONF.capi_helm_cluster_labels.monitoring_enabled,
)
def _get_kube_dash_enabled(self, cluster):
# NOTE(mkjpryor) default on, like the heat driver
return self._get_label_bool(cluster, "kube_dashboard_enabled", True)
return self._get_label_bool(
cluster,
"kube_dashboard_enabled",
CONF.capi_helm_cluster_labels.kube_dashboard_enabled,
)
def _get_autoheal_enabled(self, cluster):
return self._get_label_bool(cluster, "auto_healing_enabled", True)
return self._get_label_bool(
cluster,
"auto_healing_enabled",
CONF.capi_helm_cluster_labels.auto_healing_enabled,
)
def _get_autoscale_enabled(self, cluster):
return self._get_label_bool(cluster, "auto_scaling_enabled", False)
return self._get_label_bool(
cluster,
"auto_scaling_enabled",
CONF.capi_helm_cluster_labels.auto_scaling_enabled,
)
def _get_autoscale_values(self, cluster, nodegroup):
auto_scale = self._get_autoscale_enabled(cluster)
@@ -662,7 +716,11 @@ class Driver(driver.Driver):
return auto_scale_args
def _get_k8s_keystone_auth_enabled(self, cluster):
return self._get_label_bool(cluster, "keystone_auth_enabled", False)
return self._get_label_bool(
cluster,
"keystone_auth_enabled",
CONF.capi_helm_cluster_labels.keystone_auth_enabled,
)
def _get_fixed_network_id(self, context, cluster):
network = cluster.fixed_network
@@ -737,11 +795,17 @@ class Driver(driver.Driver):
# We still want to be able to override the default node
# group values with labels for consistent behaviour with
# Magnum Heat driver.
conf_min = CONF.capi_helm_cluster_labels.min_node_count
conf_max = CONF.capi_helm_cluster_labels.max_node_count
min_nodes = self._get_label_int(
cluster, "min_node_count", min_nodes
cluster,
"min_node_count",
conf_min if conf_min is not None else min_nodes,
)
max_nodes = self._get_label_int(
cluster, "max_node_count", max_nodes
cluster,
"max_node_count",
conf_max if conf_max is not None else max_nodes,
)
return min_nodes, max_nodes
@@ -780,54 +844,64 @@ class Driver(driver.Driver):
return self._label(
cluster,
"csi_cinder_availability_zone",
CONF.capi_helm.csi_cinder_availability_zone,
CONF.capi_helm_cluster_labels.csi_cinder_availability_zone,
)
def _get_csi_cinder_reclaim_policy(self, cluster):
return self._label(
cluster,
"csi_cinder_reclaim_policy",
CONF.capi_helm.csi_cinder_reclaim_policy,
CONF.capi_helm_cluster_labels.csi_cinder_reclaim_policy,
)
def _get_csi_cinder_volume_binding_mode(self, cluster):
return self._label(
cluster,
"csi_cinder_volume_binding_mode",
CONF.capi_helm.csi_cinder_volume_binding_mode,
CONF.capi_helm_cluster_labels.csi_cinder_volume_binding_mode,
)
def _get_csi_cinder_fstype(self, cluster):
return self._label(
cluster,
"csi_cinder_fstype",
CONF.capi_helm.csi_cinder_fstype,
CONF.capi_helm_cluster_labels.csi_cinder_fstype,
)
def _get_csi_cinder_allow_volume_expansion(self, cluster):
return self._get_label_bool(
cluster,
"csi_cinder_allow_volume_expansion",
CONF.capi_helm.csi_cinder_allow_volume_expansion,
CONF.capi_helm_cluster_labels.csi_cinder_allow_volume_expansion,
)
def _get_octavia_provider(self, cluster):
return self._label(cluster, "octavia_provider", "amphora")
return self._label(
cluster,
"octavia_provider",
CONF.capi_helm_cluster_labels.octavia_provider,
)
def _get_octavia_lb_algorithm(self, cluster):
provider = self._get_octavia_provider(cluster)
conf_default = CONF.capi_helm_cluster_labels.octavia_lb_algorithm
dynamic_default = (
"SOURCE_IP_PORT" if provider.lower() == "ovn" else "ROUND_ROBIN"
)
return self._label(
cluster,
"octavia_lb_algorithm",
"SOURCE_IP_PORT" if provider.lower() == "ovn" else "ROUND_ROBIN",
conf_default or dynamic_default,
)
def _get_allowed_cidrs(self, cluster):
cidr_list = cluster.labels.get("api_master_lb_allowed_cidrs", "")
LOG.debug(f"CIDR list {cidr_list}")
if isinstance(cidr_list, str) and cidr_list != "":
return cidr_list.split(",")
return False
cidrs = self._get_label_csv(
cluster,
"api_master_lb_allowed_cidrs",
CONF.capi_helm_cluster_labels.api_master_lb_allowed_cidrs,
)
LOG.debug(f"CIDR list {cidrs}")
return cidrs or False
def _storageclass_definitions(self, context, cluster):
"""Query cinder API to retrieve list of available volume types.
@@ -908,6 +982,7 @@ class Driver(driver.Driver):
return nodegroup_set
def _update_helm_release(self, context, cluster, nodegroups=None):
lconf = CONF.capi_helm_cluster_labels
if nodegroups is None:
nodegroups = cluster.nodegroups
@@ -930,7 +1005,7 @@ class Driver(driver.Driver):
"associateFloatingIP": self._get_label_bool(
cluster,
"master_lb_floating_ip_enabled",
True,
lconf.master_lb_floating_ip_enabled,
),
"enableLoadBalancer": True,
"loadBalancerProvider": self._get_octavia_provider(cluster),
@@ -947,7 +1022,9 @@ class Driver(driver.Driver):
"subnetFilter": ({"id": subnet_id} if subnet_id else None),
# This is only used if a fixed network is not specified
"nodeCidr": self._label(
cluster, "fixed_subnet_cidr", "10.0.0.0/24"
cluster,
"fixed_subnet_cidr",
lconf.fixed_subnet_cidr,
),
},
},
@@ -977,7 +1054,9 @@ class Driver(driver.Driver):
cluster
),
"create-monitor": self._get_label_bool(
cluster, "octavia_lb_healthcheck", True
cluster,
"octavia_lb_healthcheck",
lconf.octavia_lb_healthcheck,
),
}
},
@@ -997,7 +1076,9 @@ class Driver(driver.Driver):
# Add boot disk details, if defined in config file.
# Helm chart defaults to ephemeral disks, if unset.
boot_volume_type = self._label(
cluster, "boot_volume_type", CONF.cinder.default_boot_volume_type
cluster,
"boot_volume_type",
lconf.boot_volume_type or CONF.cinder.default_boot_volume_type,
)
if boot_volume_type:
disk_type_details = {
@@ -1014,8 +1095,15 @@ class Driver(driver.Driver):
}
values = helm.mergeconcat(values, disk_type_details)
conf_boot_size = lconf.boot_volume_size
boot_volume_size_gb = self._get_label_int(
cluster, "boot_volume_size", CONF.cinder.default_boot_volume_size
cluster,
"boot_volume_size",
(
conf_boot_size
if conf_boot_size is not None
else CONF.cinder.default_boot_volume_size
),
)
if boot_volume_size_gb:
disk_size_details = {
@@ -1034,7 +1122,11 @@ class Driver(driver.Driver):
# Sometimes you need to add an extra network
# for things like Cinder CSI CephFS Native
extra_network_name = self._label(cluster, "extra_network_name", "")
extra_network_name = self._label(
cluster,
"extra_network_name",
lconf.extra_network_name,
)
if extra_network_name:
network_details = {
"nodeGroupDefaults": {
+86 -12
View File
@@ -967,25 +967,40 @@ class ClusterAPIDriverTest(base.DbTestCase):
self.cluster_obj.labels = dict()
self.cluster_obj.cluster_template.labels = dict()
result = self.driver._label(self.cluster_obj, "foo", "bar")
result = self.driver._label(
self.cluster_obj, "monitoring_enabled", "bar"
)
self.assertEqual("bar", result)
def test_label_return_template(self):
self.cluster_obj.cluster_template.labels = dict(foo=42)
self.cluster_obj.cluster_template.labels = dict(monitoring_enabled=42)
result = self.driver._label(self.cluster_obj, "foo", "bar")
result = self.driver._label(
self.cluster_obj, "monitoring_enabled", "bar"
)
self.assertEqual("42", result)
def test_label_return_cluster(self):
self.cluster_obj.labels = dict(foo=41)
self.cluster_obj.cluster_template.labels = dict(foo=42)
self.cluster_obj.labels = dict(monitoring_enabled=41)
self.cluster_obj.cluster_template.labels = dict(monitoring_enabled=42)
result = self.driver._label(self.cluster_obj, "foo", "bar")
result = self.driver._label(
self.cluster_obj, "monitoring_enabled", "bar"
)
self.assertEqual("41", result)
def test_label_unregistered_key_raises(self):
self.assertRaises(
ValueError,
self.driver._label,
self.cluster_obj,
"not_a_real_label",
"default",
)
def test_sanitized_name_no_suffix(self):
self.assertEqual(
"123-456fab", driver_utils.sanitized_name("123-456Fab")
@@ -2020,7 +2035,9 @@ class ClusterAPIDriverTest(base.DbTestCase):
@mock.patch("magnum.common.clients.OpenStackClients.cinder")
def test_get_storage_classes(self, mock_cinder):
CONF.capi_helm.csi_cinder_default_volume_type = "type3"
CONF.capi_helm.csi_cinder_availability_zone = "middle_earth_east"
CONF.capi_helm_cluster_labels.csi_cinder_availability_zone = (
"middle_earth_east"
)
mock_vol_type_1 = mock.MagicMock()
mock_vol_type_1.name = "type1"
mock_vol_type_2 = mock.MagicMock()
@@ -2055,7 +2072,9 @@ class ClusterAPIDriverTest(base.DbTestCase):
@mock.patch("magnum.common.clients.OpenStackClients.cinder")
def test_get_storage_class_volume_type_not_available(self, mock_cinder):
CONF.capi_helm.csi_cinder_default_volume_type = "type4"
CONF.capi_helm.csi_cinder_availability_zone = "middle_earth_east"
CONF.capi_helm_cluster_labels.csi_cinder_availability_zone = (
"middle_earth_east"
)
mock_vol_type_1 = mock.MagicMock()
mock_vol_type_1.name = "type1"
mock_vol_type_2 = mock.MagicMock()
@@ -2082,7 +2101,9 @@ class ClusterAPIDriverTest(base.DbTestCase):
@mock.patch("magnum.common.clients.OpenStackClients.cinder")
def test_get_storage_class_volume_type_not_defined(self, mock_cinder):
CONF.capi_helm.csi_cinder_default_volume_type = None
CONF.capi_helm.csi_cinder_availability_zone = "middle_earth_east"
CONF.capi_helm_cluster_labels.csi_cinder_availability_zone = (
"middle_earth_east"
)
mock_vol_type_1 = mock.MagicMock()
mock_vol_type_1.name = "__TYPE1__"
mock_vol_type_2 = mock.MagicMock()
@@ -2108,7 +2129,9 @@ class ClusterAPIDriverTest(base.DbTestCase):
@mock.patch("magnum.common.clients.OpenStackClients.cinder")
def test_get_storage_classes_openstacksdk(self, mock_cinder):
CONF.capi_helm.csi_cinder_default_volume_type = "type3"
CONF.capi_helm.csi_cinder_availability_zone = "middle_earth_east"
CONF.capi_helm_cluster_labels.csi_cinder_availability_zone = (
"middle_earth_east"
)
mock_vol_type_1 = mock.MagicMock()
mock_vol_type_1.name = "type1"
mock_vol_type_2 = mock.MagicMock()
@@ -2145,7 +2168,9 @@ class ClusterAPIDriverTest(base.DbTestCase):
self, mock_cinder
):
CONF.capi_helm.csi_cinder_default_volume_type = "type4"
CONF.capi_helm.csi_cinder_availability_zone = "middle_earth_east"
CONF.capi_helm_cluster_labels.csi_cinder_availability_zone = (
"middle_earth_east"
)
mock_vol_type_1 = mock.MagicMock()
mock_vol_type_1.name = "type1"
mock_vol_type_2 = mock.MagicMock()
@@ -2174,7 +2199,9 @@ class ClusterAPIDriverTest(base.DbTestCase):
self, mock_cinder
):
CONF.capi_helm.csi_cinder_default_volume_type = None
CONF.capi_helm.csi_cinder_availability_zone = "middle_earth_east"
CONF.capi_helm_cluster_labels.csi_cinder_availability_zone = (
"middle_earth_east"
)
mock_vol_type_1 = mock.MagicMock()
mock_vol_type_1.name = "__TYPE1__"
mock_vol_type_2 = mock.MagicMock()
@@ -3696,3 +3723,50 @@ class ClusterAPIDriverTest(base.DbTestCase):
fields.ClusterStatus.UPDATE_FAILED, self.cluster_obj.status
)
self.assertIsNotNone(self.cluster_obj.status_reason)
class TestClusterLabelRegistry(base.DbTestCase):
"""Enforce that every label key used in the driver is registered."""
def test_all_label_calls_use_registered_keys(self):
import ast
import inspect
from magnum_capi_helm import conf as capi_conf
from magnum_capi_helm import driver as capi_driver
registered = frozenset(
opt.name for opt in capi_conf.capi_helm_cluster_labels_opts
)
source = inspect.getsource(capi_driver)
tree = ast.parse(source)
label_methods = {
"_label",
"_get_label_bool",
"_get_label_int",
"_get_label_csv",
}
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
func = node.func
if not (
isinstance(func, ast.Attribute) and func.attr in label_methods
):
continue
# second positional arg (index 1) is the label key
if len(node.args) < 2:
continue
key_node = node.args[1]
# Variable keys (e.g. inside _get_label_bool forwarding to
# _label) are validated at runtime; only check string literals.
if not isinstance(key_node, ast.Constant):
continue
self.assertIn(
key_node.value,
registered,
f"Label '{key_node.value}' used at line {node.lineno} "
"is not registered in capi_helm_cluster_labels_opts. "
"Add it to conf.py before using it in the driver.",
)
@@ -0,0 +1,15 @@
---
features:
- |
All cluster label defaults are now configurable site-wide via a new
``[capi_helm_cluster_labels]`` section in ``magnum.conf``. Each supported
label has a typed option with a default value that users can still override
per cluster or template via Magnum labels.
deprecations:
- |
The ``csi_cinder_availability_zone``, ``csi_cinder_reclaim_policy``,
``csi_cinder_volume_binding_mode``, ``csi_cinder_fstype``, and
``csi_cinder_allow_volume_expansion`` options have moved from the
``[capi_helm]`` section to the new ``[capi_helm_cluster_labels]`` section.
The old locations are still accepted but will produce a deprecation
warning. Please update your ``magnum.conf`` accordingly.
+1
View File
@@ -31,3 +31,4 @@ magnum.drivers =
k8s_capi_helm_v1 = magnum_capi_helm.driver:Driver
oslo.config.opts =
capi_helm = magnum_capi_helm.conf:list_capi_opts
capi_helm_cluster_labels = magnum_capi_helm.conf:list_capi_cluster_label_opts