[stable/2023.2] feat: add neutron_policy_server support (#1487)

Depends-On https://github.com/vexxhost/neutron-policy-server/pull/1\n\nCloses #1483
diff --git a/charts/neutron/templates/bin/_neutron-policy-server.sh.tpl b/charts/neutron/templates/bin/_neutron-policy-server.sh.tpl
new file mode 100644
index 0000000..3ff9c8d
--- /dev/null
+++ b/charts/neutron/templates/bin/_neutron-policy-server.sh.tpl
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+{{/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/}}
+
+set -ex
+COMMAND="${@:-start}"
+
+function start () {
+  exec uwsgi --ini /etc/neutron/neutron-policy-server-uwsgi.ini
+}
+
+function stop () {
+  kill -TERM 1
+}
+
+$COMMAND
diff --git a/charts/neutron/templates/configmap-bin.yaml b/charts/neutron/templates/configmap-bin.yaml
index 40b7006..b6ad4af 100644
--- a/charts/neutron/templates/configmap-bin.yaml
+++ b/charts/neutron/templates/configmap-bin.yaml
@@ -91,6 +91,8 @@
 {{- end }}
   neutron-server.sh: |
 {{ tuple "bin/_neutron-server.sh.tpl" . | include "helm-toolkit.utils.template" | indent 4 }}
+  neutron-policy-server.sh: |
+{{ tuple "bin/_neutron-policy-server.sh.tpl" . | include "helm-toolkit.utils.template" | indent 4 }}
   neutron-rpc-server.sh: |
 {{ tuple "bin/_neutron-rpc-server.sh.tpl" . | include "helm-toolkit.utils.template" | indent 4 }}
   neutron-ironic-agent.sh: |
diff --git a/charts/neutron/templates/configmap-etc.yaml b/charts/neutron/templates/configmap-etc.yaml
index f7411bf..bd52753 100644
--- a/charts/neutron/templates/configmap-etc.yaml
+++ b/charts/neutron/templates/configmap-etc.yaml
@@ -198,6 +198,15 @@
 {{- $_ := set .Values.conf.neutron_api_uwsgi.uwsgi "http-socket" $http_socket -}}
 {{- end -}}
 
+{{- if empty .Values.conf.neutron_policy_server_uwsgi.uwsgi.processes -}}
+{{- $_ := set .Values.conf.neutron_policy_server_uwsgi.uwsgi "processes" .Values.conf.neutron.DEFAULT.api_workers -}}
+{{- end -}}
+{{- if empty (index .Values.conf.neutron_policy_server_uwsgi.uwsgi "http-socket") -}}
+{{- $http_socket_port := tuple "network" "service" "policy_server" . | include "helm-toolkit.endpoints.endpoint_port_lookup" | toString }}
+{{- $http_socket := printf "0.0.0.0:%s" $http_socket_port }}
+{{- $_ := set .Values.conf.neutron_policy_server_uwsgi.uwsgi "http-socket" $http_socket -}}
+{{- end -}}
+
 {{- if and (empty .Values.conf.logging.handler_fluent) (has "fluent" .Values.conf.logging.handlers.keys) -}}
 {{- $fluentd_host := tuple "fluentd" "internal" $envAll | include "helm-toolkit.endpoints.hostname_namespaced_endpoint_lookup" }}
 {{- $fluentd_port := tuple "fluentd" "internal" "service" $envAll | include "helm-toolkit.endpoints.endpoint_port_lookup" }}
@@ -293,6 +302,7 @@
   api-paste.ini: {{ include "helm-toolkit.utils.to_ini" $envAll.Values.conf.paste | b64enc }}
   policy.yaml: {{ toYaml $envAll.Values.conf.policy | b64enc }}
   neutron-api-uwsgi.ini: {{ include "helm-toolkit.utils.to_oslo_conf" .Values.conf.neutron_api_uwsgi | b64enc }}
+  neutron-policy-server-uwsgi.ini: {{ include "helm-toolkit.utils.to_oslo_conf" .Values.conf.neutron_policy_server_uwsgi | b64enc }}
   neutron.conf: {{ include "helm-toolkit.utils.to_oslo_conf" $envAll.Values.conf.neutron | b64enc }}
   logging.conf: {{ include "helm-toolkit.utils.to_oslo_conf" .Values.conf.logging | b64enc }}
   api_audit_map.conf: {{ include "helm-toolkit.utils.to_oslo_conf" .Values.conf.api_audit_map | b64enc }}
diff --git a/charts/neutron/templates/deployment-server.yaml b/charts/neutron/templates/deployment-server.yaml
index b6b634d..457401b 100644
--- a/charts/neutron/templates/deployment-server.yaml
+++ b/charts/neutron/templates/deployment-server.yaml
@@ -275,6 +275,64 @@
 {{- dict "enabled" (or .Values.manifests.certificates .Values.tls.identity) "name" .Values.secrets.tls.network.server.internal "path" "/etc/neutron/certs" | include "helm-toolkit.snippets.tls_volume_mount" | indent 12 }}
 {{- dict "enabled" $envAll.Values.manifests.certificates "name" $envAll.Values.endpoints.oslo_messaging.auth.admin.secret.tls.internal "path" "/etc/rabbitmq/certs" | include "helm-toolkit.snippets.tls_volume_mount" | indent 12 }}
 {{ if $mounts_neutron_server.volumeMounts }}{{ toYaml $mounts_neutron_server.volumeMounts | indent 12 }}{{ end }}
+        {{- if .Values.pod.sidecars.neutron_policy_server }}
+        - name: neutron-policy-server
+{{ tuple $envAll "neutron_policy_server" | include "helm-toolkit.snippets.image" | indent 10 }}
+{{ tuple $envAll $envAll.Values.pod.resources.neutron_policy_server | include "helm-toolkit.snippets.kubernetes_resources" | indent 10 }}
+{{ dict "envAll" $envAll "application" "neutron_server" "container" "neutron_policy_server" | include "helm-toolkit.snippets.kubernetes_container_security_context" | indent 10 }}
+          command:
+            - /tmp/neutron-policy-server.sh
+            - start
+          ports:
+            - name: q-policy
+              containerPort: {{ tuple "network" "service" "policy_server" . | include "helm-toolkit.endpoints.endpoint_port_lookup" }}
+          readinessProbe:
+            httpGet:
+              path: /health
+              port: {{ tuple "network" "service" "policy_server" . | include "helm-toolkit.endpoints.endpoint_port_lookup" }}
+          volumeMounts:
+            - name: neutron-bin
+              mountPath: /tmp/neutron-policy-server.sh
+              subPath: neutron-policy-server.sh
+              readOnly: true
+            - name: neutron-etc
+              mountPath: /etc/neutron/neutron-policy-server-uwsgi.ini
+              subPath: neutron-policy-server-uwsgi.ini
+              readOnly: true
+            - name: neutron-etc
+              mountPath: /etc/neutron/neutron.conf
+              subPath: neutron.conf
+              readOnly: true
+            {{- if( has "tungstenfabric" .Values.network.backend ) }}
+            - name: neutron-etc
+              mountPath: /etc/neutron/plugins/tungstenfabric/tf_plugin.ini
+              subPath: tf_plugin.ini
+              readOnly: true
+            - name: neutron-etc
+              mountPath: /etc/contrail/vnc_api_lib.ini
+              subPath: vnc_api_lib.ini
+              readOnly: true
+            - name: neutron-plugin-shared
+              mountPath: /opt/plugin
+            - name: neutron-bin
+              mountPath: /usr/local/lib/python2.7/site-packages/tf-plugin.pth
+              subPath: tf-plugin.pth
+              readOnly: true
+            - name: neutron-bin
+              mountPath: /var/lib/openstack/lib/python2.7/site-packages/tf-plugin.pth
+              subPath: tf-plugin.pth
+              readOnly: true
+            - name: neutron-bin
+              mountPath: /var/lib/openstack/lib/python3.6/site-packages/tf-plugin.pth
+              subPath: tf-plugin.pth
+              readOnly: true
+            {{- else }}
+            - name: neutron-etc
+              mountPath: /etc/neutron/plugins/ml2/ml2_conf.ini
+              subPath: ml2_conf.ini
+              readOnly: true
+            {{- end }}
+        {{- end }}
       volumes:
         - name: pod-tmp
           emptyDir: {}
diff --git a/charts/neutron/templates/service-server.yaml b/charts/neutron/templates/service-server.yaml
index 8fcee66..c7075b1 100644
--- a/charts/neutron/templates/service-server.yaml
+++ b/charts/neutron/templates/service-server.yaml
@@ -26,6 +26,11 @@
     {{ if .Values.network.server.node_port.enabled }}
       nodePort: {{ .Values.network.server.node_port.port }}
     {{ end }}
+    - name: q-policy
+      port: {{ tuple "network" "service" "policy_server" . | include "helm-toolkit.endpoints.endpoint_port_lookup" }}
+    {{ if .Values.network.server.node_port.enabled }}
+      nodePort: {{ .Values.network.server.node_port.port }}
+    {{ end }}
   selector:
 {{ tuple $envAll "neutron" "server" | include "helm-toolkit.snippets.kubernetes_metadata_labels" | indent 4 }}
   {{ if .Values.network.server.node_port.enabled }}
diff --git a/charts/neutron/values.yaml b/charts/neutron/values.yaml
index 4e9c6a1..cd3889b 100644
--- a/charts/neutron/values.yaml
+++ b/charts/neutron/values.yaml
@@ -32,6 +32,7 @@
     ks_endpoints: docker.io/openstackhelm/heat:2024.1-ubuntu_jammy
     netoffload: ghcr.io/vexxhost/netoffload:v1.0.1
     neutron_server: docker.io/openstackhelm/neutron:2024.1-ubuntu_jammy
+    neutron_policy_server: docker.io/openstackhelm/neutron:2024.1-ubuntu_jammy
     neutron_rpc_server: docker.io/openstackhelm/neutron:2024.1-ubuntu_jammy
     neutron_dhcp: docker.io/openstackhelm/neutron:2024.1-ubuntu_jammy
     neutron_metadata: docker.io/openstackhelm/neutron:2024.1-ubuntu_jammy
@@ -372,6 +373,8 @@
           service: local_image_registry
 
 pod:
+  sidecars:
+    neutron_policy_server: false
   use_fqdn:
     neutron_agent: true
   probes:
@@ -615,6 +618,9 @@
         neutron_server:
           allowPrivilegeEscalation: false
           readOnlyRootFilesystem: true
+        neutron_policy_server:
+          allowPrivilegeEscalation: false
+          readOnlyRootFilesystem: true
     neutron_rpc_server:
       pod:
         runAsUser: 42424
@@ -890,6 +896,13 @@
       limits:
         memory: "1024Mi"
         cpu: "2000m"
+    neutron_policy_server:
+      requests:
+        memory: "128Mi"
+        cpu: "100m"
+      limits:
+        memory: "256Mi"
+        cpu: "500m"
     ironic_agent:
       requests:
         memory: "128Mi"
@@ -1318,6 +1331,22 @@
       thunder-lock: true
       worker-reload-mercy: 80
       wsgi-file: /var/lib/openstack/bin/neutron-api
+  neutron_policy_server_uwsgi:
+    uwsgi:
+      add-header: "Connection: close"
+      buffer-size: 65535
+      die-on-term: true
+      enable-threads: true
+      exit-on-reload: false
+      hook-master-start: unix_signal:15 gracefully_kill_them_all
+      lazy-apps: true
+      log-x-forwarded-for: true
+      master: true
+      procname-prefix-spaced: "neutron-policy-server:"
+      route-user-agent: '^kube-probe.* donotlog:'
+      thunder-lock: true
+      worker-reload-mercy: 80
+      wsgi-file: /var/lib/openstack/bin/neutron-policy-server-wsgi
   policy: {}
   api_audit_map:
     DEFAULT:
@@ -2463,6 +2492,10 @@
         default: 9696
         public: 80
         service: 9696
+      policy_server:
+        default: 9697
+        public: 80
+        service: 9697
   load_balancer:
     name: octavia
     hosts:
diff --git a/charts/patches/neutron/0002-add-neutron-policy-server.patch b/charts/patches/neutron/0002-add-neutron-policy-server.patch
new file mode 100644
index 0000000..3878556
--- /dev/null
+++ b/charts/patches/neutron/0002-add-neutron-policy-server.patch
@@ -0,0 +1,239 @@
+diff --git a/neutron/templates/bin/_neutron-policy-server.sh.tpl b/neutron/templates/bin/_neutron-policy-server.sh.tpl
+new file mode 100644
+index 00000000..3ff9c8dd
+--- /dev/null
++++ b/neutron/templates/bin/_neutron-policy-server.sh.tpl
+@@ -0,0 +1,28 @@
++#!/bin/bash
++
++{{/*
++Licensed under the Apache License, Version 2.0 (the "License");
++you may not use this file except in compliance with the License.
++You may obtain a copy of the License at
++
++   http://www.apache.org/licenses/LICENSE-2.0
++
++Unless required by applicable law or agreed to in writing, software
++distributed under the License is distributed on an "AS IS" BASIS,
++WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++See the License for the specific language governing permissions and
++limitations under the License.
++*/}}
++
++set -ex
++COMMAND="${@:-start}"
++
++function start () {
++  exec uwsgi --ini /etc/neutron/neutron-policy-server-uwsgi.ini
++}
++
++function stop () {
++  kill -TERM 1
++}
++
++$COMMAND
+diff --git a/neutron/templates/configmap-bin.yaml b/neutron/templates/configmap-bin.yaml
+index 40b70060..b6ad4af4 100644
+--- a/neutron/templates/configmap-bin.yaml
++++ b/neutron/templates/configmap-bin.yaml
+@@ -91,6 +91,8 @@ data:
+ {{- end }}
+   neutron-server.sh: |
+ {{ tuple "bin/_neutron-server.sh.tpl" . | include "helm-toolkit.utils.template" | indent 4 }}
++  neutron-policy-server.sh: |
++{{ tuple "bin/_neutron-policy-server.sh.tpl" . | include "helm-toolkit.utils.template" | indent 4 }}
+   neutron-rpc-server.sh: |
+ {{ tuple "bin/_neutron-rpc-server.sh.tpl" . | include "helm-toolkit.utils.template" | indent 4 }}
+   neutron-ironic-agent.sh: |
+diff --git a/neutron/templates/configmap-etc.yaml b/neutron/templates/configmap-etc.yaml
+index f7411bf5..bd52753d 100644
+--- a/neutron/templates/configmap-etc.yaml
++++ b/neutron/templates/configmap-etc.yaml
+@@ -198,6 +198,15 @@ limitations under the License.
+ {{- $_ := set .Values.conf.neutron_api_uwsgi.uwsgi "http-socket" $http_socket -}}
+ {{- end -}}
+ 
++{{- if empty .Values.conf.neutron_policy_server_uwsgi.uwsgi.processes -}}
++{{- $_ := set .Values.conf.neutron_policy_server_uwsgi.uwsgi "processes" .Values.conf.neutron.DEFAULT.api_workers -}}
++{{- end -}}
++{{- if empty (index .Values.conf.neutron_policy_server_uwsgi.uwsgi "http-socket") -}}
++{{- $http_socket_port := tuple "network" "service" "policy_server" . | include "helm-toolkit.endpoints.endpoint_port_lookup" | toString }}
++{{- $http_socket := printf "0.0.0.0:%s" $http_socket_port }}
++{{- $_ := set .Values.conf.neutron_policy_server_uwsgi.uwsgi "http-socket" $http_socket -}}
++{{- end -}}
++
+ {{- if and (empty .Values.conf.logging.handler_fluent) (has "fluent" .Values.conf.logging.handlers.keys) -}}
+ {{- $fluentd_host := tuple "fluentd" "internal" $envAll | include "helm-toolkit.endpoints.hostname_namespaced_endpoint_lookup" }}
+ {{- $fluentd_port := tuple "fluentd" "internal" "service" $envAll | include "helm-toolkit.endpoints.endpoint_port_lookup" }}
+@@ -293,6 +302,7 @@ data:
+   api-paste.ini: {{ include "helm-toolkit.utils.to_ini" $envAll.Values.conf.paste | b64enc }}
+   policy.yaml: {{ toYaml $envAll.Values.conf.policy | b64enc }}
+   neutron-api-uwsgi.ini: {{ include "helm-toolkit.utils.to_oslo_conf" .Values.conf.neutron_api_uwsgi | b64enc }}
++  neutron-policy-server-uwsgi.ini: {{ include "helm-toolkit.utils.to_oslo_conf" .Values.conf.neutron_policy_server_uwsgi | b64enc }}
+   neutron.conf: {{ include "helm-toolkit.utils.to_oslo_conf" $envAll.Values.conf.neutron | b64enc }}
+   logging.conf: {{ include "helm-toolkit.utils.to_oslo_conf" .Values.conf.logging | b64enc }}
+   api_audit_map.conf: {{ include "helm-toolkit.utils.to_oslo_conf" .Values.conf.api_audit_map | b64enc }}
+diff --git a/neutron/templates/deployment-server.yaml b/neutron/templates/deployment-server.yaml
+index b6b634d2..457401b4 100644
+--- a/neutron/templates/deployment-server.yaml
++++ b/neutron/templates/deployment-server.yaml
+@@ -275,6 +275,64 @@ spec:
+ {{- dict "enabled" (or .Values.manifests.certificates .Values.tls.identity) "name" .Values.secrets.tls.network.server.internal "path" "/etc/neutron/certs" | include "helm-toolkit.snippets.tls_volume_mount" | indent 12 }}
+ {{- dict "enabled" $envAll.Values.manifests.certificates "name" $envAll.Values.endpoints.oslo_messaging.auth.admin.secret.tls.internal "path" "/etc/rabbitmq/certs" | include "helm-toolkit.snippets.tls_volume_mount" | indent 12 }}
+ {{ if $mounts_neutron_server.volumeMounts }}{{ toYaml $mounts_neutron_server.volumeMounts | indent 12 }}{{ end }}
++        {{- if .Values.pod.sidecars.neutron_policy_server }}
++        - name: neutron-policy-server
++{{ tuple $envAll "neutron_policy_server" | include "helm-toolkit.snippets.image" | indent 10 }}
++{{ tuple $envAll $envAll.Values.pod.resources.neutron_policy_server | include "helm-toolkit.snippets.kubernetes_resources" | indent 10 }}
++{{ dict "envAll" $envAll "application" "neutron_server" "container" "neutron_policy_server" | include "helm-toolkit.snippets.kubernetes_container_security_context" | indent 10 }}
++          command:
++            - /tmp/neutron-policy-server.sh
++            - start
++          ports:
++            - name: q-policy
++              containerPort: {{ tuple "network" "service" "policy_server" . | include "helm-toolkit.endpoints.endpoint_port_lookup" }}
++          readinessProbe:
++            httpGet:
++              path: /health
++              port: {{ tuple "network" "service" "policy_server" . | include "helm-toolkit.endpoints.endpoint_port_lookup" }}
++          volumeMounts:
++            - name: neutron-bin
++              mountPath: /tmp/neutron-policy-server.sh
++              subPath: neutron-policy-server.sh
++              readOnly: true
++            - name: neutron-etc
++              mountPath: /etc/neutron/neutron-policy-server-uwsgi.ini
++              subPath: neutron-policy-server-uwsgi.ini
++              readOnly: true
++            - name: neutron-etc
++              mountPath: /etc/neutron/neutron.conf
++              subPath: neutron.conf
++              readOnly: true
++            {{- if( has "tungstenfabric" .Values.network.backend ) }}
++            - name: neutron-etc
++              mountPath: /etc/neutron/plugins/tungstenfabric/tf_plugin.ini
++              subPath: tf_plugin.ini
++              readOnly: true
++            - name: neutron-etc
++              mountPath: /etc/contrail/vnc_api_lib.ini
++              subPath: vnc_api_lib.ini
++              readOnly: true
++            - name: neutron-plugin-shared
++              mountPath: /opt/plugin
++            - name: neutron-bin
++              mountPath: /usr/local/lib/python2.7/site-packages/tf-plugin.pth
++              subPath: tf-plugin.pth
++              readOnly: true
++            - name: neutron-bin
++              mountPath: /var/lib/openstack/lib/python2.7/site-packages/tf-plugin.pth
++              subPath: tf-plugin.pth
++              readOnly: true
++            - name: neutron-bin
++              mountPath: /var/lib/openstack/lib/python3.6/site-packages/tf-plugin.pth
++              subPath: tf-plugin.pth
++              readOnly: true
++            {{- else }}
++            - name: neutron-etc
++              mountPath: /etc/neutron/plugins/ml2/ml2_conf.ini
++              subPath: ml2_conf.ini
++              readOnly: true
++            {{- end }}
++        {{- end }}
+       volumes:
+         - name: pod-tmp
+           emptyDir: {}
+diff --git a/neutron/templates/service-server.yaml b/neutron/templates/service-server.yaml
+index 8fcee669..c7075b10 100644
+--- a/neutron/templates/service-server.yaml
++++ b/neutron/templates/service-server.yaml
+@@ -26,6 +26,11 @@ spec:
+     {{ if .Values.network.server.node_port.enabled }}
+       nodePort: {{ .Values.network.server.node_port.port }}
+     {{ end }}
++    - name: q-policy
++      port: {{ tuple "network" "service" "policy_server" . | include "helm-toolkit.endpoints.endpoint_port_lookup" }}
++    {{ if .Values.network.server.node_port.enabled }}
++      nodePort: {{ .Values.network.server.node_port.port }}
++    {{ end }}
+   selector:
+ {{ tuple $envAll "neutron" "server" | include "helm-toolkit.snippets.kubernetes_metadata_labels" | indent 4 }}
+   {{ if .Values.network.server.node_port.enabled }}
+diff --git a/neutron/values.yaml b/neutron/values.yaml
+index 4e9c6a11..495e0bab 100644
+--- a/neutron/values.yaml
++++ b/neutron/values.yaml
+@@ -32,6 +32,7 @@ images:
+     ks_endpoints: docker.io/openstackhelm/heat:2024.1-ubuntu_jammy
+     netoffload: ghcr.io/vexxhost/netoffload:v1.0.1
+     neutron_server: docker.io/openstackhelm/neutron:2024.1-ubuntu_jammy
++    neutron_policy_server: docker.io/openstackhelm/neutron:2024.1-ubuntu_jammy
+     neutron_rpc_server: docker.io/openstackhelm/neutron:2024.1-ubuntu_jammy
+     neutron_dhcp: docker.io/openstackhelm/neutron:2024.1-ubuntu_jammy
+     neutron_metadata: docker.io/openstackhelm/neutron:2024.1-ubuntu_jammy
+@@ -372,6 +373,8 @@ dependencies:
+           service: local_image_registry
+ 
+ pod:
++  sidecars:
++    neutron_policy_server: false
+   use_fqdn:
+     neutron_agent: true
+   probes:
+@@ -615,6 +618,9 @@ pod:
+         neutron_server:
+           allowPrivilegeEscalation: false
+           readOnlyRootFilesystem: true
++        neutron_policy_server:
++          allowPrivilegeEscalation: false
++          readOnlyRootFilesystem: true
+     neutron_rpc_server:
+       pod:
+         runAsUser: 42424
+@@ -890,6 +896,13 @@ pod:
+       limits:
+         memory: "1024Mi"
+         cpu: "2000m"
++    neutron_policy_server:
++      requests:
++        memory: "128Mi"
++        cpu: "100m"
++      limits:
++        memory: "256Mi"
++        cpu: "500m"
+     ironic_agent:
+       requests:
+         memory: "128Mi"
+@@ -1318,6 +1331,22 @@ conf:
+       thunder-lock: true
+       worker-reload-mercy: 80
+       wsgi-file: /var/lib/openstack/bin/neutron-api
++  neutron_policy_server_uwsgi:
++    uwsgi:
++      add-header: "Connection: close"
++      buffer-size: 65535
++      die-on-term: true
++      enable-threads: true
++      exit-on-reload: false
++      hook-master-start: unix_signal:15 gracefully_kill_them_all
++      lazy-apps: true
++      log-x-forwarded-for: true
++      master: true
++      procname-prefix-spaced: "neutron-policy-server:"
++      route-user-agent: '^kube-probe.* donotlog:'
++      thunder-lock: true
++      worker-reload-mercy: 80
++      wsgi-file: /var/lib/openstack/bin/neutron-policy-server-wsgi
+   policy: {}
+   api_audit_map:
+     DEFAULT:
+@@ -2463,6 +2492,10 @@ endpoints:
+         default: 9696
+         public: 80
+         service: 9696
++      policy_server:
++        default: 9697
++        public: 80
++        service: 9697
+   load_balancer:
+     name: octavia
+     hosts:
diff --git a/images/neutron/Dockerfile b/images/neutron/Dockerfile
index a812984..36aa59f 100644
--- a/images/neutron/Dockerfile
+++ b/images/neutron/Dockerfile
@@ -10,11 +10,15 @@
 ARG NEUTRON_VPNAAS_GIT_REF=1d1b1282542516a615eba90a25faf1e9b278fc55
 ADD --keep-git-dir=true https://opendev.org/openstack/neutron-vpnaas.git#${NEUTRON_VPNAAS_GIT_REF} /src/neutron-vpnaas
 RUN git -C /src/neutron-vpnaas fetch --unshallow
+ARG POLICY_SERVER_GIT_REF=4a86b140d5510823a8fb8a59137feddf5b111b26
+ADD --keep-git-dir=true https://github.com/vexxhost/neutron-policy-server.git#${POLICY_SERVER_GIT_REF} /src/neutron-policy-server
+RUN git -C /src/neutron-policy-server fetch --unshallow
 RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
 pip3 install \
     --constraint /upper-constraints.txt \
         /src/neutron \
-        /src/neutron-vpnaas
+        /src/neutron-vpnaas \
+        /src/neutron-policy-server
 EOF
 
 FROM registry.atmosphere.dev/library/openstack-python-runtime:${RELEASE}
diff --git a/roles/defaults/vars/main.yml b/roles/defaults/vars/main.yml
index c365cd1..7314565 100644
--- a/roles/defaults/vars/main.yml
+++ b/roles/defaults/vars/main.yml
@@ -143,6 +143,7 @@
   neutron_rpc_server: "registry.atmosphere.dev/library/neutron:{{ atmosphere_release }}"
   neutron_sriov_agent_init: "registry.atmosphere.dev/library/neutron:{{ atmosphere_release }}"
   neutron_sriov_agent: "registry.atmosphere.dev/library/neutron:{{ atmosphere_release }}"
+  neutron_policy_server: "registry.atmosphere.dev/library/neutron:{{ atmosphere_release }}"
   node_feature_discovery: registry.k8s.io/nfd/node-feature-discovery:v0.15.4
   nova_api: "registry.atmosphere.dev/library/nova:{{ atmosphere_release }}"
   nova_archive_deleted_rows: "registry.atmosphere.dev/library/nova:{{ atmosphere_release }}"
diff --git a/roles/neutron/defaults/main.yml b/roles/neutron/defaults/main.yml
index c304bb8..1cb4215 100644
--- a/roles/neutron/defaults/main.yml
+++ b/roles/neutron/defaults/main.yml
@@ -27,3 +27,7 @@
 
 # Enable dns integration
 neutron_designate_integration_enabled: false
+
+# Enable neutron policy server to force external
+# policy check neutron port and address pairs actions.
+neutron_policy_server_integration_enabled: true
diff --git a/roles/neutron/tasks/main.yml b/roles/neutron/tasks/main.yml
index aae6b5c..eabe333 100644
--- a/roles/neutron/tasks/main.yml
+++ b/roles/neutron/tasks/main.yml
@@ -26,6 +26,11 @@
   ansible.builtin.set_fact:
     _neutron_helm_values: "{{ _neutron_helm_values | combine(__neutron_ovn_helm_values, recursive=True) }}"
 
+- name: Append Helm values (neutron_policy_server)
+  when: neutron_policy_server_integration_enabled | bool
+  ansible.builtin.set_fact:
+    _neutron_helm_values: "{{ _neutron_helm_values | combine(__neutron_policy_server_helm_values, recursive=True) }}"
+
 - name: Deploy Helm chart
   run_once: true
   kubernetes.core.helm:
diff --git a/roles/neutron/vars/main.yml b/roles/neutron/vars/main.yml
index 98a66e2..fc58750 100644
--- a/roles/neutron/vars/main.yml
+++ b/roles/neutron/vars/main.yml
@@ -23,6 +23,8 @@
     replicas:
       server: 3
       rpc_server: 3
+    sidecars:
+      neutron_policy_server: true
   conf:
     neutron:
       DEFAULT:
@@ -101,3 +103,10 @@
     daemonset_ovn_metadata_agent: true
     daemonset_ovs_agent: false
     deployment_rpc_server: false
+
+__neutron_policy_server_helm_values:
+  conf:
+    policy:
+      delete_port: "((rule:admin_only) or (rule:service_api) or role:member and rule:network_owner or role:member and project_id:%(project_id)s) and http://neutron-server:9697/port-delete"
+      update_port:mac_address: "((rule:admin_only) or (rule:service_api)) and http://neutron-server:9697/port-update"
+      update_port:fixed_ips: "((rule:admin_only) or (rule:service_api) or role:member and rule:network_owner) and http://neutron-server:9697/port-update"