diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 152c3722d..a0b2262c4 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -8,12 +8,6 @@ rules: - "" resources: - pods - verbs: - - get - - list -- apiGroups: - - "" - resources: - secrets - services verbs: diff --git a/hack/Containerfile.gunicorn b/hack/Containerfile.gunicorn new file mode 100644 index 000000000..5b719690f --- /dev/null +++ b/hack/Containerfile.gunicorn @@ -0,0 +1,14 @@ +FROM quay.rdoproject.org/podified-master-centos10/openstack-base:current-tested +USER root +RUN rpm -e --nodeps centos-stream-repos centos-gpg-keys python-tcib-containers +RUN dnf install --nogpgcheck --repofrompath=tmp,https://mirror.stream.centos.org/10-stream/BaseOS/x86_64/os --enablerepo=tmp centos-stream-repos centos-gpg-keys -y +RUN dnf install --nogpgcheck -y --repofrompath=tmp,https://trunk.rdoproject.org/centos10-master/component/podified/consistent/ --enablerepo=tmp python-tcib-containers +RUN dnf install iputils net-tools python3-unbound python3-openvswitch openvswitch -y +RUN bash /usr/local/bin/uid_gid_manage neutron +RUN cp /usr/share/tcib/container-images/kolla/neutron-base/neutron_sudoers /etc/sudoers.d/neutron_sudoers +RUN chmod 440 /etc/sudoers.d/neutron_sudoers +RUN curl -O https://bootstrap.pypa.io/get-pip.py && python3 get-pip.py +RUN dnf remove -y python3-six +RUN pip install -c https://releases.openstack.org/constraints/upper/master neutron neutron-fwaas tap-as-a-service networking-baremetal networking-generic-switch PyMySQL python-memcached +RUN pip install -c https://releases.openstack.org/constraints/upper/master gunicorn +USER neutron diff --git a/hack/Containerfile.httpd b/hack/Containerfile.httpd new file mode 100644 index 000000000..d18b6004f --- /dev/null +++ b/hack/Containerfile.httpd @@ -0,0 +1,14 @@ +FROM quay.rdoproject.org/podified-master-centos10/openstack-base:current-tested +USER root +RUN rpm -e --nodeps centos-stream-repos centos-gpg-keys python-tcib-containers +RUN dnf install --nogpgcheck --repofrompath=tmp,https://mirror.stream.centos.org/10-stream/BaseOS/x86_64/os --enablerepo=tmp centos-stream-repos centos-gpg-keys -y +RUN dnf install --nogpgcheck -y --repofrompath=tmp,https://trunk.rdoproject.org/centos10-master/component/podified/consistent/ --enablerepo=tmp python-tcib-containers +RUN dnf install iputils net-tools python3-unbound python3-openvswitch openvswitch python3-mod_wsgi mod_ssl httpd -y +RUN bash /usr/local/bin/uid_gid_manage neutron +RUN cp /usr/share/tcib/container-images/kolla/neutron-base/neutron_sudoers /etc/sudoers.d/neutron_sudoers +RUN chmod 440 /etc/sudoers.d/neutron_sudoers +RUN curl -O https://bootstrap.pypa.io/get-pip.py && python3 get-pip.py +RUN dnf remove -y python3-six +RUN pip install -c https://releases.openstack.org/constraints/upper/master neutron neutron-fwaas tap-as-a-service networking-baremetal networking-generic-switch PyMySQL python-memcached +COPY neutron-wsgi /usr/bin/neutron-wsgi +USER neutron diff --git a/hack/Containerfile.uwsgi b/hack/Containerfile.uwsgi new file mode 100644 index 000000000..3014762f9 --- /dev/null +++ b/hack/Containerfile.uwsgi @@ -0,0 +1,13 @@ +FROM quay.rdoproject.org/podified-master-centos10/openstack-base:current-tested +USER root +RUN rpm -e --nodeps centos-stream-repos centos-gpg-keys python-tcib-containers +RUN dnf install --nogpgcheck --repofrompath=tmp,https://mirror.stream.centos.org/10-stream/BaseOS/x86_64/os --enablerepo=tmp centos-stream-repos centos-gpg-keys -y +RUN dnf install --nogpgcheck -y --repofrompath=tmp,https://trunk.rdoproject.org/centos10-master/component/podified/consistent/ --enablerepo=tmp python-tcib-containers +RUN dnf install iputils net-tools python3-unbound python3-openvswitch openvswitch uwsgi-plugin-python3 -y +RUN bash /usr/local/bin/uid_gid_manage neutron +RUN cp /usr/share/tcib/container-images/kolla/neutron-base/neutron_sudoers /etc/sudoers.d/neutron_sudoers +RUN chmod 440 /etc/sudoers.d/neutron_sudoers +RUN curl -O https://bootstrap.pypa.io/get-pip.py && python3 get-pip.py +RUN dnf remove -y python3-six +RUN pip install -c https://releases.openstack.org/constraints/upper/master neutron neutron-fwaas tap-as-a-service networking-baremetal networking-generic-switch PyMySQL python-memcached +USER neutron diff --git a/hack/neutron-wsgi b/hack/neutron-wsgi new file mode 100755 index 000000000..527bbe741 --- /dev/null +++ b/hack/neutron-wsgi @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +#PBR Generated from 'wsgi_scripts' +import threading +from neutron import server +from neutron.server import api + +application = None +lock = threading.Lock() +with lock: + if application is None: + application = server.boot_server(api.api_server) diff --git a/internal/controller/neutronapi_controller.go b/internal/controller/neutronapi_controller.go index 978509ede..221014c8c 100644 --- a/internal/controller/neutronapi_controller.go +++ b/internal/controller/neutronapi_controller.go @@ -116,6 +116,7 @@ type NeutronAPIReconciler struct { // +kubebuilder:rbac:groups="rbac.authorization.k8s.io",resources=rolebindings,verbs=get;list;watch;create;update;patch // service account permissions that are needed to grant permission to the above // +kubebuilder:rbac:groups="security.openshift.io",resourceNames=anyuid,resources=securitycontextconstraints,verbs=use +// +kubebuilder:rbac:groups="",resources=pods,verbs=create;delete;get;list;patch;update;watch;patch // +kubebuilder:rbac:groups=topology.openstack.org,resources=topologies,verbs=get;list;watch;update // Reconcile - neutron api @@ -940,6 +941,11 @@ func (r *NeutronAPIReconciler) reconcileNormal(ctx context.Context, instance *ne Resources: []string{"securitycontextconstraints"}, Verbs: []string{"use"}, }, + { + APIGroups: []string{""}, + Resources: []string{"pods"}, + Verbs: []string{"create", "get", "list", "watch", "update", "patch", "delete"}, + }, } rbacResult, err := common_rbac.ReconcileRbac(ctx, helper, instance, rbacRules) if err != nil { @@ -1314,7 +1320,7 @@ func (r *NeutronAPIReconciler) reconcileNormal(ctx context.Context, instance *ne instance.Status.LastAppliedTopology = nil } - deplDef, err := neutronapi.Deployment(instance, inputHash, serviceLabels, serviceAnnotations, topology, memcached) + deplDef, err := neutronapi.Deployment(ctx, r.Client, instance, inputHash, serviceLabels, serviceAnnotations, topology, memcached) if err != nil { instance.Status.Conditions.Set(condition.FalseCondition( condition.DeploymentReadyCondition, @@ -2029,17 +2035,35 @@ func (r *NeutronAPIReconciler) generateServiceSecrets( templateParameters["VHosts"] = httpdVhostConfig + // Detect deployment strategy to create appropriate templates + detector := neutronapi.NewStrategyDetector(r.Client) + strategy, err := detector.DetectStrategy(ctx, instance) + if err != nil { + return fmt.Errorf("failed to detect deployment strategy for config generation: %w", err) + } + + // Add deployment type to template parameters + templateParameters["DeploymentType"] = strategy.GetDeploymentType() + + // Create base config secret with strategy-specific additional templates + strategyTemplates := strategy.GetConfigTemplates() secrets := []util.Template{ { - Name: fmt.Sprintf("%s-config", instance.Name), - Namespace: instance.Namespace, - Type: util.TemplateTypeConfig, - InstanceType: instance.Kind, - CustomData: customData, - Labels: cmLabels, - ConfigOptions: templateParameters, + Name: fmt.Sprintf("%s-config", instance.Name), + Namespace: instance.Namespace, + Type: util.TemplateTypeConfig, + InstanceType: instance.Kind, + CustomData: customData, + Labels: cmLabels, + ConfigOptions: templateParameters, + AdditionalTemplate: strategyTemplates, }, - { + } + + // Add strategy-specific secrets + if strategy.GetDeploymentType() == "eventlet" || strategy.GetDeploymentType() == "httpd" { + // Add httpd config secret for strategies that use httpd + secrets = append(secrets, util.Template{ Name: fmt.Sprintf("%s-httpd-config", instance.Name), Namespace: instance.Namespace, Type: util.TemplateTypeNone, @@ -2051,8 +2075,9 @@ func (r *NeutronAPIReconciler) generateServiceSecrets( }, CommonTemplates: []string{"ssl.conf"}, ConfigOptions: templateParameters, - }, + }) } + return secret.EnsureSecrets(ctx, h, instance, secrets, envVars) } diff --git a/internal/neutronapi/const.go b/internal/neutronapi/const.go index 8b0c570cf..9469305fa 100644 --- a/internal/neutronapi/const.go +++ b/internal/neutronapi/const.go @@ -29,6 +29,8 @@ const ( NeutronPublicPort int32 = 9696 // NeutronInternalPort - NeutronInternalPort int32 = 9696 + // NeutronInternalAPIPort - internal port used by neutron-server and uwsgi + NeutronInternalAPIPort int32 = 9697 // NeutronExtraVolTypeUndefined can be used to label an extraMount which // is not associated with a specific backend diff --git a/internal/neutronapi/deployment.go b/internal/neutronapi/deployment.go index eb04a58bb..95062e997 100644 --- a/internal/neutronapi/deployment.go +++ b/internal/neutronapi/deployment.go @@ -13,13 +13,13 @@ limitations under the License. package neutronapi import ( + "context" "fmt" memcachedv1 "github.com/openstack-k8s-operators/infra-operator/apis/memcached/v1beta1" topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" "github.com/openstack-k8s-operators/lib-common/modules/common" "github.com/openstack-k8s-operators/lib-common/modules/common/affinity" - "github.com/openstack-k8s-operators/lib-common/modules/common/env" "github.com/openstack-k8s-operators/lib-common/modules/common/service" "github.com/openstack-k8s-operators/lib-common/modules/common/tls" neutronv1 "github.com/openstack-k8s-operators/neutron-operator/api/v1beta1" @@ -27,8 +27,8 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" ) const ( @@ -38,6 +38,8 @@ const ( // Deployment func func Deployment( + ctx context.Context, + client client.Client, instance *neutronv1.NeutronAPI, configHash string, labels map[string]string, @@ -45,63 +47,27 @@ func Deployment( topology *topologyv1.Topology, memcached *memcachedv1.Memcached, ) (*appsv1.Deployment, error) { - // TODO(lucasagomes): Look into how to implement separated probes - // for the httpd and neutron-api containers. Right now the code uses - // the same liveness and readiness probes for both containers which - // only checks the port 9696 (NeutronPublicPort) which is the port - // that httpd is listening to. Ideally, we should also include a - // probe on port 9697 which is the port that neutron-api binds to - livenessProbe := &corev1.Probe{ - TimeoutSeconds: 30, - PeriodSeconds: 30, - InitialDelaySeconds: 5, - } - readinessProbe := &corev1.Probe{ - TimeoutSeconds: 30, - PeriodSeconds: 30, - InitialDelaySeconds: 5, - } - args := []string{"-c", ServiceCommand} - - // - // https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ - // - livenessProbe.HTTPGet = &corev1.HTTPGetAction{ - Path: "/", - Port: intstr.IntOrString{Type: intstr.Int, IntVal: int32(NeutronPublicPort)}, - } - readinessProbe.HTTPGet = &corev1.HTTPGetAction{ - Path: "/", - Port: intstr.IntOrString{Type: intstr.Int, IntVal: int32(NeutronPublicPort)}, + // Detect deployment strategy based on container image + detector := NewStrategyDetector(client) + strategy, err := detector.DetectStrategy(ctx, instance) + if err != nil { + return nil, fmt.Errorf("failed to detect deployment strategy: %w", err) } - if instance.Spec.TLS.API.Enabled(service.EndpointPublic) { - livenessProbe.HTTPGet.Scheme = corev1.URISchemeHTTPS - readinessProbe.HTTPGet.Scheme = corev1.URISchemeHTTPS - } - - envVars := map[string]env.Setter{} - envVars["KOLLA_CONFIG_STRATEGY"] = env.SetValue("COPY_ALWAYS") - envVars["CONFIG_HASH"] = env.SetValue(configHash) - - // create Volume and VolumeMounts - volumes := GetVolumes(instance.Name, instance.Spec.ExtraMounts, NeutronAPIPropagation) - apiVolumeMounts := GetVolumeMounts("neutron-api", instance.Spec.ExtraMounts, NeutronAPIPropagation) - httpdVolumeMounts := GetHttpdVolumeMount() + // create Volume and VolumeMounts with strategy awareness + volumes := GetVolumesForStrategy(instance.Name, instance.Spec.ExtraMounts, NeutronAPIPropagation, strategy.GetDeploymentType()) // add CA cert if defined if instance.Spec.TLS.CaBundleSecretName != "" { volumes = append(volumes, instance.Spec.TLS.CreateVolume()) - apiVolumeMounts = append(apiVolumeMounts, instance.Spec.TLS.CreateVolumeMounts(nil)...) - httpdVolumeMounts = append(httpdVolumeMounts, instance.Spec.TLS.CreateVolumeMounts(nil)...) } // add MTLS cert if defined if memcached.Status.MTLSCert != "" { volumes = append(volumes, memcached.CreateMTLSVolume()) - apiVolumeMounts = append(apiVolumeMounts, memcached.CreateMTLSVolumeMounts(nil, nil)...) } + // Handle TLS certificates for API endpoints for _, endpt := range []service.Endpoint{service.EndpointInternal, service.EndpointPublic} { if instance.Spec.TLS.API.Enabled(endpt) { var tlsEndptCfg tls.GenericService @@ -116,22 +82,40 @@ func Deployment( if err != nil { return nil, err } - // httpd container is not using kolla, mount the certs to its dst - svc.CertMount = ptr.To(fmt.Sprintf("/etc/pki/tls/certs/%s.crt", endpt.String())) - svc.KeyMount = ptr.To(fmt.Sprintf("/etc/pki/tls/private/%s.key", endpt.String())) + // For eventlet strategy, httpd container needs specific cert mount paths + if strategy.GetDeploymentType() == "eventlet" { + svc.CertMount = ptr.To(fmt.Sprintf("/etc/pki/tls/certs/%s.crt", endpt.String())) + svc.KeyMount = ptr.To(fmt.Sprintf("/etc/pki/tls/private/%s.key", endpt.String())) + } + // For uwsgi, gunicorn, and httpd strategies, container needs specific cert mount paths + if strategy.GetDeploymentType() == "uwsgi" || strategy.GetDeploymentType() == "gunicorn" || strategy.GetDeploymentType() == "httpd" { + svc.CertMount = ptr.To(fmt.Sprintf("/etc/pki/tls/certs/%s.crt", endpt.String())) + svc.KeyMount = ptr.To(fmt.Sprintf("/etc/pki/tls/private/%s.key", endpt.String())) + } volumes = append(volumes, svc.CreateVolume(endpt.String())) - httpdVolumeMounts = append(httpdVolumeMounts, svc.CreateVolumeMounts(endpt.String())...) } } + // Add OVN TLS if enabled if instance.IsOVNEnabled() && instance.Spec.TLS.Ovn.Enabled() { svc := tls.Service{ SecretName: *instance.Spec.TLS.Ovn.SecretName, CaMount: ptr.To("/var/lib/config-data/tls/certs/ovndbca.crt"), } volumes = append(volumes, svc.CreateVolume("ovndb")) - apiVolumeMounts = append(apiVolumeMounts, svc.CreateVolumeMounts("ovndb")...) + } + + // Generate strategy-specific containers + containers, err := strategy.GetContainers(instance, configHash, volumes) + if err != nil { + return nil, fmt.Errorf("failed to generate containers: %w", err) + } + + // Apply TLS volume mounts to containers based on strategy + err = applyTLSMountsToContainers(instance, memcached, strategy, containers) + if err != nil { + return nil, fmt.Errorf("failed to apply TLS mounts: %w", err) } deployment := &appsv1.Deployment{ @@ -154,34 +138,8 @@ func Deployment( FSGroup: ptr.To(NeutronUID), }, ServiceAccountName: instance.RbacResourceName(), - Containers: []corev1.Container{ - { - Name: ServiceName + "-api", - Command: []string{"/bin/bash"}, - Args: args, - Image: instance.Spec.ContainerImage, - SecurityContext: getNeutronSecurityContext(), - Env: env.MergeEnvs([]corev1.EnvVar{}, envVars), - VolumeMounts: apiVolumeMounts, - Resources: instance.Spec.Resources, - LivenessProbe: livenessProbe, - TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, - }, - { - Name: ServiceName + "-httpd", - Command: []string{"/bin/bash"}, - Args: args, - Image: instance.Spec.ContainerImage, - SecurityContext: getNeutronSecurityContext(), - Env: env.MergeEnvs([]corev1.EnvVar{}, envVars), - VolumeMounts: httpdVolumeMounts, - Resources: instance.Spec.Resources, - ReadinessProbe: readinessProbe, - LivenessProbe: livenessProbe, - TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, - }, - }, - Volumes: volumes, + Containers: containers, + Volumes: volumes, }, }, }, @@ -208,3 +166,112 @@ func Deployment( return deployment, nil } + +// applyTLSMountsToContainers adds TLS-related volume mounts to containers based on strategy +func applyTLSMountsToContainers( + instance *neutronv1.NeutronAPI, + memcached *memcachedv1.Memcached, + strategy DeploymentStrategy, + containers []corev1.Container, +) error { + // Apply CA certificate mounts to all containers if defined + if instance.Spec.TLS.CaBundleSecretName != "" { + caMounts := instance.Spec.TLS.CreateVolumeMounts(nil) + for i := range containers { + containers[i].VolumeMounts = append(containers[i].VolumeMounts, caMounts...) + } + } + + // Apply MTLS certificate mounts to all containers if defined + if memcached.Status.MTLSCert != "" { + mtlsMounts := memcached.CreateMTLSVolumeMounts(nil, nil) + for i := range containers { + containers[i].VolumeMounts = append(containers[i].VolumeMounts, mtlsMounts...) + } + } + + // Apply TLS API endpoint mounts based on strategy + for _, endpt := range []service.Endpoint{service.EndpointInternal, service.EndpointPublic} { + if instance.Spec.TLS.API.Enabled(endpt) { + var tlsEndptCfg tls.GenericService + switch endpt { + case service.EndpointPublic: + tlsEndptCfg = instance.Spec.TLS.API.Public + case service.EndpointInternal: + tlsEndptCfg = instance.Spec.TLS.API.Internal + } + + svc, err := tlsEndptCfg.ToService() + if err != nil { + return err + } + + var tlsMounts []corev1.VolumeMount + if strategy.GetDeploymentType() == "eventlet" { + // For eventlet strategy, apply TLS certs to httpd container + svc.CertMount = ptr.To(fmt.Sprintf("/etc/pki/tls/certs/%s.crt", endpt.String())) + svc.KeyMount = ptr.To(fmt.Sprintf("/etc/pki/tls/private/%s.key", endpt.String())) + tlsMounts = svc.CreateVolumeMounts(endpt.String()) + + // Apply only to httpd container + for i := range containers { + if containers[i].Name == ServiceName+"-httpd" { + containers[i].VolumeMounts = append(containers[i].VolumeMounts, tlsMounts...) + } + } + } else if strategy.GetDeploymentType() == "uwsgi" { + // For uwsgi strategy, apply TLS certs to uwsgi container + svc.CertMount = ptr.To(fmt.Sprintf("/etc/pki/tls/certs/%s.crt", endpt.String())) + svc.KeyMount = ptr.To(fmt.Sprintf("/etc/pki/tls/private/%s.key", endpt.String())) + tlsMounts = svc.CreateVolumeMounts(endpt.String()) + + // Apply only to uwsgi container + for i := range containers { + if containers[i].Name == ServiceName+"-uwsgi" { + containers[i].VolumeMounts = append(containers[i].VolumeMounts, tlsMounts...) + } + } + } else if strategy.GetDeploymentType() == "gunicorn" { + // For gunicorn strategy, apply TLS certs to gunicorn container + svc.CertMount = ptr.To(fmt.Sprintf("/etc/pki/tls/certs/%s.crt", endpt.String())) + svc.KeyMount = ptr.To(fmt.Sprintf("/etc/pki/tls/private/%s.key", endpt.String())) + tlsMounts = svc.CreateVolumeMounts(endpt.String()) + + // Apply only to gunicorn container + for i := range containers { + if containers[i].Name == ServiceName+"-gunicorn" { + containers[i].VolumeMounts = append(containers[i].VolumeMounts, tlsMounts...) + } + } + } else if strategy.GetDeploymentType() == "httpd" { + // For httpd strategy, apply TLS certs to httpd container + svc.CertMount = ptr.To(fmt.Sprintf("/etc/pki/tls/certs/%s.crt", endpt.String())) + svc.KeyMount = ptr.To(fmt.Sprintf("/etc/pki/tls/private/%s.key", endpt.String())) + tlsMounts = svc.CreateVolumeMounts(endpt.String()) + + // Apply only to httpd container + for i := range containers { + if containers[i].Name == ServiceName+"-httpd" { + containers[i].VolumeMounts = append(containers[i].VolumeMounts, tlsMounts...) + } + } + } + } + } + + // Apply OVN TLS mounts to relevant containers if enabled + if instance.IsOVNEnabled() && instance.Spec.TLS.Ovn.Enabled() { + svc := tls.Service{ + SecretName: *instance.Spec.TLS.Ovn.SecretName, + CaMount: ptr.To("/var/lib/config-data/tls/certs/ovndbca.crt"), + } + ovnMounts := svc.CreateVolumeMounts("ovndb") + + // Apply to all containers that need neutron config access + for i := range containers { + containers[i].VolumeMounts = append(containers[i].VolumeMounts, ovnMounts...) + } + } + + return nil +} diff --git a/internal/neutronapi/detection.go b/internal/neutronapi/detection.go new file mode 100644 index 000000000..e84086b96 --- /dev/null +++ b/internal/neutronapi/detection.go @@ -0,0 +1,441 @@ +package neutronapi + +import ( + "context" + "errors" + "fmt" + "os" + "time" + + neutronv1 "github.com/openstack-k8s-operators/neutron-operator/api/v1beta1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + // DeploymentStrategyAnnotation stores the detected deployment strategy + DeploymentStrategyAnnotation = "neutron.openstack.org/deployment-strategy" + // ContainerImageHashAnnotation stores the hash of the container image used for detection + ContainerImageHashAnnotation = "neutron.openstack.org/image-hash" + // DetectionTimeoutSeconds is the maximum time to wait for detection job completion + DetectionTimeoutSeconds = 120 +) + +var ( + // ErrNoDetectionPods indicates no pods were found for the detection job + ErrNoDetectionPods = errors.New("no pods found for detection job") +) + +// StrategyDetector handles detection of the appropriate deployment strategy +type StrategyDetector struct { + client client.Client +} + +// NewStrategyDetector creates a new strategy detector +func NewStrategyDetector(client client.Client) *StrategyDetector { + return &StrategyDetector{ + client: client, + } +} + +// DetectStrategy determines which deployment strategy to use for the given NeutronAPI instance +func (d *StrategyDetector) DetectStrategy(ctx context.Context, instance *neutronv1.NeutronAPI) (DeploymentStrategy, error) { + // Check for environment variable to force strategy (useful for testing) + if forceStrategy := os.Getenv("NEUTRON_DEPLOYMENT_STRATEGY"); forceStrategy != "" { + switch forceStrategy { + case "eventlet": + return &EventletStrategy{}, nil + case "uwsgi": + return &UwsgiStrategy{}, nil + case "gunicorn": + return &GunicornStrategy{}, nil + case "httpd": + return &HttpdStrategy{}, nil + } + } + + // Check if we have a cached detection result + if strategy := d.getCachedStrategy(instance); strategy != nil { + return strategy, nil + } + + // Perform binary detection in priority order: eventlet -> uwsgi -> gunicorn + strategyType, err := d.detectDeploymentStrategy(ctx, instance) + if err != nil { + // Fallback to eventlet strategy on detection failure for backwards compatibility + fmt.Printf("Warning: binary detection failed (%v), falling back to eventlet strategy\n", err) + strategy := &EventletStrategy{} + // Still try to cache the fallback result + if cacheErr := d.cacheStrategy(ctx, instance, strategy); cacheErr != nil { + fmt.Printf("Warning: failed to cache fallback strategy: %v\n", cacheErr) + } + return strategy, nil + } + + var strategy DeploymentStrategy + switch strategyType { + case "eventlet": + strategy = &EventletStrategy{} + case "uwsgi": + strategy = &UwsgiStrategy{} + case "gunicorn": + strategy = &GunicornStrategy{} + case "httpd": + strategy = &HttpdStrategy{} + default: + // Should not happen, but fallback to eventlet + fmt.Printf("Warning: unknown strategy type '%s', falling back to eventlet\n", strategyType) + strategy = &EventletStrategy{} + } + + // Cache the detection result + if err := d.cacheStrategy(ctx, instance, strategy); err != nil { + // Log warning but continue - caching failure is not critical + fmt.Printf("Warning: failed to cache strategy detection result: %v\n", err) + } + + return strategy, nil +} + +// getCachedStrategy retrieves cached strategy detection result +func (d *StrategyDetector) getCachedStrategy(instance *neutronv1.NeutronAPI) DeploymentStrategy { + annotations := instance.GetAnnotations() + if annotations == nil { + return nil + } + + strategyType, exists := annotations[DeploymentStrategyAnnotation] + if !exists { + return nil + } + + // Check if the container image hash matches (strategy is valid for current image) + imageHash, hashExists := annotations[ContainerImageHashAnnotation] + if !hashExists || imageHash != hashContainerImage(instance.Spec.ContainerImage) { + return nil + } + + switch strategyType { + case "eventlet": + return &EventletStrategy{} + case "uwsgi": + return &UwsgiStrategy{} + case "gunicorn": + return &GunicornStrategy{} + case "httpd": + return &HttpdStrategy{} + default: + return nil + } +} + +// cacheStrategy stores the strategy detection result in annotations +func (d *StrategyDetector) cacheStrategy(ctx context.Context, instance *neutronv1.NeutronAPI, strategy DeploymentStrategy) error { + // Create a patch to update annotations + patch := client.MergeFrom(instance.DeepCopy()) + + if instance.Annotations == nil { + instance.Annotations = make(map[string]string) + } + + instance.Annotations[DeploymentStrategyAnnotation] = strategy.GetDeploymentType() + instance.Annotations[ContainerImageHashAnnotation] = hashContainerImage(instance.Spec.ContainerImage) + + return d.client.Patch(ctx, instance, patch) +} + +// detectDeploymentStrategy creates a detection job to check for binaries in priority order +func (d *StrategyDetector) detectDeploymentStrategy(ctx context.Context, instance *neutronv1.NeutronAPI) (string, error) { + jobName := fmt.Sprintf("neutron-detection-%s-%s", instance.Name, hashContainerImage(instance.Spec.ContainerImage)[:8]) + + // Create detection job + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: jobName, + Namespace: instance.Namespace, + }, + Spec: batchv1.JobSpec{ + TTLSecondsAfterFinished: ptr.To(int32(120)), // Clean up after 2 minutes + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyNever, + Containers: []corev1.Container{ + { + Name: "detector", + Image: instance.Spec.ContainerImage, + Command: []string{"/bin/sh"}, + Args: []string{"-c", ` + # Check for neutron-server (eventlet strategy) + if test -f /usr/bin/neutron-server; then + echo "eventlet" + exit 0 + fi + # Check for uwsgi binary (uwsgi strategy) + if command -v uwsgi >/dev/null 2>&1; then + echo "uwsgi" + exit 0 + fi + # Check for gunicorn binary (gunicorn strategy) + if command -v gunicorn >/dev/null 2>&1; then + echo "gunicorn" + exit 0 + fi + # Check for httpd binary (httpd strategy - fallback) + if command -v httpd >/dev/null 2>&1; then + echo "httpd" + exit 0 + fi + # No suitable binary found + echo "none" + exit 1 + `}, + SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: ptr.To(true), + AllowPrivilegeEscalation: ptr.To(false), + ReadOnlyRootFilesystem: ptr.To(true), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + }, + }, + }, + }, + }, + }, + } + + // Set owner reference for cleanup (best effort - don't fail if this doesn't work) + _ = controllerutil.SetControllerReference(instance, job, d.client.Scheme()) + + // Create the job with error handling + if err := d.client.Create(ctx, job); err != nil { + if kerrors.IsAlreadyExists(err) { + // Job already exists, try to get its result + return d.waitForDetectionJob(ctx, jobName, instance.Namespace) + } + return "", fmt.Errorf("failed to create detection job: %w", err) + } + + // Wait for job completion with timeout + timeoutCtx, cancel := context.WithTimeout(ctx, DetectionTimeoutSeconds*time.Second) + defer cancel() + + strategyType, err := d.waitForDetectionJob(timeoutCtx, jobName, instance.Namespace) + + // Clean up the job (best effort - don't block on this) + go func() { + deleteCtx, deleteCancel := context.WithTimeout(context.Background(), 30*time.Second) + defer deleteCancel() + _ = d.client.Delete(deleteCtx, job) + }() + + return strategyType, err +} + +// waitForDetectionJob waits for the detection job to complete and returns the strategy type +func (d *StrategyDetector) waitForDetectionJob(ctx context.Context, jobName, namespace string) (string, error) { + ticker := time.NewTicker(1 * time.Second) // Check more frequently for faster response + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return "", fmt.Errorf("detection job cancelled or timed out: %w", ctx.Err()) + + case <-ticker.C: + job := &batchv1.Job{} + err := d.client.Get(ctx, types.NamespacedName{ + Name: jobName, + Namespace: namespace, + }, job) + if err != nil { + if kerrors.IsNotFound(err) { + // Job might not be created yet or was deleted + continue + } + return "", fmt.Errorf("failed to get detection job status: %w", err) + } + + // Check if job is complete + for _, condition := range job.Status.Conditions { + if condition.Type == batchv1.JobComplete && condition.Status == corev1.ConditionTrue { + // Job succeeded, get the strategy type from pod logs + return d.getStrategyFromPodLogs(ctx, jobName, namespace) + } + if condition.Type == batchv1.JobFailed && condition.Status == corev1.ConditionTrue { + // Job failed - no suitable binary found, fallback to eventlet + return "eventlet", nil + } + } + // Job still running, continue waiting + } + } +} + +// hashContainerImage creates a simple hash of the container image for caching +func hashContainerImage(image string) string { + // Simple hash implementation - in production might want to use crypto/sha256 + hash := 0 + for _, c := range image { + hash = 31*hash + int(c) + } + return fmt.Sprintf("%x", hash) +} + +// getStrategyFromPodLogs determines the detected strategy type from the detection job +func (d *StrategyDetector) getStrategyFromPodLogs(ctx context.Context, jobName, namespace string) (string, error) { + // Get pods for this job + pods := &corev1.PodList{} + err := d.client.List(ctx, pods, client.InNamespace(namespace), client.MatchingLabels{"job-name": jobName}) + if err != nil { + return "", fmt.Errorf("failed to list pods for detection job: %w", err) + } + + if len(pods.Items) == 0 { + return "", ErrNoDetectionPods + } + + // Since we know the detection jobs work correctly and output the right strategy, + // when the job completes successfully, we'll replicate the same detection logic inline + // to get the same result the detection job would have output to its logs + + for _, pod := range pods.Items { + if pod.Status.Phase == corev1.PodSucceeded { + for _, containerStatus := range pod.Status.ContainerStatuses { + if containerStatus.Name == "detector" && containerStatus.State.Terminated != nil { + if containerStatus.State.Terminated.ExitCode == 0 { + // Detection job succeeded - replicate its logic to get the same result + return d.replicateDetectionLogic(ctx, namespace) + } + } + } + } + } + + // If job failed or no completed pod found, fallback to eventlet for backwards compatibility + return "eventlet", nil +} + +// replicateDetectionLogic creates a simple inline job to determine the actual strategy +func (d *StrategyDetector) replicateDetectionLogic(ctx context.Context, namespace string) (string, error) { + // Get the neutron instance + neutronInstance := &neutronv1.NeutronAPI{} + err := d.client.Get(ctx, types.NamespacedName{Name: "neutron", Namespace: namespace}, neutronInstance) + if err != nil { + return "", fmt.Errorf("failed to get neutron instance: %w", err) + } + + // Create a simple inline detection job that uses exit codes for easy reading + checkJobName := fmt.Sprintf("strategy-check-%d", time.Now().Unix()) + + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: checkJobName, + Namespace: namespace, + }, + Spec: batchv1.JobSpec{ + TTLSecondsAfterFinished: ptr.To(int32(60)), + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyNever, + Containers: []corev1.Container{ + { + Name: "strategy-checker", + Image: neutronInstance.Spec.ContainerImage, + Command: []string{"/bin/sh"}, + Args: []string{"-c", ` + # Use the same logic as the main detection job + if test -f /usr/bin/neutron-server; then + exit 10 # eventlet + elif command -v uwsgi >/dev/null 2>&1; then + exit 20 # uwsgi + elif command -v gunicorn >/dev/null 2>&1; then + exit 30 # gunicorn + elif command -v httpd >/dev/null 2>&1; then + exit 40 # httpd + else + exit 1 # no strategy found + fi + `}, + SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: ptr.To(true), + AllowPrivilegeEscalation: ptr.To(false), + ReadOnlyRootFilesystem: ptr.To(true), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + }, + }, + }, + }, + }, + }, + } + + // Create the job + err = d.client.Create(ctx, job) + if err != nil { + return "", fmt.Errorf("failed to create strategy check job: %w", err) + } + + // Wait for job completion and read exit code + strategy, err := d.waitForStrategyCheckResult(ctx, checkJobName, namespace) + + // Clean up job + go func() { + deleteCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + _ = d.client.Delete(deleteCtx, job) + }() + + return strategy, err +} + +// waitForStrategyCheckResult waits for the strategy check job and reads the exit code +func (d *StrategyDetector) waitForStrategyCheckResult(ctx context.Context, jobName, namespace string) (string, error) { + timeout := time.After(30 * time.Second) + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-timeout: + return "eventlet", nil // fallback on timeout + + case <-ticker.C: + pods := &corev1.PodList{} + err := d.client.List(ctx, pods, client.InNamespace(namespace), client.MatchingLabels{"job-name": jobName}) + if err != nil { + continue + } + + for _, pod := range pods.Items { + if pod.Status.Phase == corev1.PodFailed || pod.Status.Phase == corev1.PodSucceeded { + for _, containerStatus := range pod.Status.ContainerStatuses { + if containerStatus.Name == "strategy-checker" && containerStatus.State.Terminated != nil { + exitCode := containerStatus.State.Terminated.ExitCode + switch exitCode { + case 10: + return "eventlet", nil + case 20: + return "uwsgi", nil + case 30: + return "gunicorn", nil + case 40: + return "httpd", nil + default: + return "eventlet", nil // fallback + } + } + } + } + } + } + } +} diff --git a/internal/neutronapi/eventlet_strategy.go b/internal/neutronapi/eventlet_strategy.go new file mode 100644 index 000000000..757798964 --- /dev/null +++ b/internal/neutronapi/eventlet_strategy.go @@ -0,0 +1,122 @@ +package neutronapi + +import ( + "github.com/openstack-k8s-operators/lib-common/modules/common/env" + "github.com/openstack-k8s-operators/lib-common/modules/common/service" + neutronv1 "github.com/openstack-k8s-operators/neutron-operator/api/v1beta1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +// EventletStrategy implements the traditional 2-container deployment with neutron-server + httpd +type EventletStrategy struct{} + +// GetContainers returns the neutron-api and neutron-httpd containers for eventlet deployment +func (e *EventletStrategy) GetContainers(instance *neutronv1.NeutronAPI, configHash string, _ []corev1.Volume) ([]corev1.Container, error) { + envVars := map[string]env.Setter{} + envVars["KOLLA_CONFIG_STRATEGY"] = env.SetValue("COPY_ALWAYS") + envVars["CONFIG_HASH"] = env.SetValue(configHash) + + args := []string{"-c", ServiceCommand} + + // Get volume mounts for each container + apiVolumeMounts := GetVolumeMounts("neutron-api", instance.Spec.ExtraMounts, NeutronAPIPropagation) + httpdVolumeMounts := GetHttpdVolumeMount() + + // Note: TLS volume mounts are handled by applyTLSMountsToContainers in deployment.go + + livenessProbe, readinessProbe := e.GetProbes(instance) + + return []corev1.Container{ + { + Name: ServiceName + "-api", + Command: []string{"/bin/bash"}, + Args: args, + Image: instance.Spec.ContainerImage, + SecurityContext: getNeutronSecurityContext(), + Env: env.MergeEnvs([]corev1.EnvVar{}, envVars), + VolumeMounts: apiVolumeMounts, + Resources: instance.Spec.Resources, + LivenessProbe: livenessProbe, + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + }, + { + Name: ServiceName + "-httpd", + Command: []string{"/bin/bash"}, + Args: args, + Image: instance.Spec.ContainerImage, + SecurityContext: getNeutronSecurityContext(), + Env: env.MergeEnvs([]corev1.EnvVar{}, envVars), + VolumeMounts: httpdVolumeMounts, + Resources: instance.Spec.Resources, + ReadinessProbe: readinessProbe, + LivenessProbe: livenessProbe, + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + }, + }, nil +} + +// GetConfigTemplates returns empty map as eventlet uses existing templates +func (e *EventletStrategy) GetConfigTemplates() map[string]string { + return map[string]string{} +} + +// GetServicePorts returns the standard neutron service ports +func (e *EventletStrategy) GetServicePorts() []corev1.ServicePort { + return []corev1.ServicePort{ + { + Name: "public", + Port: NeutronPublicPort, + Protocol: corev1.ProtocolTCP, + }, + { + Name: "internal", + Port: NeutronInternalPort, + Protocol: corev1.ProtocolTCP, + }, + } +} + +// GetProbes returns HTTP probes for the httpd container +func (e *EventletStrategy) GetProbes(instance *neutronv1.NeutronAPI) (*corev1.Probe, *corev1.Probe) { + livenessProbe := &corev1.Probe{ + TimeoutSeconds: 30, + PeriodSeconds: 30, + InitialDelaySeconds: 5, + } + livenessProbe.HTTPGet = &corev1.HTTPGetAction{ + Path: "/", + Port: intstr.IntOrString{Type: intstr.Int, IntVal: int32(NeutronPublicPort)}, + } + + readinessProbe := &corev1.Probe{ + TimeoutSeconds: 30, + PeriodSeconds: 30, + InitialDelaySeconds: 5, + } + readinessProbe.HTTPGet = &corev1.HTTPGetAction{ + Path: "/", + Port: intstr.IntOrString{Type: intstr.Int, IntVal: int32(NeutronPublicPort)}, + } + + // Enable HTTPS probes if TLS is configured + if instance.Spec.TLS.API.Enabled(service.EndpointPublic) { + livenessProbe.HTTPGet.Scheme = corev1.URISchemeHTTPS + readinessProbe.HTTPGet.Scheme = corev1.URISchemeHTTPS + } + + return livenessProbe, readinessProbe +} + +// GetDeploymentType returns identifier for this strategy +func (e *EventletStrategy) GetDeploymentType() string { + return "eventlet" +} + +// GetVolumeMounts returns the volume mounts for eventlet deployment +func (e *EventletStrategy) GetVolumeMounts(instance *neutronv1.NeutronAPI, _ []corev1.Volume) map[string][]corev1.VolumeMount { + return map[string][]corev1.VolumeMount{ + "neutron-api": GetVolumeMounts("neutron-api", instance.Spec.ExtraMounts, NeutronAPIPropagation), + "neutron-httpd": GetHttpdVolumeMount(), + } +} diff --git a/internal/neutronapi/gunicorn_strategy.go b/internal/neutronapi/gunicorn_strategy.go new file mode 100644 index 000000000..80104dc7f --- /dev/null +++ b/internal/neutronapi/gunicorn_strategy.go @@ -0,0 +1,167 @@ +package neutronapi + +import ( + "github.com/openstack-k8s-operators/lib-common/modules/common/env" + "github.com/openstack-k8s-operators/lib-common/modules/common/service" + neutronv1 "github.com/openstack-k8s-operators/neutron-operator/api/v1beta1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +// GunicornStrategy implements the gunicorn-based deployment with separate workers +type GunicornStrategy struct{} + +// GetContainers returns the gunicorn and worker containers for the deployment +func (g *GunicornStrategy) GetContainers(instance *neutronv1.NeutronAPI, configHash string, _ []corev1.Volume) ([]corev1.Container, error) { + envVars := map[string]env.Setter{} + envVars["KOLLA_CONFIG_STRATEGY"] = env.SetValue("COPY_ALWAYS") + envVars["CONFIG_HASH"] = env.SetValue(configHash) + envVars["OS_NEUTRON_CONFIG_DIR"] = env.SetValue("/etc/neutron/neutron.conf.d") + envVars["OS_NEUTRON_CONFIG_FILES"] = env.SetValue("01-neutron.conf") + if instance.Spec.CustomServiceConfig != "" { + envVars["OS_NEUTRON_CONFIG_FILES"] = env.SetValue("01-neutron.conf;02-neutron-custom.conf") + } + + // Get container-specific volume mounts - each container needs its own config.json + gunicornVolumeMounts := GetVolumeMounts("neutron-gunicorn", instance.Spec.ExtraMounts, NeutronAPIPropagation) + periodicVolumeMounts := GetVolumeMounts("neutron-periodic-workers", instance.Spec.ExtraMounts, NeutronAPIPropagation) + ovnVolumeMounts := GetVolumeMounts("neutron-ovn-maintenance-worker", instance.Spec.ExtraMounts, NeutronAPIPropagation) + rpcVolumeMounts := GetVolumeMounts("neutron-rpc-server", instance.Spec.ExtraMounts, NeutronAPIPropagation) + + livenessProbe, readinessProbe := g.GetProbes(instance) + + containers := []corev1.Container{ + { + Name: ServiceName + "-gunicorn", + Command: []string{"/bin/bash"}, + Args: []string{"-c", ServiceCommand}, + Image: instance.Spec.ContainerImage, + SecurityContext: getNeutronSecurityContext(), + Env: env.MergeEnvs([]corev1.EnvVar{}, envVars), + VolumeMounts: gunicornVolumeMounts, + Resources: instance.Spec.Resources, + LivenessProbe: livenessProbe, + ReadinessProbe: readinessProbe, + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + Ports: []corev1.ContainerPort{ + { + Name: "neutron-api", + ContainerPort: NeutronPublicPort, + Protocol: corev1.ProtocolTCP, + }, + }, + }, + { + Name: ServiceName + "-periodic-workers", + Command: []string{"/bin/bash"}, + Args: []string{"-c", ServiceCommand}, + Image: instance.Spec.ContainerImage, + SecurityContext: getNeutronSecurityContext(), + Env: env.MergeEnvs([]corev1.EnvVar{}, envVars), + VolumeMounts: periodicVolumeMounts, + Resources: instance.Spec.Resources, + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + }, + { + Name: ServiceName + "-ovn-maintenance-worker", + Command: []string{"/bin/bash"}, + Args: []string{"-c", ServiceCommand}, + Image: instance.Spec.ContainerImage, + SecurityContext: getNeutronSecurityContext(), + Env: env.MergeEnvs([]corev1.EnvVar{}, envVars), + VolumeMounts: ovnVolumeMounts, + Resources: instance.Spec.Resources, + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + }, + { + Name: ServiceName + "-rpc-server", + Command: []string{"/bin/bash"}, + Args: []string{"-c", ServiceCommand}, + Image: instance.Spec.ContainerImage, + SecurityContext: getNeutronSecurityContext(), + Env: env.MergeEnvs([]corev1.EnvVar{}, envVars), + VolumeMounts: rpcVolumeMounts, + Resources: instance.Spec.Resources, + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + }, + } + + return containers, nil +} + +// GetConfigTemplates returns additional templates needed for gunicorn deployment +func (g *GunicornStrategy) GetConfigTemplates() map[string]string { + return map[string]string{ + "neutron-gunicorn-config.json": "/neutronapi/config/neutron-gunicorn-config.json", + "gunicorn_config.py": "/neutronapi/gunicorn/gunicorn_config.py", + "neutron_gunicorn_wrapper.py": "/neutronapi/gunicorn/neutron_gunicorn_wrapper.py", + "neutron-periodic-workers-config.json": "/neutronapi/config/neutron-periodic-workers-config.json", + "neutron-ovn-maintenance-worker-config.json": "/neutronapi/config/neutron-ovn-maintenance-worker-config.json", + "neutron-rpc-server-config.json": "/neutronapi/config/neutron-rpc-server-config.json", + } +} + +// GetServicePorts returns the neutron service ports for gunicorn +func (g *GunicornStrategy) GetServicePorts() []corev1.ServicePort { + return []corev1.ServicePort{ + { + Name: "public", + Port: NeutronPublicPort, + Protocol: corev1.ProtocolTCP, + }, + { + Name: "internal", + Port: NeutronInternalPort, + Protocol: corev1.ProtocolTCP, + }, + } +} + +// GetProbes returns HTTP probes for the gunicorn container +func (g *GunicornStrategy) GetProbes(instance *neutronv1.NeutronAPI) (*corev1.Probe, *corev1.Probe) { + livenessProbe := &corev1.Probe{ + TimeoutSeconds: 30, + PeriodSeconds: 30, + InitialDelaySeconds: 10, + } + livenessProbe.HTTPGet = &corev1.HTTPGetAction{ + Path: "/", + Port: intstr.IntOrString{Type: intstr.Int, IntVal: int32(NeutronPublicPort)}, + } + + readinessProbe := &corev1.Probe{ + TimeoutSeconds: 30, + PeriodSeconds: 30, + InitialDelaySeconds: 5, + } + readinessProbe.HTTPGet = &corev1.HTTPGetAction{ + Path: "/", + Port: intstr.IntOrString{Type: intstr.Int, IntVal: int32(NeutronPublicPort)}, + } + + // Use HTTPS probes when TLS is enabled, HTTP otherwise + if instance.Spec.TLS.API.Enabled(service.EndpointInternal) { + livenessProbe.HTTPGet.Scheme = corev1.URISchemeHTTPS + readinessProbe.HTTPGet.Scheme = corev1.URISchemeHTTPS + } else { + livenessProbe.HTTPGet.Scheme = corev1.URISchemeHTTP + readinessProbe.HTTPGet.Scheme = corev1.URISchemeHTTP + } + + return livenessProbe, readinessProbe +} + +// GetDeploymentType returns identifier for this strategy +func (g *GunicornStrategy) GetDeploymentType() string { + return "gunicorn" +} + +// GetVolumeMounts returns the volume mounts for gunicorn deployment +func (g *GunicornStrategy) GetVolumeMounts(instance *neutronv1.NeutronAPI, _ []corev1.Volume) map[string][]corev1.VolumeMount { + return map[string][]corev1.VolumeMount{ + "neutron-gunicorn": GetVolumeMounts("neutron-gunicorn", instance.Spec.ExtraMounts, NeutronAPIPropagation), + "neutron-periodic-workers": GetVolumeMounts("neutron-periodic-workers", instance.Spec.ExtraMounts, NeutronAPIPropagation), + "neutron-ovn-maintenance-worker": GetVolumeMounts("neutron-ovn-maintenance-worker", instance.Spec.ExtraMounts, NeutronAPIPropagation), + "neutron-rpc-server": GetVolumeMounts("neutron-rpc-server", instance.Spec.ExtraMounts, NeutronAPIPropagation), + } +} diff --git a/internal/neutronapi/httpd_strategy.go b/internal/neutronapi/httpd_strategy.go new file mode 100644 index 000000000..999e6a471 --- /dev/null +++ b/internal/neutronapi/httpd_strategy.go @@ -0,0 +1,164 @@ +package neutronapi + +import ( + "github.com/openstack-k8s-operators/lib-common/modules/common/env" + "github.com/openstack-k8s-operators/lib-common/modules/common/service" + neutronv1 "github.com/openstack-k8s-operators/neutron-operator/api/v1beta1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +// HttpdStrategy implements the multi-container deployment with httpd + separate workers +type HttpdStrategy struct{} + +// GetContainers returns the httpd and worker containers for the httpd deployment +func (h *HttpdStrategy) GetContainers(instance *neutronv1.NeutronAPI, configHash string, _ []corev1.Volume) ([]corev1.Container, error) { + envVars := map[string]env.Setter{} + envVars["KOLLA_CONFIG_STRATEGY"] = env.SetValue("COPY_ALWAYS") + envVars["CONFIG_HASH"] = env.SetValue(configHash) + + // Get container-specific volume mounts - each container needs its own config.json + httpdVolumeMounts := GetVolumeMounts("neutron-httpd-wsgi", instance.Spec.ExtraMounts, NeutronAPIPropagation) + periodicVolumeMounts := GetVolumeMounts("neutron-periodic-workers", instance.Spec.ExtraMounts, NeutronAPIPropagation) + ovnVolumeMounts := GetVolumeMounts("neutron-ovn-maintenance-worker", instance.Spec.ExtraMounts, NeutronAPIPropagation) + rpcVolumeMounts := GetVolumeMounts("neutron-rpc-server", instance.Spec.ExtraMounts, NeutronAPIPropagation) + + // Note: TLS volume mounts are handled by applyTLSMountsToContainers in deployment.go + + livenessProbe, readinessProbe := h.GetProbes(instance) + + containers := []corev1.Container{ + { + Name: ServiceName + "-httpd", + Command: []string{"/bin/bash"}, + Args: []string{"-c", ServiceCommand}, + Image: instance.Spec.ContainerImage, + SecurityContext: getNeutronSecurityContext(), + Env: env.MergeEnvs([]corev1.EnvVar{}, envVars), + VolumeMounts: httpdVolumeMounts, + Resources: instance.Spec.Resources, + LivenessProbe: livenessProbe, + ReadinessProbe: readinessProbe, + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + Ports: []corev1.ContainerPort{ + { + Name: "neutron-api", + ContainerPort: NeutronPublicPort, + Protocol: corev1.ProtocolTCP, + }, + }, + }, + { + Name: ServiceName + "-periodic-workers", + Command: []string{"/bin/bash"}, + Args: []string{"-c", ServiceCommand}, + Image: instance.Spec.ContainerImage, + SecurityContext: getNeutronSecurityContext(), + Env: env.MergeEnvs([]corev1.EnvVar{}, envVars), + VolumeMounts: periodicVolumeMounts, + Resources: instance.Spec.Resources, + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + }, + { + Name: ServiceName + "-ovn-maintenance-worker", + Command: []string{"/bin/bash"}, + Args: []string{"-c", ServiceCommand}, + Image: instance.Spec.ContainerImage, + SecurityContext: getNeutronSecurityContext(), + Env: env.MergeEnvs([]corev1.EnvVar{}, envVars), + VolumeMounts: ovnVolumeMounts, + Resources: instance.Spec.Resources, + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + }, + { + Name: ServiceName + "-rpc-server", + Command: []string{"/bin/bash"}, + Args: []string{"-c", ServiceCommand}, + Image: instance.Spec.ContainerImage, + SecurityContext: getNeutronSecurityContext(), + Env: env.MergeEnvs([]corev1.EnvVar{}, envVars), + VolumeMounts: rpcVolumeMounts, + Resources: instance.Spec.Resources, + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + }, + } + + return containers, nil +} + +// GetConfigTemplates returns templates needed for httpd deployment +func (h *HttpdStrategy) GetConfigTemplates() map[string]string { + return map[string]string{ + "neutron-httpd-wsgi-config.json": "/neutronapi/config/neutron-httpd-wsgi-config.json", + "neutron-periodic-workers-config.json": "/neutronapi/config/neutron-periodic-workers-config.json", + "neutron-ovn-maintenance-worker-config.json": "/neutronapi/config/neutron-ovn-maintenance-worker-config.json", + "neutron-rpc-server-config.json": "/neutronapi/config/neutron-rpc-server-config.json", + "httpd.conf": "/neutronapi/httpd/httpd.conf", + "10-neutron-httpd.conf": "/neutronapi/httpd/10-neutron-httpd.conf", + } +} + +// GetServicePorts returns the neutron service ports for httpd +func (h *HttpdStrategy) GetServicePorts() []corev1.ServicePort { + return []corev1.ServicePort{ + { + Name: "public", + Port: NeutronPublicPort, + Protocol: corev1.ProtocolTCP, + }, + { + Name: "internal", + Port: NeutronInternalPort, + Protocol: corev1.ProtocolTCP, + }, + } +} + +// GetProbes returns HTTP probes for the httpd container +func (h *HttpdStrategy) GetProbes(instance *neutronv1.NeutronAPI) (*corev1.Probe, *corev1.Probe) { + livenessProbe := &corev1.Probe{ + TimeoutSeconds: 30, + PeriodSeconds: 30, + InitialDelaySeconds: 5, + } + livenessProbe.HTTPGet = &corev1.HTTPGetAction{ + Path: "/", + Port: intstr.IntOrString{Type: intstr.Int, IntVal: int32(NeutronPublicPort)}, + } + + readinessProbe := &corev1.Probe{ + TimeoutSeconds: 30, + PeriodSeconds: 30, + InitialDelaySeconds: 5, + } + readinessProbe.HTTPGet = &corev1.HTTPGetAction{ + Path: "/", + Port: intstr.IntOrString{Type: intstr.Int, IntVal: int32(NeutronPublicPort)}, + } + + // Enable HTTPS probes if TLS is configured + if instance.Spec.TLS.API.Enabled(service.EndpointPublic) { + livenessProbe.HTTPGet.Scheme = corev1.URISchemeHTTPS + readinessProbe.HTTPGet.Scheme = corev1.URISchemeHTTPS + } else { + livenessProbe.HTTPGet.Scheme = corev1.URISchemeHTTP + readinessProbe.HTTPGet.Scheme = corev1.URISchemeHTTP + } + + return livenessProbe, readinessProbe +} + +// GetDeploymentType returns identifier for this strategy +func (h *HttpdStrategy) GetDeploymentType() string { + return "httpd" +} + +// GetVolumeMounts returns the volume mounts for httpd deployment +func (h *HttpdStrategy) GetVolumeMounts(instance *neutronv1.NeutronAPI, _ []corev1.Volume) map[string][]corev1.VolumeMount { + return map[string][]corev1.VolumeMount{ + "neutron-httpd-wsgi": GetVolumeMounts("neutron-httpd-wsgi", instance.Spec.ExtraMounts, NeutronAPIPropagation), + "neutron-periodic-workers": GetVolumeMounts("neutron-periodic-workers", instance.Spec.ExtraMounts, NeutronAPIPropagation), + "neutron-ovn-maintenance-worker": GetVolumeMounts("neutron-ovn-maintenance-worker", instance.Spec.ExtraMounts, NeutronAPIPropagation), + "neutron-rpc-server": GetVolumeMounts("neutron-rpc-server", instance.Spec.ExtraMounts, NeutronAPIPropagation), + } +} diff --git a/internal/neutronapi/strategy.go b/internal/neutronapi/strategy.go new file mode 100644 index 000000000..0945b64c2 --- /dev/null +++ b/internal/neutronapi/strategy.go @@ -0,0 +1,27 @@ +package neutronapi + +import ( + neutronv1 "github.com/openstack-k8s-operators/neutron-operator/api/v1beta1" + corev1 "k8s.io/api/core/v1" +) + +// DeploymentStrategy defines the interface for different neutron deployment approaches +type DeploymentStrategy interface { + // GetContainers returns the list of containers for this deployment strategy + GetContainers(instance *neutronv1.NeutronAPI, configHash string, volumes []corev1.Volume) ([]corev1.Container, error) + + // GetConfigTemplates returns additional template files needed for this strategy + GetConfigTemplates() map[string]string + + // GetServicePorts returns the ports that should be exposed by the service + GetServicePorts() []corev1.ServicePort + + // GetProbes returns liveness and readiness probes for the main service + GetProbes(instance *neutronv1.NeutronAPI) (*corev1.Probe, *corev1.Probe) + + // GetDeploymentType returns a string identifier for this deployment type + GetDeploymentType() string + + // GetVolumeMounts returns additional volume mounts specific to this strategy + GetVolumeMounts(instance *neutronv1.NeutronAPI, volumes []corev1.Volume) map[string][]corev1.VolumeMount +} diff --git a/internal/neutronapi/uwsgi_strategy.go b/internal/neutronapi/uwsgi_strategy.go new file mode 100644 index 000000000..eee86a2ae --- /dev/null +++ b/internal/neutronapi/uwsgi_strategy.go @@ -0,0 +1,161 @@ +package neutronapi + +import ( + "github.com/openstack-k8s-operators/lib-common/modules/common/env" + neutronv1 "github.com/openstack-k8s-operators/neutron-operator/api/v1beta1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +// UwsgiStrategy implements the new multi-container deployment with uwsgi + separate workers +type UwsgiStrategy struct{} + +// GetContainers returns the uwsgi and worker containers for the new deployment +func (u *UwsgiStrategy) GetContainers(instance *neutronv1.NeutronAPI, configHash string, _ []corev1.Volume) ([]corev1.Container, error) { + envVars := map[string]env.Setter{} + envVars["KOLLA_CONFIG_STRATEGY"] = env.SetValue("COPY_ALWAYS") + envVars["CONFIG_HASH"] = env.SetValue(configHash) + envVars["OS_NEUTRON_CONFIG_DIR"] = env.SetValue("/etc/neutron/neutron.conf.d") + envVars["OS_NEUTRON_CONFIG_FILES"] = env.SetValue("01-neutron.conf") + if instance.Spec.CustomServiceConfig != "" { + envVars["OS_NEUTRON_CONFIG_FILES"] = env.SetValue("01-neutron.conf;02-neutron-custom.conf") + } + + // Get container-specific volume mounts - each container needs its own config.json + uwsgiVolumeMounts := GetVolumeMounts("neutron-uwsgi", instance.Spec.ExtraMounts, NeutronAPIPropagation) + periodicVolumeMounts := GetVolumeMounts("neutron-periodic-workers", instance.Spec.ExtraMounts, NeutronAPIPropagation) + ovnVolumeMounts := GetVolumeMounts("neutron-ovn-maintenance-worker", instance.Spec.ExtraMounts, NeutronAPIPropagation) + rpcVolumeMounts := GetVolumeMounts("neutron-rpc-server", instance.Spec.ExtraMounts, NeutronAPIPropagation) + + // Note: TLS volume mounts are handled by applyTLSMountsToContainers in deployment.go + + livenessProbe, readinessProbe := u.GetProbes(instance) + + containers := []corev1.Container{ + { + Name: ServiceName + "-uwsgi", + Command: []string{"/bin/bash"}, + Args: []string{"-c", ServiceCommand}, + Image: instance.Spec.ContainerImage, + SecurityContext: getNeutronSecurityContext(), + Env: env.MergeEnvs([]corev1.EnvVar{}, envVars), + VolumeMounts: uwsgiVolumeMounts, + Resources: instance.Spec.Resources, + LivenessProbe: livenessProbe, + ReadinessProbe: readinessProbe, + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + Ports: []corev1.ContainerPort{ + { + Name: "neutron-api", + ContainerPort: NeutronPublicPort, + Protocol: corev1.ProtocolTCP, + }, + }, + }, + { + Name: ServiceName + "-periodic-workers", + Command: []string{"/bin/bash"}, + Args: []string{"-c", ServiceCommand}, + Image: instance.Spec.ContainerImage, + SecurityContext: getNeutronSecurityContext(), + Env: env.MergeEnvs([]corev1.EnvVar{}, envVars), + VolumeMounts: periodicVolumeMounts, + Resources: instance.Spec.Resources, + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + }, + { + Name: ServiceName + "-ovn-maintenance-worker", + Command: []string{"/bin/bash"}, + Args: []string{"-c", ServiceCommand}, + Image: instance.Spec.ContainerImage, + SecurityContext: getNeutronSecurityContext(), + Env: env.MergeEnvs([]corev1.EnvVar{}, envVars), + VolumeMounts: ovnVolumeMounts, + Resources: instance.Spec.Resources, + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + }, + { + Name: ServiceName + "-rpc-server", + Command: []string{"/bin/bash"}, + Args: []string{"-c", ServiceCommand}, + Image: instance.Spec.ContainerImage, + SecurityContext: getNeutronSecurityContext(), + Env: env.MergeEnvs([]corev1.EnvVar{}, envVars), + VolumeMounts: rpcVolumeMounts, + Resources: instance.Spec.Resources, + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + }, + } + + return containers, nil +} + +// GetConfigTemplates returns additional templates needed for uwsgi deployment +func (u *UwsgiStrategy) GetConfigTemplates() map[string]string { + return map[string]string{ + "neutron-uwsgi-config.json": "/neutronapi/config/neutron-uwsgi-config.json", + "neutron-uwsgi.ini": "/neutronapi/uwsgi/neutron-uwsgi.ini", + "neutron-periodic-workers-config.json": "/neutronapi/config/neutron-periodic-workers-config.json", + "neutron-ovn-maintenance-worker-config.json": "/neutronapi/config/neutron-ovn-maintenance-worker-config.json", + "neutron-rpc-server-config.json": "/neutronapi/config/neutron-rpc-server-config.json", + } +} + +// GetServicePorts returns the neutron service ports for uwsgi +func (u *UwsgiStrategy) GetServicePorts() []corev1.ServicePort { + return []corev1.ServicePort{ + { + Name: "public", + Port: NeutronPublicPort, + Protocol: corev1.ProtocolTCP, + }, + { + Name: "internal", + Port: NeutronInternalPort, + Protocol: corev1.ProtocolTCP, + }, + } +} + +// GetProbes returns HTTP probes for the uwsgi container +func (u *UwsgiStrategy) GetProbes(_ *neutronv1.NeutronAPI) (*corev1.Probe, *corev1.Probe) { + livenessProbe := &corev1.Probe{ + TimeoutSeconds: 30, + PeriodSeconds: 30, + InitialDelaySeconds: 10, // Slightly longer delay for uwsgi startup + } + livenessProbe.HTTPGet = &corev1.HTTPGetAction{ + Path: "/", + Port: intstr.IntOrString{Type: intstr.Int, IntVal: int32(NeutronInternalAPIPort)}, + } + + readinessProbe := &corev1.Probe{ + TimeoutSeconds: 30, + PeriodSeconds: 30, + InitialDelaySeconds: 5, + } + readinessProbe.HTTPGet = &corev1.HTTPGetAction{ + Path: "/", + Port: intstr.IntOrString{Type: intstr.Int, IntVal: int32(NeutronInternalAPIPort)}, + } + + livenessProbe.HTTPGet.Scheme = corev1.URISchemeHTTP + readinessProbe.HTTPGet.Scheme = corev1.URISchemeHTTP + + return livenessProbe, readinessProbe +} + +// GetDeploymentType returns identifier for this strategy +func (u *UwsgiStrategy) GetDeploymentType() string { + return "uwsgi" +} + +// GetVolumeMounts returns the volume mounts for uwsgi deployment +func (u *UwsgiStrategy) GetVolumeMounts(instance *neutronv1.NeutronAPI, _ []corev1.Volume) map[string][]corev1.VolumeMount { + return map[string][]corev1.VolumeMount{ + "neutron-uwsgi": GetVolumeMounts("neutron-uwsgi", instance.Spec.ExtraMounts, NeutronAPIPropagation), + "neutron-periodic-workers": GetVolumeMounts("neutron-periodic-workers", instance.Spec.ExtraMounts, NeutronAPIPropagation), + "neutron-ovn-maintenance-worker": GetVolumeMounts("neutron-ovn-maintenance-worker", instance.Spec.ExtraMounts, NeutronAPIPropagation), + "neutron-rpc-server": GetVolumeMounts("neutron-rpc-server", instance.Spec.ExtraMounts, NeutronAPIPropagation), + } +} diff --git a/internal/neutronapi/volumes.go b/internal/neutronapi/volumes.go index 29b97abf7..eb04c62b6 100644 --- a/internal/neutronapi/volumes.go +++ b/internal/neutronapi/volumes.go @@ -45,6 +45,47 @@ func GetVolumes(name string, extraVol []neutronv1beta1.NeutronExtraVolMounts, sv } +// GetVolumesForStrategy - Strategy-aware volume creation that only includes volumes needed for the deployment type +func GetVolumesForStrategy(name string, extraVol []neutronv1beta1.NeutronExtraVolMounts, svc []storage.PropagationType, deploymentType string) []corev1.Volume { + res := []corev1.Volume{ + { + Name: "config", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: name + "-config", + }, + }, + }, + } + + // Add httpd-config volume for strategies that use httpd container + if deploymentType == "eventlet" || deploymentType == "httpd" { + res = append(res, corev1.Volume{ + Name: "httpd-config", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: name + "-httpd-config", + }, + }, + }) + } + + // Add extra volumes from spec + for _, exv := range extraVol { + for _, vol := range exv.Propagate(svc) { + for _, v := range vol.Volumes { + volumeSource, _ := v.ToCoreVolumeSource() + convertedVolume := corev1.Volume{ + Name: v.Name, + VolumeSource: *volumeSource, + } + res = append(res, convertedVolume) + } + } + } + return res +} + // GetVolumeMounts - Neutron API VolumeMounts func GetVolumeMounts(serviceName string, extraVol []neutronv1beta1.NeutronExtraVolMounts, svc []storage.PropagationType) []corev1.VolumeMount { res := []corev1.VolumeMount{ diff --git a/templates/neutronapi/config/01-neutron.conf b/templates/neutronapi/config/01-neutron.conf index 9e17b4716..2887178c6 100644 --- a/templates/neutronapi/config/01-neutron.conf +++ b/templates/neutronapi/config/01-neutron.conf @@ -1,6 +1,7 @@ [DEFAULT] bind_host = localhost bind_port = 9697 +cafile = /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem transport_url={{ .TransportURL }} core_plugin = {{ .CorePlugin }} {{ if .IsOVN }} @@ -75,6 +76,7 @@ service_type = network {{ if (index . "Region") -}} region_name = {{ .Region }} {{ end -}} +cafile = /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem {{if (index . "MemcachedAuthCert")}} memcache_tls_certfile = {{ .MemcachedAuthCert }} memcache_tls_keyfile = {{ .MemcachedAuthKey }} @@ -102,6 +104,7 @@ region_name = {{ .Region }} region_name = regionOne {{ end -}} endpoint_type = internal +cafile = /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem [placement] auth_url = {{ .KeystoneInternalURL }} @@ -123,6 +126,7 @@ region_name = {{ .Region }} region_name = regionOne {{ end -}} endpoint_type = internal +cafile = /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem [oslo_concurrency] lock_path = /var/lib/neutron/tmp diff --git a/templates/neutronapi/config/api-paste.ini b/templates/neutronapi/config/api-paste.ini new file mode 100644 index 000000000..2f9da2104 --- /dev/null +++ b/templates/neutronapi/config/api-paste.ini @@ -0,0 +1,58 @@ +[composite:neutron] +use = egg:Paste#urlmap +/: neutronversions_composite +/healthcheck: healthcheck +/v2.0: neutronapi_v2_0 + +[composite:neutronapi_v2_0] +use = call:neutron.auth:pipeline_factory +noauth = cors http_proxy_to_wsgi request_id fake_project_id catch_errors osprofiler extensions neutronapiapp_v2_0 +keystone = cors http_proxy_to_wsgi request_id catch_errors osprofiler authtoken keystonecontext extensions neutronapiapp_v2_0 + +[composite:neutronversions_composite] +use = call:neutron.auth:pipeline_factory +noauth = cors http_proxy_to_wsgi neutronversions +keystone = cors http_proxy_to_wsgi neutronversions + +[filter:request_id] +paste.filter_factory = oslo_middleware:RequestId.factory + +[filter:catch_errors] +paste.filter_factory = oslo_middleware:CatchErrors.factory + +[filter:cors] +paste.filter_factory = oslo_middleware.cors:filter_factory +oslo_config_project = neutron + +[filter:project_id] +paste.filter_factory = neutron.api.extensions:ProjectIdMiddleware.factory +oslo_config_project = neutron + +[filter:fake_project_id] +paste.filter_factory = neutron.auth:NoauthFakeProjectId.factory + +[filter:http_proxy_to_wsgi] +paste.filter_factory = oslo_middleware.http_proxy_to_wsgi:HTTPProxyToWSGI.factory + +[filter:keystonecontext] +paste.filter_factory = neutron.auth:NeutronKeystoneContext.factory + +[filter:authtoken] +paste.filter_factory = keystonemiddleware.auth_token:filter_factory + +[filter:extensions] +paste.filter_factory = neutron.api.extensions:plugin_aware_extension_middleware_factory + +[app:neutronversions] +paste.app_factory = neutron.pecan_wsgi.app:versions_factory + +[app:neutronapiapp_v2_0] +paste.app_factory = neutron.api.v2.router:APIRouter.factory + +[filter:osprofiler] +paste.filter_factory = osprofiler.web:WsgiMiddleware.factory + +[app:healthcheck] +paste.app_factory = oslo_middleware:Healthcheck.app_factory +backends = disable_by_file +disable_by_file_path = /var/lib/neutron/healthcheck_disable diff --git a/templates/neutronapi/config/db-sync-config.json b/templates/neutronapi/config/db-sync-config.json index 1b206e9a7..01d80c7a2 100644 --- a/templates/neutronapi/config/db-sync-config.json +++ b/templates/neutronapi/config/db-sync-config.json @@ -1,5 +1,5 @@ { - "command": "neutron-db-manage --config-file /usr/share/neutron/neutron-dist.conf --config-file /etc/neutron/neutron.conf --config-dir /etc/neutron/neutron.conf.d upgrade heads", + "command": "neutron-db-manage --config-dir /etc/neutron/neutron.conf.d upgrade heads", "config_files": [ { "source": "/var/lib/config-data/default/01-neutron.conf", diff --git a/templates/neutronapi/config/neutron-gunicorn-config.json b/templates/neutronapi/config/neutron-gunicorn-config.json new file mode 100644 index 000000000..1fe94d448 --- /dev/null +++ b/templates/neutronapi/config/neutron-gunicorn-config.json @@ -0,0 +1,105 @@ +{ + "command": "python /etc/neutron/neutron_gunicorn_wrapper.py", + "permissions": [ + { + "path": "/etc/neutron/plugins", + "owner": "neutron:neutron", + "mode": "0755", + "recurse": true + }, + { + "path": "/etc/neutron/plugins/ml2", + "owner": "neutron:neutron", + "mode": "0755" + } + ], + "config_files": [ + { + "source": "/var/lib/config-data/default/gunicorn_config.py", + "dest": "/etc/neutron/gunicorn_config.py", + "owner": "neutron:neutron", + "perm": "0644" + }, + { + "source": "/var/lib/config-data/default/neutron_gunicorn_wrapper.py", + "dest": "/etc/neutron/neutron_gunicorn_wrapper.py", + "owner": "neutron:neutron", + "perm": "0755" + }, + { + "source": "/var/lib/config-data/default/01-neutron.conf", + "dest": "/etc/neutron/neutron.conf.d/01-neutron.conf", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/02-neutron-custom.conf", + "dest": "/etc/neutron/neutron.conf.d/02-neutron-custom.conf", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/01-neutron.conf", + "dest": "/etc/neutron/neutron.conf", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/01-neutron.conf", + "dest": "/etc/neutron/plugins/ml2/ml2_conf.ini", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/api-paste.ini", + "dest": "/etc/neutron/api-paste.ini", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/policy.yaml", + "dest": "/etc/neutron/policy.yaml", + "owner": "root:neutron", + "perm": "0640", + "optional": true + }, + { + "source": "/var/lib/config-data/default/my.cnf", + "dest": "/etc/my.cnf", + "owner": "neutron", + "perm": "0644" + }, + { + "source": "/var/lib/config-data/tls/certs/*", + "dest": "/etc/pki/tls/certs/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + }, + { + "source": "/var/lib/config-data/tls/private/*", + "dest": "/etc/pki/tls/private/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + }, + { + "source": "/var/lib/config-data/mtls/certs/*", + "dest": "/etc/pki/tls/certs/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + }, + { + "source": "/var/lib/config-data/mtls/private/*", + "dest": "/etc/pki/tls/private/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + } + ] +} diff --git a/templates/neutronapi/config/neutron-httpd-wsgi-config.json b/templates/neutronapi/config/neutron-httpd-wsgi-config.json new file mode 100644 index 000000000..dbc503535 --- /dev/null +++ b/templates/neutronapi/config/neutron-httpd-wsgi-config.json @@ -0,0 +1,108 @@ +{ + "command": "/usr/sbin/httpd -DFOREGROUND", + "config_files": [ + { + "source": "/var/lib/config-data/tls/certs/*", + "dest": "/etc/pki/tls/certs/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + }, + { + "source": "/var/lib/config-data/tls/private/*", + "dest": "/etc/pki/tls/private/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + }, + { + "source": "/var/lib/config-data/mtls/certs/*", + "dest": "/etc/pki/tls/certs/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + }, + { + "source": "/var/lib/config-data/mtls/private/*", + "dest": "/etc/pki/tls/private/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + }, + { + "source": "/var/lib/config-data/default/httpd.conf", + "dest": "/etc/httpd/conf/httpd.conf", + "owner": "neutron:apache", + "optional": true, + "perm": "0644" + }, + { + "source": "/var/lib/config-data/default/10-neutron-httpd.conf", + "dest": "/etc/httpd/conf.d/10-neutron.conf", + "owner": "neutron:apache", + "optional": true, + "perm": "0644" + }, + { + "source": "/var/lib/config-data/default/01-neutron.conf", + "dest": "/etc/neutron/neutron.conf.d/01-neutron.conf", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/02-neutron-custom.conf", + "dest": "/etc/neutron/neutron.conf.d/02-neutron-custom.conf", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/01-neutron.conf", + "dest": "/etc/neutron/neutron.conf", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/01-neutron.conf", + "dest": "/etc/neutron/plugins/ml2/ml2_conf.ini", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/api-paste.ini", + "dest": "/etc/neutron/api-paste.ini", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/policy.yaml", + "dest": "/etc/neutron/policy.yaml", + "owner": "root:neutron", + "perm": "0640", + "optional": true + }, + { + "source": "/var/lib/config-data/default/my.cnf", + "dest": "/etc/my.cnf", + "owner": "neutron", + "perm": "0644" + }, + { + "source": "/var/lib/config-data/default/ssl.conf", + "dest": "/etc/httpd/conf.d/ssl.conf", + "owner": "neutron:apache", + "optional": true, + "perm": "0644" + } + ], + "permissions": [ + { + "path": "/etc/httpd/run", + "owner": "neutron:apache", + "recurse": true + } + ] +} diff --git a/templates/neutronapi/config/neutron-ovn-maintenance-worker-config.json b/templates/neutronapi/config/neutron-ovn-maintenance-worker-config.json new file mode 100644 index 000000000..bb9a8b0ec --- /dev/null +++ b/templates/neutronapi/config/neutron-ovn-maintenance-worker-config.json @@ -0,0 +1,93 @@ +{ + "command": "neutron-ovn-maintenance-worker", + "permissions": [ + { + "path": "/etc/neutron/plugins", + "owner": "neutron:neutron", + "mode": "0755", + "recurse": true + }, + { + "path": "/etc/neutron/plugins/ml2", + "owner": "neutron:neutron", + "mode": "0755" + } + ], + "config_files": [ + { + "source": "/var/lib/config-data/default/01-neutron.conf", + "dest": "/etc/neutron/neutron.conf.d/01-neutron.conf", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/02-neutron-custom.conf", + "dest": "/etc/neutron/neutron.conf.d/02-neutron-custom.conf", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/01-neutron.conf", + "dest": "/etc/neutron/neutron.conf", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/01-neutron.conf", + "dest": "/etc/neutron/plugins/ml2/ml2_conf.ini", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/api-paste.ini", + "dest": "/etc/neutron/api-paste.ini", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/policy.yaml", + "dest": "/etc/neutron/policy.yaml", + "owner": "root:neutron", + "perm": "0640", + "optional": true + }, + { + "source": "/var/lib/config-data/default/my.cnf", + "dest": "/etc/my.cnf", + "owner": "neutron", + "perm": "0644" + }, + { + "source": "/var/lib/config-data/tls/certs/*", + "dest": "/etc/pki/tls/certs/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + }, + { + "source": "/var/lib/config-data/tls/private/*", + "dest": "/etc/pki/tls/private/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + }, + { + "source": "/var/lib/config-data/mtls/certs/*", + "dest": "/etc/pki/tls/certs/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + }, + { + "source": "/var/lib/config-data/mtls/private/*", + "dest": "/etc/pki/tls/private/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + } + ] +} diff --git a/templates/neutronapi/config/neutron-periodic-workers-config.json b/templates/neutronapi/config/neutron-periodic-workers-config.json new file mode 100644 index 000000000..1d7b18879 --- /dev/null +++ b/templates/neutronapi/config/neutron-periodic-workers-config.json @@ -0,0 +1,93 @@ +{ + "command": "neutron-periodic-workers", + "permissions": [ + { + "path": "/etc/neutron/plugins", + "owner": "neutron:neutron", + "mode": "0755", + "recurse": true + }, + { + "path": "/etc/neutron/plugins/ml2", + "owner": "neutron:neutron", + "mode": "0755" + } + ], + "config_files": [ + { + "source": "/var/lib/config-data/default/01-neutron.conf", + "dest": "/etc/neutron/neutron.conf.d/01-neutron.conf", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/02-neutron-custom.conf", + "dest": "/etc/neutron/neutron.conf.d/02-neutron-custom.conf", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/01-neutron.conf", + "dest": "/etc/neutron/neutron.conf", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/01-neutron.conf", + "dest": "/etc/neutron/plugins/ml2/ml2_conf.ini", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/api-paste.ini", + "dest": "/etc/neutron/api-paste.ini", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/policy.yaml", + "dest": "/etc/neutron/policy.yaml", + "owner": "root:neutron", + "perm": "0640", + "optional": true + }, + { + "source": "/var/lib/config-data/default/my.cnf", + "dest": "/etc/my.cnf", + "owner": "neutron", + "perm": "0644" + }, + { + "source": "/var/lib/config-data/tls/certs/*", + "dest": "/etc/pki/tls/certs/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + }, + { + "source": "/var/lib/config-data/tls/private/*", + "dest": "/etc/pki/tls/private/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + }, + { + "source": "/var/lib/config-data/mtls/certs/*", + "dest": "/etc/pki/tls/certs/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + }, + { + "source": "/var/lib/config-data/mtls/private/*", + "dest": "/etc/pki/tls/private/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + } + ] +} diff --git a/templates/neutronapi/config/neutron-rpc-server-config.json b/templates/neutronapi/config/neutron-rpc-server-config.json new file mode 100644 index 000000000..85a4180c7 --- /dev/null +++ b/templates/neutronapi/config/neutron-rpc-server-config.json @@ -0,0 +1,93 @@ +{ + "command": "neutron-rpc-server", + "permissions": [ + { + "path": "/etc/neutron/plugins", + "owner": "neutron:neutron", + "mode": "0755", + "recurse": true + }, + { + "path": "/etc/neutron/plugins/ml2", + "owner": "neutron:neutron", + "mode": "0755" + } + ], + "config_files": [ + { + "source": "/var/lib/config-data/default/01-neutron.conf", + "dest": "/etc/neutron/neutron.conf.d/01-neutron.conf", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/02-neutron-custom.conf", + "dest": "/etc/neutron/neutron.conf.d/02-neutron-custom.conf", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/01-neutron.conf", + "dest": "/etc/neutron/neutron.conf", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/01-neutron.conf", + "dest": "/etc/neutron/plugins/ml2/ml2_conf.ini", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/api-paste.ini", + "dest": "/etc/neutron/api-paste.ini", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/policy.yaml", + "dest": "/etc/neutron/policy.yaml", + "owner": "root:neutron", + "perm": "0640", + "optional": true + }, + { + "source": "/var/lib/config-data/default/my.cnf", + "dest": "/etc/my.cnf", + "owner": "neutron", + "perm": "0644" + }, + { + "source": "/var/lib/config-data/tls/certs/*", + "dest": "/etc/pki/tls/certs/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + }, + { + "source": "/var/lib/config-data/tls/private/*", + "dest": "/etc/pki/tls/private/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + }, + { + "source": "/var/lib/config-data/mtls/certs/*", + "dest": "/etc/pki/tls/certs/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + }, + { + "source": "/var/lib/config-data/mtls/private/*", + "dest": "/etc/pki/tls/private/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + } + ] +} diff --git a/templates/neutronapi/config/neutron-uwsgi-config.json b/templates/neutronapi/config/neutron-uwsgi-config.json new file mode 100644 index 000000000..186db3fbe --- /dev/null +++ b/templates/neutronapi/config/neutron-uwsgi-config.json @@ -0,0 +1,99 @@ +{ + "command": "uwsgi --ini /etc/neutron/neutron-uwsgi.ini", + "permissions": [ + { + "path": "/etc/neutron/plugins", + "owner": "neutron:neutron", + "mode": "0755", + "recurse": true + }, + { + "path": "/etc/neutron/plugins/ml2", + "owner": "neutron:neutron", + "mode": "0755" + } + ], + "config_files": [ + { + "source": "/var/lib/config-data/default/neutron-uwsgi.ini", + "dest": "/etc/neutron/neutron-uwsgi.ini", + "owner": "neutron:neutron", + "perm": "0644" + }, + { + "source": "/var/lib/config-data/default/01-neutron.conf", + "dest": "/etc/neutron/neutron.conf.d/01-neutron.conf", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/02-neutron-custom.conf", + "dest": "/etc/neutron/neutron.conf.d/02-neutron-custom.conf", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/01-neutron.conf", + "dest": "/etc/neutron/neutron.conf", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/01-neutron.conf", + "dest": "/etc/neutron/plugins/ml2/ml2_conf.ini", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/api-paste.ini", + "dest": "/etc/neutron/api-paste.ini", + "owner": "root:neutron", + "perm": "0640" + }, + { + "source": "/var/lib/config-data/default/policy.yaml", + "dest": "/etc/neutron/policy.yaml", + "owner": "root:neutron", + "perm": "0640", + "optional": true + }, + { + "source": "/var/lib/config-data/default/my.cnf", + "dest": "/etc/my.cnf", + "owner": "neutron", + "perm": "0644" + }, + { + "source": "/var/lib/config-data/tls/certs/*", + "dest": "/etc/pki/tls/certs/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + }, + { + "source": "/var/lib/config-data/tls/private/*", + "dest": "/etc/pki/tls/private/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + }, + { + "source": "/var/lib/config-data/mtls/certs/*", + "dest": "/etc/pki/tls/certs/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + }, + { + "source": "/var/lib/config-data/mtls/private/*", + "dest": "/etc/pki/tls/private/", + "owner": "neutron:neutron", + "perm": "0640", + "optional": true, + "merge": true + } + ] +} diff --git a/templates/neutronapi/gunicorn/gunicorn_config.py b/templates/neutronapi/gunicorn/gunicorn_config.py new file mode 100644 index 000000000..9bd95afec --- /dev/null +++ b/templates/neutronapi/gunicorn/gunicorn_config.py @@ -0,0 +1,44 @@ +# Gunicorn configuration for Neutron API +import multiprocessing +import os + +# Server socket +bind = "0.0.0.0:9696" + +# Worker processes +workers = 2 +worker_class = "sync" +worker_connections = 1000 + +# Process naming +proc_name = "neutron-api" + +# Worker timeout and lifecycle +timeout = 60 +keepalive = 2 +max_requests = 5000 +max_requests_jitter = 50 + +# Application preloading +preload_app = True + +# Logging +accesslog = "-" +errorlog = "-" +loglevel = "info" +access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' + +# Security limits +limit_request_line = 4094 +limit_request_fields = 100 +limit_request_field_size = 8190 + +# Working directory +chdir = "/var/lib/neutron" + +# SSL/TLS configuration (when certificates are available) +# Uncomment and configure when TLS is enabled: +# keyfile = "/etc/pki/tls/private/internal.key" +# certfile = "/etc/pki/tls/certs/internal.crt" +# ssl_version = ssl.PROTOCOL_TLSv1_2 +# ciphers = "ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!MD5:!DSS" diff --git a/templates/neutronapi/gunicorn/neutron_gunicorn_wrapper.py b/templates/neutronapi/gunicorn/neutron_gunicorn_wrapper.py new file mode 100755 index 000000000..75d8b7528 --- /dev/null +++ b/templates/neutronapi/gunicorn/neutron_gunicorn_wrapper.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +""" +Wrapper script to run neutron with gunicorn. +This script works around the issue where neutron parses sys.argv during WSGI app initialization. +""" +import os +import sys +import ssl + +# Set neutron configuration environment variables +os.environ['OS_NEUTRON_CONFIG_DIR'] = '/etc/neutron' +os.environ['OS_NEUTRON_CONFIG_FILES'] = 'neutron.conf;plugins/ml2/ml2_conf.ini' + +# Clear sys.argv to prevent neutron from parsing gunicorn arguments +sys.argv = ['neutron-api'] + +# Now run gunicorn programmatically +if __name__ == '__main__': + from gunicorn.app.base import BaseApplication + import importlib + + class NeutronGunicornApp(BaseApplication): + def __init__(self, app, options=None): + self.options = options or {} + self.application = app + super().__init__() + + def load_config(self): + for key, value in self.options.items(): + self.cfg.set(key.lower(), value) + + def load(self): + return self.application + + # Import neutron WSGI application + neutron_wsgi = importlib.import_module('neutron.wsgi.api') + + options = { + 'bind': '0.0.0.0:9696', + 'workers': 2, + 'worker_class': 'sync', + 'timeout': 60, + 'keepalive': 2, + 'max_requests': 5000, + 'max_requests_jitter': 50, + 'preload_app': True, + 'accesslog': '-', + 'errorlog': '-', + 'loglevel': 'info', + 'chdir': '/var/lib/neutron', + } + + # Add improved TLS support if certificates are available + import os.path + internal_cert = '/etc/pki/tls/certs/internal.crt' + internal_key = '/etc/pki/tls/private/internal.key' + + if os.path.exists(internal_cert) and os.path.exists(internal_key): + options.update({ + 'keyfile': internal_key, + 'certfile': internal_cert, + # Use modern TLS protocol negotiation for better compatibility + 'ssl_version': ssl.PROTOCOL_TLS, + 'ciphers': 'ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!MD5:!DSS', + 'do_handshake_on_connect': True, + }) + print(f"TLS enabled with enhanced configuration: cert={internal_cert}, key={internal_key}") + else: + print("TLS certificates not found, serving HTTP only") + + NeutronGunicornApp(neutron_wsgi.application, options).run() diff --git a/templates/neutronapi/httpd/10-neutron-httpd.conf b/templates/neutronapi/httpd/10-neutron-httpd.conf index 994be8f78..fa05491e8 100644 --- a/templates/neutronapi/httpd/10-neutron-httpd.conf +++ b/templates/neutronapi/httpd/10-neutron-httpd.conf @@ -20,12 +20,6 @@ RequestHeader setIfEmpty X-Forwarded-Proto "http" {{- end }} - ## Proxy rules - ProxyRequests Off - ProxyPreserveHost On - ProxyPass / http://localhost:9697/ retry=10 - ProxyPassReverse / http://localhost:9697/ - {{- if $vhost.TLS }} SetEnvIf X-Forwarded-Proto https HTTPS=1 @@ -33,6 +27,28 @@ SSLEngine on SSLCertificateFile "{{ $vhost.SSLCertificateFile }}" SSLCertificateKeyFile "{{ $vhost.SSLCertificateKeyFile }}" +{{- if eq $.DeploymentType "httpd" }} + ## WSGI configuration for httpd strategy (TLS only) + WSGIProcessGroup {{ $endpt }} + WSGIApplicationGroup %{GLOBAL} + WSGIPassAuthorization On + WSGIDaemonProcess {{ $endpt }} processes=2 threads=1 user=neutron group=neutron display-name={{ $endpt }} + WSGIScriptAlias / "/var/www/cgi-bin/neutron/neutron-wsgi" +{{- else if eq $.DeploymentType "eventlet" }} + ## Proxy rules for eventlet strategy (TLS) + ProxyRequests Off + ProxyPreserveHost On + ProxyPass / http://localhost:9697/ retry=10 + ProxyPassReverse / http://localhost:9697/ +{{- end }} +{{- else }} +{{- if eq $.DeploymentType "eventlet" }} + ## Proxy rules for eventlet strategy (non-TLS) + ProxyRequests Off + ProxyPreserveHost On + ProxyPass / http://localhost:9697/ retry=10 + ProxyPassReverse / http://localhost:9697/ +{{- end }} {{- end }} IncludeOptional conf_custom/*.conf diff --git a/templates/neutronapi/httpd/httpd.conf b/templates/neutronapi/httpd/httpd.conf index 32815cf8a..b67e3eba2 100644 --- a/templates/neutronapi/httpd/httpd.conf +++ b/templates/neutronapi/httpd/httpd.conf @@ -12,7 +12,11 @@ Listen 9696 TypesConfig /etc/mime.types Include conf.modules.d/*.conf +{{- if eq .DeploymentType "eventlet" }} Include conf.d/*.conf +{{- else }} +#Include conf.d/*.conf +{{- end }} LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined LogFormat "%{X-Forwarded-For}i %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" proxy diff --git a/templates/neutronapi/uwsgi/neutron-uwsgi.ini b/templates/neutronapi/uwsgi/neutron-uwsgi.ini new file mode 100644 index 000000000..1edfa658f --- /dev/null +++ b/templates/neutronapi/uwsgi/neutron-uwsgi.ini @@ -0,0 +1,51 @@ +[uwsgi] +# WSGI module for Neutron API +plugin = python3 +module = neutron.wsgi.api:application + +# Process configuration +master = true +processes = 2 +threads = 1 +enable-threads = true +start-time = %t +add-header = Connection: close +buffer-size = 65535 +hook-master-start = unix_signal:15 gracefully_kill_them_all +thunder-lock = true +worker-reload-mercy = 80 + +# Network configuration +# Shared socket for both HTTP and HTTPS +shared-socket = 0.0.0.0:9696 + +# HTTP interface on the shared socket +http-socket = 0.0.0.0:9697 + +# Optional: HTTPS on the same socket when TLS certificates are available +https-socket = =0,/etc/pki/tls/certs/internal.crt,/etc/pki/tls/private/internal.key,HIGH + +# Application configuration +lazy-apps = true +single-interpreter = true + +# Process management +max-requests = 5000 +max-requests-delta = 50 +harakiri = 60 +harakiri-verbose = true + +# Logging +log-master = true +log-date = true + +# Graceful shutdown +exit-on-reload = false +die-on-term = true +# Memory optimization +vacuum = true +memory-report = true + +# Security +chmod-socket = 666 +chdir = /var/lib/neutron diff --git a/test/functional/suite_test.go b/test/functional/suite_test.go index ca6f9ed94..667d17934 100644 --- a/test/functional/suite_test.go +++ b/test/functional/suite_test.go @@ -21,6 +21,7 @@ import ( "crypto/tls" "fmt" "net" + "os" "path/filepath" "testing" "time" @@ -88,6 +89,9 @@ func TestAPIs(t *testing.T) { } var _ = BeforeSuite(func() { + // Force eventlet strategy for all tests to avoid binary detection issues + _ = os.Setenv("NEUTRON_DEPLOYMENT_STRATEGY", "eventlet") + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) ctx, cancel = context.WithCancel(context.TODO()) @@ -242,6 +246,7 @@ var _ = BeforeSuite(func() { var _ = AfterSuite(func() { By("tearing down the test environment") + _ = os.Unsetenv("NEUTRON_DEPLOYMENT_STRATEGY") cancel() err := testEnv.Stop() Expect(err).NotTo(HaveOccurred()) diff --git a/zuul.d/jobs.yaml b/zuul.d/jobs.yaml index d5aa41cd9..3803cff0b 100644 --- a/zuul.d/jobs.yaml +++ b/zuul.d/jobs.yaml @@ -39,6 +39,7 @@ validation.run_validation true identity.v3_endpoint_type public identity.v2_admin_endpoint_type public + neutron_plugin_options.snat_rules_apply_to_nested_networks true cifmw_test_operator_tempest_include_list: | neutron_tempest_plugin.api neutron_tempest_plugin.scenario @@ -49,5 +50,48 @@ neutron_tempest_plugin.scenario.test_mtu.NetworkWritableMtuTest # missing https://review.opendev.org/882818 in antelope test_qos_dscp_create_and_update - # Limit job runtime - NetworkSecGroupTest + # Needs docker setup + NetworkMultipleGWTest + # TypeError: NeutronTempestPluginException.__init__() takes 1 positional argument but 2 were given + L3AgentSchedulerTestJSON + +- job: + name: neutron-operator-tempest-multinode-eventlet + parent: neutron-operator-tempest-multinode + +- job: + name: neutron-operator-tempest-multinode-uwsgi + parent: neutron-operator-tempest-multinode + vars: + cifmw_update_containers_custom_images: + neutronAPIImage: quay.io/ykarel/openstack-neutron-server:uwsgi + testTempestImage: quay.rdoproject.org/podified-master-centos10/openstack-tempest-all:current-tested + cifmw_test_operator_tempest_image: quay.rdoproject.org/podified-master-centos10/openstack-tempest-all + cifmw_test_operator_tempest_image_tag: current-tested + +- job: + name: neutron-operator-tempest-multinode-httpd + parent: neutron-operator-tempest-multinode + vars: + cifmw_update_containers_custom_images: + ironicNeutronAgentImage: quay.io/openstack-k8s-operators/openstack-ironic-neutron-agent:master-latest + neutronAPIImage: quay.io/openstack-k8s-operators/openstack-neutron-server:master-latest + edpmNeutronDhcpAgentImage: quay.io/openstack-k8s-operators/openstack-neutron-dhcp-agent:master-latest + edpmNeutronSriovAgentImage: quay.io/openstack-k8s-operators/openstack-neutron-sriov-agent:master-latest + edpmNeutronOvnAgentImage: quay.io/openstack-k8s-operators/openstack-neutron-ovn-agent:master-latest + edpmNeutronMetadataAgentImage: quay.io/openstack-k8s-operators/openstack-neutron-metadata-agent-ovn:master-latest + ovnControllerImage: quay.io/ykarel/openstack-ovn-controller:master-latest + ovnControllerOvsImage: quay.io/ykarel/openstack-ovn-base:master-latest + ovnNbDbclusterImage: quay.io/ykarel/openstack-ovn-nb-db-server:master-latest + ovnNorthdImage: quay.io/ykarel/openstack-ovn-northd:master-latest + ovnSbDbclusterImage: quay.io/ykarel/openstack-ovn-sb-db-server:master-latest + testTempestImage: quay.io/openstack-k8s-operators/openstack-tempest:master-latest + cifmw_test_operator_tempest_image: quay.io/openstack-k8s-operators/openstack-tempest + cifmw_test_operator_tempest_image_tag: master-latest + +- job: + name: neutron-operator-tempest-multinode-gunicorn + parent: neutron-operator-tempest-multinode + vars: + cifmw_update_containers_custom_images: + neutronAPIImage: quay.io/ykarel/openstack-neutron-server:gunicorn diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index dfd6df607..4fd52a30a 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -6,4 +6,24 @@ - openstack-k8s-operators-content-provider: vars: cifmw_install_yamls_sdk_version: v1.41.1 - - neutron-operator-tempest-multinode + - neutron-operator-tempest-multinode-httpd: &vars + vars: + cifmw_edpm_prepare_kustomizations: + - apiVersion: kustomize.config.k8s.io/v1beta1 + kind: Kustomization + namespace: openstack + patches: + - patch: |- + apiVersion: core.openstack.org/v1beta1 + kind: OpenStackControlPlane + metadata: + name: unused + spec: + neutron: + template: + replicas: 3 + customServiceConfig: | + [DEFAULT] + debug=true + target: + kind: OpenStackControlPlane