Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions api/bases/neutron.openstack.org_neutronapis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1306,9 +1306,22 @@ spec:
description: ReadyCount of neutron API instances
format: int32
type: integer
rpcReadyCount:
description: |-
RPCReadyCount of neutron-rpc-server instances. Only populated when the
WSGI deployment strategy is enabled and the RPC worker is not disabled
via rpc_workers=0 in customServiceConfig.
format: int32
type: integer
transportURLSecret:
description: TransportURLSecret - Secret containing RabbitMQ transportURL
type: string
workerReadyCount:
description: |-
WorkerReadyCount of neutron background worker (periodic/OVN maintenance)
instances. Only populated when the WSGI deployment strategy is enabled.
format: int32
type: integer
type: object
type: object
served: true
Expand Down
29 changes: 29 additions & 0 deletions api/v1beta1/conditions.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ import (
const (
// Neutron External Configs Ready indicates when the external config is ready
NeutronExternalConfigsReady condition.Type = "Neutron External Configs Ready"

// NeutronRPCReadyCondition indicates the status of the neutron-rpc
// Deployment (neutron-rpc-server), only used under the WSGI strategy
NeutronRPCReadyCondition condition.Type = "NeutronRPCReady"

// NeutronWorkerReadyCondition indicates the status of the neutron-worker
// Deployment (periodic/OVN maintenance workers), only used under the
// WSGI strategy
NeutronWorkerReadyCondition condition.Type = "NeutronWorkerReady"
)

// Common Messages used by API objects.
Expand All @@ -34,4 +43,24 @@ const (

//NeutronDhcpAgentConfigErrorMessageW
NeutronExternalConfigsErrorMessage = "Neutron external configs generation error occurred %s"

// NeutronRPCReadyInitMessage
NeutronRPCReadyInitMessage = "NeutronRPC not started"
// NeutronRPCReadyMessage
NeutronRPCReadyMessage = "NeutronRPC ready"
// NeutronRPCDisabledMessage
NeutronRPCDisabledMessage = "NeutronRPC disabled by rpc_workers=0 in customServiceConfig"
// NeutronRPCReadyRunningMessage
NeutronRPCReadyRunningMessage = "NeutronRPC deployment in progress"
// NeutronRPCReadyErrorMessage
NeutronRPCReadyErrorMessage = "NeutronRPC error occurred %s"

// NeutronWorkerReadyInitMessage
NeutronWorkerReadyInitMessage = "NeutronWorker not started"
// NeutronWorkerReadyMessage
NeutronWorkerReadyMessage = "NeutronWorker ready"
// NeutronWorkerReadyRunningMessage
NeutronWorkerReadyRunningMessage = "NeutronWorker deployment in progress"
// NeutronWorkerReadyErrorMessage
NeutronWorkerReadyErrorMessage = "NeutronWorker error occurred %s"
)
73 changes: 73 additions & 0 deletions api/v1beta1/neutronapi_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ limitations under the License.
package v1beta1

import (
"strconv"
"strings"

rabbitmqv1 "github.com/openstack-k8s-operators/infra-operator/apis/rabbitmq/v1beta1"
topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1"
"github.com/openstack-k8s-operators/lib-common/modules/common/condition"
Expand All @@ -40,6 +43,16 @@ const (

// NeutronAPIContainerImage is the fall-back container image for NeutronAPI
NeutronAPIContainerImage = "quay.io/podified-antelope-centos9/openstack-neutron-server:current-podified"

// NeutronWSGILabel is the annotation used to select between the WSGI
// (httpd/mod_wsgi + separate neutron-rpc-server/worker Deployments) and
// the legacy Eventlet (neutron-server + httpd reverse-proxy) deployment
// strategies. It is set by the openstack-operator based on the
// OpenStackVersion service defaults
// so that upgrading neutron-operator alone never changes the strategy
// of an existing deployment. When the annotation is absent, the operator
// defaults to the legacy Eventlet strategy to preserve backward compatibility.
NeutronWSGILabel = "neutron.openstack.org/wsgi"
)

// NeutronAPISpec defines the desired state of NeutronAPI
Expand Down Expand Up @@ -256,6 +269,15 @@ type NeutronAPIStatus struct {
// NotificationsTransportURLSecret - Secret containing
// external notifications transportURL
NotificationsTransportURLSecret *string `json:"notificationsTransportURLSecret,omitempty"`

// RPCReadyCount of neutron-rpc-server instances. Only populated when the
// WSGI deployment strategy is enabled and the RPC worker is not disabled
// via rpc_workers=0 in customServiceConfig.
RPCReadyCount int32 `json:"rpcReadyCount,omitempty"`

// WorkerReadyCount of neutron background worker (periodic/OVN maintenance)
// instances. Only populated when the WSGI deployment strategy is enabled.
WorkerReadyCount int32 `json:"workerReadyCount,omitempty"`
}

// +kubebuilder:object:root=true
Expand Down Expand Up @@ -332,6 +354,21 @@ func (instance NeutronAPI) RbacResourceName() string {
return "neutron-" + instance.Name
}

// IsWSGI - returns true if this NeutronAPI should be deployed using the
// WSGI strategy (httpd/mod_wsgi + separate neutron-rpc/neutron-worker
// Deployments), based on the NeutronWSGILabel annotation. Absent the
// annotation, it defaults to false (the legacy Eventlet strategy) so that
// upgrading neutron-operator alone never changes the deployment strategy of
// an existing NeutronAPI. The annotation is expected to be set explicitly by
// openstack-operator for both fresh and existing deployments; standalone
// users of neutron-operator opt in by setting it manually.
func (instance NeutronAPI) IsWSGI() bool {
if v, ok := instance.GetAnnotations()[NeutronWSGILabel]; ok {
return v == "true"
}
return false
}

func (instance NeutronAPI) IsOVNEnabled() bool {
for _, driver := range instance.Spec.Ml2MechanismDrivers {
// TODO: use const
Expand All @@ -342,6 +379,42 @@ func (instance NeutronAPI) IsOVNEnabled() bool {
return false
}

// GetRPCWorkers parses instance.Spec.CustomServiceConfig for an explicit
// rpc_workers setting under the [DEFAULT] section (the only section
// rpc_workers is valid in), the same way GetEnabledBackends() does for
// Glance's enabled_backends. It returns the parsed value and whether
// rpc_workers was found at all. Used to decide whether the neutron-rpc
// Deployment should be disabled (rpc_workers=0) when running under the WSGI
// strategy.
func GetRPCWorkers(customServiceConfig string) (int, bool) {
// Content before any section header belongs to the implicit [DEFAULT]
// section, matching Python's configparser (and oslo.config) semantics.
section := "DEFAULT"
for _, line := range strings.Split(customServiceConfig, "\n") {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
// Skip blank lines and comments
continue
}
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
section = strings.TrimSpace(trimmed[1 : len(trimmed)-1])
continue
}
if section != "DEFAULT" {
continue
}
tokenLine := strings.SplitN(trimmed, "=", 2)
token := strings.ReplaceAll(tokenLine[0], " ", "")
if token == "rpc_workers" && len(tokenLine) == 2 {
val, err := strconv.Atoi(strings.TrimSpace(tokenLine[1]))
if err == nil {
return val, true
}
}
}
return -1, false
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// SetupDefaults - initializes any CRD field defaults based on environment variables (the defaulting mechanism itself is implemented via webhooks)
func SetupDefaults() {
// Acquire environmental defaults and initialize Neutron defaults with them
Expand Down
13 changes: 13 additions & 0 deletions config/crd/bases/neutron.openstack.org_neutronapis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1306,9 +1306,22 @@ spec:
description: ReadyCount of neutron API instances
format: int32
type: integer
rpcReadyCount:
description: |-
RPCReadyCount of neutron-rpc-server instances. Only populated when the
WSGI deployment strategy is enabled and the RPC worker is not disabled
via rpc_workers=0 in customServiceConfig.
format: int32
type: integer
transportURLSecret:
description: TransportURLSecret - Secret containing RabbitMQ transportURL
type: string
workerReadyCount:
description: |-
WorkerReadyCount of neutron background worker (periodic/OVN maintenance)
instances. Only populated when the WSGI deployment strategy is enabled.
format: int32
type: integer
type: object
type: object
served: true
Expand Down
159 changes: 158 additions & 1 deletion internal/controller/neutronapi_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -1314,7 +1314,9 @@ func (r *NeutronAPIReconciler) reconcileNormal(ctx context.Context, instance *ne
instance.Status.LastAppliedTopology = nil
}

deplDef, err := neutronapi.Deployment(instance, inputHash, serviceLabels, serviceAnnotations, topology, memcached)
wsgi := instance.IsWSGI()

deplDef, err := neutronapi.Deployment(instance, inputHash, serviceLabels, serviceAnnotations, topology, memcached, wsgi)
if err != nil {
instance.Status.Conditions.Set(condition.FalseCondition(
condition.DeploymentReadyCondition,
Expand Down Expand Up @@ -1392,6 +1394,160 @@ func (r *NeutronAPIReconciler) reconcileNormal(ctx context.Context, instance *ne
}
}
// create Deployment - end

//
// neutron-rpc / neutron-worker Deployments (WSGI strategy only)
//
if wsgi {
// rpc_workers=0 in customServiceConfig means the human operator has
// explicitly asked for no RPC workers -- skip (and clean up) the
// neutron-rpc Deployment entirely rather than scaling it to 0, since
// there is no HTTP endpoint behind it to keep alive at 0 replicas.
rpcWorkers, rpcSet := neutronv1beta1.GetRPCWorkers(instance.Spec.CustomServiceConfig)
rpcEnabled := !rpcSet || rpcWorkers != 0
rpcName := fmt.Sprintf("%s-%s", neutronapi.ServiceName, neutronapi.RPCDeploymentSuffix)

if rpcEnabled {
// Seed a non-true state before CreateOrPatch: it returns an
// empty ctrl.Result even right after creating a brand-new
// Deployment, and immediately post-create Generation !=
// ObservedGeneration, so the Generation-gated block below can
// be skipped entirely on this pass. Without this seed, the
// condition would stay absent rather than False -- and
// AllSubConditionIsTrue() ignores absent conditions, which
// could let the aggregate Ready condition go True before the
// RPC Deployment has actually rolled out any pods.
instance.Status.Conditions.Set(condition.FalseCondition(
neutronv1beta1.NeutronRPCReadyCondition,
condition.RequestedReason,
condition.SeverityInfo,
neutronv1beta1.NeutronRPCReadyRunningMessage))

rpcLabels := map[string]string{
common.AppSelector: rpcName,
}
rpcDeplDef := neutronapi.RPCDeployment(instance, inputHash, rpcLabels, serviceAnnotations, topology, memcached)
rpcDepl := deployment.NewDeployment(rpcDeplDef, time.Duration(5)*time.Second)

ctrlResult, err = rpcDepl.CreateOrPatch(ctx, helper)
if err != nil {
instance.Status.Conditions.Set(condition.FalseCondition(
neutronv1beta1.NeutronRPCReadyCondition,
condition.ErrorReason,
condition.SeverityWarning,
neutronv1beta1.NeutronRPCReadyErrorMessage,
err.Error()))
return ctrlResult, err
} else if (ctrlResult != ctrl.Result{}) {
instance.Status.Conditions.Set(condition.FalseCondition(
neutronv1beta1.NeutronRPCReadyCondition,
condition.RequestedReason,
condition.SeverityInfo,
neutronv1beta1.NeutronRPCReadyRunningMessage))
return ctrlResult, nil
}

rpcDeploy := rpcDepl.GetDeployment()
if rpcDeploy.Generation == rpcDeploy.Status.ObservedGeneration {
instance.Status.RPCReadyCount = rpcDeploy.Status.ReadyReplicas
if deployment.IsReady(rpcDeploy) {
instance.Status.Conditions.MarkTrue(
neutronv1beta1.NeutronRPCReadyCondition,
neutronv1beta1.NeutronRPCReadyMessage)
} else {
instance.Status.Conditions.Set(condition.FalseCondition(
neutronv1beta1.NeutronRPCReadyCondition,
condition.RequestedReason,
condition.SeverityInfo,
neutronv1beta1.NeutronRPCReadyRunningMessage))
}
}
Comment thread
karelyatin marked this conversation as resolved.
} else {
// rpc_workers=0: remove a previously-created neutron-rpc
// Deployment, e.g. left over from before the user disabled it.
existingRPC := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: rpcName, Namespace: instance.Namespace},
}
if err := helper.GetClient().Delete(ctx, existingRPC); err != nil && !k8s_errors.IsNotFound(err) {
return ctrl.Result{}, err
}
instance.Status.RPCReadyCount = 0
instance.Status.Conditions.MarkTrue(
neutronv1beta1.NeutronRPCReadyCondition,
neutronv1beta1.NeutronRPCDisabledMessage)
}

// Same seeding rationale as NeutronRPCReadyCondition above: the
// worker Deployment is always active under wsgi, so its condition
// must never be left absent while CreateOrPatch/Generation catch up.
instance.Status.Conditions.Set(condition.FalseCondition(
neutronv1beta1.NeutronWorkerReadyCondition,
condition.RequestedReason,
condition.SeverityInfo,
neutronv1beta1.NeutronWorkerReadyRunningMessage))

workerName := fmt.Sprintf("%s-%s", neutronapi.ServiceName, neutronapi.WorkerDeploymentSuffix)
workerLabels := map[string]string{
common.AppSelector: workerName,
}
workerDeplDef := neutronapi.WorkerDeployment(instance, inputHash, workerLabels, serviceAnnotations, topology, memcached)
workerDepl := deployment.NewDeployment(workerDeplDef, time.Duration(5)*time.Second)

ctrlResult, err = workerDepl.CreateOrPatch(ctx, helper)
if err != nil {
instance.Status.Conditions.Set(condition.FalseCondition(
neutronv1beta1.NeutronWorkerReadyCondition,
condition.ErrorReason,
condition.SeverityWarning,
neutronv1beta1.NeutronWorkerReadyErrorMessage,
err.Error()))
return ctrlResult, err
} else if (ctrlResult != ctrl.Result{}) {
instance.Status.Conditions.Set(condition.FalseCondition(
neutronv1beta1.NeutronWorkerReadyCondition,
condition.RequestedReason,
condition.SeverityInfo,
neutronv1beta1.NeutronWorkerReadyRunningMessage))
return ctrlResult, nil
}

workerDeploy := workerDepl.GetDeployment()
if workerDeploy.Generation == workerDeploy.Status.ObservedGeneration {
instance.Status.WorkerReadyCount = workerDeploy.Status.ReadyReplicas
if deployment.IsReady(workerDeploy) {
instance.Status.Conditions.MarkTrue(
neutronv1beta1.NeutronWorkerReadyCondition,
neutronv1beta1.NeutronWorkerReadyMessage)
} else {
instance.Status.Conditions.Set(condition.FalseCondition(
neutronv1beta1.NeutronWorkerReadyCondition,
condition.RequestedReason,
condition.SeverityInfo,
neutronv1beta1.NeutronWorkerReadyRunningMessage))
}
}
} else {
// Legacy Eventlet strategy: clean up any neutron-rpc/neutron-worker
// Deployments left over from a previous wsgi=true reconcile (e.g. the
// openstack-operator flipped the annotation back), and drop their
// conditions so they don't affect the aggregate Ready condition.
for _, name := range []string{
fmt.Sprintf("%s-%s", neutronapi.ServiceName, neutronapi.RPCDeploymentSuffix),
fmt.Sprintf("%s-%s", neutronapi.ServiceName, neutronapi.WorkerDeploymentSuffix),
} {
existing := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: instance.Namespace},
}
if err := helper.GetClient().Delete(ctx, existing); err != nil && !k8s_errors.IsNotFound(err) {
return ctrl.Result{}, err
}
}
instance.Status.RPCReadyCount = 0
instance.Status.WorkerReadyCount = 0
instance.Status.Conditions.Remove(neutronv1beta1.NeutronRPCReadyCondition)
instance.Status.Conditions.Remove(neutronv1beta1.NeutronWorkerReadyCondition)
}

if instance.Status.ReadyCount > 0 {
// remove finalizers from unused MariaDBAccount records
err = mariadbv1.DeleteUnusedMariaDBAccountFinalizers(
Expand Down Expand Up @@ -1931,6 +2087,7 @@ func (r *NeutronAPIReconciler) generateServiceSecrets(
templateParameters["MemcachedTLS"] = mc.GetMemcachedTLSSupport()
templateParameters["TimeOut"] = instance.Spec.APITimeout
templateParameters["QuorumQueues"] = quorumQueues
templateParameters["WSGI"] = instance.IsWSGI()

notificationsTransportURL, _, err := r.getTransportURL(ctx, h, instance, instance.Status.NotificationsTransportURLSecret)
if err != nil && !errors.Is(err, errTransportURLSecretNameNilOrEmpty) {
Expand Down
9 changes: 9 additions & 0 deletions internal/neutronapi/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ const (

// ACConsumerFinalizer is added to AC secrets that neutron is actively consuming
ACConsumerFinalizer = "openstack.org/neutronapi-ac-consumer"

// RPCDeploymentSuffix is appended to the NeutronAPI name to name the
// neutron-rpc-server Deployment (WSGI strategy only)
RPCDeploymentSuffix = "rpc"

// WorkerDeploymentSuffix is appended to the NeutronAPI name to name the
// background worker (periodic/OVN maintenance) Deployment (WSGI strategy
// only)
WorkerDeploymentSuffix = "worker"
)

// DbsyncPropagation keeps track of the DBSync Service Propagation Type
Expand Down
Loading