From 4d5f2a58bbf5a366c43871ef27b75d31b3b2a114 Mon Sep 17 00:00:00 2001
From: John Estabrook <jestabro@vyos.io>
Date: Wed, 2 Oct 2024 12:36:41 -0500
Subject: config-mgmt: T5976: add option for commit-confirm to use 'soft'
 rollback

Commit-confirm will restore a previous configuration if a confirmation
is not received in N minutes. Traditionally, this was restored by a
reboot into the last configuration on disk; add a configurable option to
reload the last completed commit without a reboot. The default setting
is to reboot.
---
 .../system_config-management.xml.in                  | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

(limited to 'interface-definitions')

diff --git a/interface-definitions/system_config-management.xml.in b/interface-definitions/system_config-management.xml.in
index e666633b7..b8fb6cdb5 100644
--- a/interface-definitions/system_config-management.xml.in
+++ b/interface-definitions/system_config-management.xml.in
@@ -67,6 +67,26 @@
               <constraintErrorMessage>Number of revisions must be between 0 and 65535</constraintErrorMessage>
             </properties>
           </leafNode>
+          <leafNode name="commit-confirm">
+            <properties>
+              <help>Commit confirm rollback type if no confirmation</help>
+              <completionHelp>
+                <list>reload reboot</list>
+              </completionHelp>
+              <valueHelp>
+                <format>reload</format>
+                <description>Reload previous configuration if not confirmed</description>
+              </valueHelp>
+              <valueHelp>
+                <format>reboot</format>
+                <description>Reboot to saved configuration if not confirmed</description>
+              </valueHelp>
+              <constraint>
+                <regex>(reload|reboot)</regex>
+              </constraint>
+            </properties>
+            <defaultValue>reboot</defaultValue>
+          </leafNode>
         </children>
       </node>
     </children>
-- 
cgit v1.2.3


From 64196ec5fd90a8caedef88edb9068a32e4e6abfa Mon Sep 17 00:00:00 2001
From: John Estabrook <jestabro@vyos.io>
Date: Mon, 7 Oct 2024 12:15:44 -0500
Subject: config-mgmt: T5976: move commit-confirm revert action to subnode

---
 .../system_config-management.xml.in                | 43 +++++++++++++---------
 src/conf_mode/system_config-management.py          |  3 +-
 2 files changed, 27 insertions(+), 19 deletions(-)

(limited to 'interface-definitions')

diff --git a/interface-definitions/system_config-management.xml.in b/interface-definitions/system_config-management.xml.in
index b8fb6cdb5..a23d44aea 100644
--- a/interface-definitions/system_config-management.xml.in
+++ b/interface-definitions/system_config-management.xml.in
@@ -67,26 +67,33 @@
               <constraintErrorMessage>Number of revisions must be between 0 and 65535</constraintErrorMessage>
             </properties>
           </leafNode>
-          <leafNode name="commit-confirm">
+          <node name="commit-confirm">
             <properties>
-              <help>Commit confirm rollback type if no confirmation</help>
-              <completionHelp>
-                <list>reload reboot</list>
-              </completionHelp>
-              <valueHelp>
-                <format>reload</format>
-                <description>Reload previous configuration if not confirmed</description>
-              </valueHelp>
-              <valueHelp>
-                <format>reboot</format>
-                <description>Reboot to saved configuration if not confirmed</description>
-              </valueHelp>
-              <constraint>
-                <regex>(reload|reboot)</regex>
-              </constraint>
+              <help>Commit confirm options</help>
             </properties>
-            <defaultValue>reboot</defaultValue>
-          </leafNode>
+            <children>
+              <leafNode name="action">
+                <properties>
+                  <help>Commit confirm revert action</help>
+                  <completionHelp>
+                    <list>reload reboot</list>
+                  </completionHelp>
+                  <valueHelp>
+                    <format>reload</format>
+                    <description>Reload previous configuration if not confirmed</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>reboot</format>
+                    <description>Reboot to saved configuration if not confirmed</description>
+                  </valueHelp>
+                  <constraint>
+                    <regex>(reload|reboot)</regex>
+                  </constraint>
+                </properties>
+                <defaultValue>reboot</defaultValue>
+              </leafNode>
+            </children>
+          </node>
         </children>
       </node>
     </children>
diff --git a/src/conf_mode/system_config-management.py b/src/conf_mode/system_config-management.py
index a1ee136cd..8de4e5342 100755
--- a/src/conf_mode/system_config-management.py
+++ b/src/conf_mode/system_config-management.py
@@ -40,7 +40,8 @@ def get_config(config=None):
 
 def verify(mgmt):
     d = mgmt.config_dict
-    if d.get('commit_confirm', '') == 'reload' and 'commit_revisions' not in d:
+    confirm = d.get('commit_confirm', {})
+    if confirm.get('action', '') == 'reload' and 'commit_revisions' not in d:
         raise ConfigError('commit-confirm reload requires non-zero commit-revisions')
 
     return
-- 
cgit v1.2.3


From 90a4827284acd3cb072cdfeef323c522802c6449 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Wed, 9 Oct 2024 14:55:11 +0200
Subject: haproxy: T6745: Rename `reverse-proxy` to `haproxy`

---
 data/config-mode-dependencies/vyos-1x.json         |   2 +-
 data/op-mode-standardized.json                     |   2 +-
 data/templates/load-balancing/haproxy.cfg.j2       |   2 +-
 debian/control                                     |   4 +-
 .../include/version/reverseproxy-version.xml.i     |   2 +-
 .../load-balancing_haproxy.xml.in                  | 344 ++++++++++++++
 .../load-balancing_reverse-proxy.xml.in            | 344 --------------
 op-mode-definitions/load-balacing_haproxy.in       |  23 +
 op-mode-definitions/reverse-proxy.xml.in           |  23 -
 .../scripts/cli/test_load-balancing_haproxy.py     | 502 +++++++++++++++++++++
 .../cli/test_load-balancing_reverse-proxy.py       | 502 ---------------------
 src/conf_mode/load-balancing_haproxy.py            | 206 +++++++++
 src/conf_mode/load-balancing_reverse-proxy.py      | 206 ---------
 src/conf_mode/pki.py                               |   2 +-
 src/migration-scripts/reverse-proxy/1-to-2         |  27 ++
 src/op_mode/load-balancing_haproxy.py              | 237 ++++++++++
 src/op_mode/restart.py                             |  10 +-
 src/op_mode/reverseproxy.py                        | 237 ----------
 18 files changed, 1351 insertions(+), 1324 deletions(-)
 create mode 100644 interface-definitions/load-balancing_haproxy.xml.in
 delete mode 100644 interface-definitions/load-balancing_reverse-proxy.xml.in
 create mode 100644 op-mode-definitions/load-balacing_haproxy.in
 delete mode 100644 op-mode-definitions/reverse-proxy.xml.in
 create mode 100755 smoketest/scripts/cli/test_load-balancing_haproxy.py
 delete mode 100755 smoketest/scripts/cli/test_load-balancing_reverse-proxy.py
 create mode 100644 src/conf_mode/load-balancing_haproxy.py
 delete mode 100755 src/conf_mode/load-balancing_reverse-proxy.py
 create mode 100755 src/migration-scripts/reverse-proxy/1-to-2
 create mode 100755 src/op_mode/load-balancing_haproxy.py
 delete mode 100755 src/op_mode/reverseproxy.py

(limited to 'interface-definitions')

diff --git a/data/config-mode-dependencies/vyos-1x.json b/data/config-mode-dependencies/vyos-1x.json
index 2981a0851..cbd14f7c6 100644
--- a/data/config-mode-dependencies/vyos-1x.json
+++ b/data/config-mode-dependencies/vyos-1x.json
@@ -26,10 +26,10 @@
     "pki": {
         "ethernet": ["interfaces_ethernet"],
         "openvpn": ["interfaces_openvpn"],
+        "haproxy": ["load-balancing_haproxy"],
         "https": ["service_https"],
         "ipsec": ["vpn_ipsec"],
         "openconnect": ["vpn_openconnect"],
-        "reverse_proxy": ["load-balancing_reverse-proxy"],
         "rpki": ["protocols_rpki"],
         "sstp": ["vpn_sstp"],
         "sstpc": ["interfaces_sstpc"],
diff --git a/data/op-mode-standardized.json b/data/op-mode-standardized.json
index baa1e9110..35587b63c 100644
--- a/data/op-mode-standardized.json
+++ b/data/op-mode-standardized.json
@@ -25,7 +25,7 @@
 "otp.py",
 "qos.py",
 "reset_vpn.py",
-"reverseproxy.py",
+"load-balancing_haproxy.py",
 "route.py",
 "storage.py",
 "system.py",
diff --git a/data/templates/load-balancing/haproxy.cfg.j2 b/data/templates/load-balancing/haproxy.cfg.j2
index 5137966c1..786ebfb21 100644
--- a/data/templates/load-balancing/haproxy.cfg.j2
+++ b/data/templates/load-balancing/haproxy.cfg.j2
@@ -1,4 +1,4 @@
-### Autogenerated by load-balancing_reverse-proxy.py ###
+### Autogenerated by load-balancing_haproxy.py ###
 
 global
     chroot /var/lib/haproxy
diff --git a/debian/control b/debian/control
index 15fb5d72e..20cfcdc43 100644
--- a/debian/control
+++ b/debian/control
@@ -202,9 +202,9 @@ Depends:
 # For "service router-advert"
   radvd,
 # End "service route-advert"
-# For "load-balancing reverse-proxy"
+# For "load-balancing haproxy"
   haproxy,
-# End "load-balancing reverse-proxy"
+# End "load-balancing haproxy"
 # For "load-balancing wan"
   vyatta-wanloadbalance,
 # End "load-balancing wan"
diff --git a/interface-definitions/include/version/reverseproxy-version.xml.i b/interface-definitions/include/version/reverseproxy-version.xml.i
index 907ea1e5e..4f09f2848 100644
--- a/interface-definitions/include/version/reverseproxy-version.xml.i
+++ b/interface-definitions/include/version/reverseproxy-version.xml.i
@@ -1,3 +1,3 @@
 <!-- include start from include/version/reverseproxy-version.xml.i -->
-<syntaxVersion component='reverse-proxy' version='1'></syntaxVersion>
+<syntaxVersion component='reverse-proxy' version='2'></syntaxVersion>
 <!-- include end -->
diff --git a/interface-definitions/load-balancing_haproxy.xml.in b/interface-definitions/load-balancing_haproxy.xml.in
new file mode 100644
index 000000000..742272436
--- /dev/null
+++ b/interface-definitions/load-balancing_haproxy.xml.in
@@ -0,0 +1,344 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+  <node name="load-balancing">
+    <children>
+      <node name="haproxy" owner="${vyos_conf_scripts_dir}/load-balancing_haproxy.py">
+        <properties>
+          <help>Configure haproxy</help>
+          <priority>900</priority>
+        </properties>
+        <children>
+          <tagNode name="service">
+            <properties>
+              <help>Frontend service name</help>
+              <constraint>
+                #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i>
+              </constraint>
+              <constraintErrorMessage>Server name must be alphanumeric and can contain hyphen and underscores</constraintErrorMessage>
+            </properties>
+            <children>
+              <leafNode name="backend">
+                <properties>
+                  <help>Backend member</help>
+                  <constraint>
+                    #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i>
+                  </constraint>
+                  <constraintErrorMessage>Backend name must be alphanumeric and can contain hyphen and underscores</constraintErrorMessage>
+                  <valueHelp>
+                    <format>txt</format>
+                    <description>Name of haproxy backend system</description>
+                  </valueHelp>
+                  <completionHelp>
+                    <path>load-balancing haproxy backend</path>
+                  </completionHelp>
+                  <multi/>
+                </properties>
+              </leafNode>
+              #include <include/generic-description.xml.i>
+              #include <include/listen-address.xml.i>
+              #include <include/haproxy/logging.xml.i>
+              #include <include/haproxy/mode.xml.i>
+              #include <include/port-number.xml.i>
+              #include <include/haproxy/rule-frontend.xml.i>
+              #include <include/haproxy/tcp-request.xml.i>
+              #include <include/haproxy/http-response-headers.xml.i>
+              <leafNode name="redirect-http-to-https">
+                <properties>
+                  <help>Redirect HTTP to HTTPS</help>
+                  <valueless/>
+                </properties>
+              </leafNode>
+              <node name="ssl">
+                <properties>
+                  <help>SSL Certificate, SSL Key and CA</help>
+                </properties>
+                <children>
+                  #include <include/pki/certificate-multi.xml.i>
+                </children>
+              </node>
+            </children>
+          </tagNode>
+          <tagNode name="backend">
+            <properties>
+              <help>Backend server name</help>
+              <constraint>
+                #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i>
+              </constraint>
+              <constraintErrorMessage>Backend name must be alphanumeric and can contain hyphen and underscores</constraintErrorMessage>
+            </properties>
+            <children>
+              <leafNode name="balance">
+                <properties>
+                  <help>Load-balancing algorithm</help>
+                  <completionHelp>
+                    <list>source-address round-robin least-connection</list>
+                  </completionHelp>
+                  <valueHelp>
+                    <format>source-address</format>
+                    <description>Based on hash of source IP address</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>round-robin</format>
+                    <description>Round robin</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>least-connection</format>
+                    <description>Least connection</description>
+                  </valueHelp>
+                  <constraint>
+                    <regex>(source-address|round-robin|least-connection)</regex>
+                  </constraint>
+                </properties>
+                <defaultValue>round-robin</defaultValue>
+              </leafNode>
+              #include <include/generic-description.xml.i>
+              #include <include/haproxy/logging.xml.i>
+              #include <include/haproxy/mode.xml.i>
+              #include <include/haproxy/http-response-headers.xml.i>
+              <node name="http-check">
+                <properties>
+                  <help>HTTP check configuration</help>
+                </properties>
+                <children>
+                  <leafNode name="method">
+                    <properties>
+                      <help>HTTP method used for health check</help>
+                      <completionHelp>
+                        <list>options head get post put</list>
+                      </completionHelp>
+                      <valueHelp>
+                        <format>options|head|get|post|put</format>
+                        <description>HTTP method used for health checking</description>
+                      </valueHelp>
+                      <constraint>
+                        <regex>(options|head|get|post|put)</regex>
+                      </constraint>
+                    </properties>
+                  </leafNode>
+                  <leafNode name="uri">
+                    <properties>
+                      <help>URI used for HTTP health check (Example: '/' or '/health')</help>
+                      <constraint>
+                        <regex>^\/([^?#\s]*)(\?[^#\s]*)?$</regex>
+                      </constraint>
+                    </properties>
+                  </leafNode>
+                  <node name="expect">
+                    <properties>
+                      <help>Expected response for the health check to pass</help>
+                    </properties>
+                    <children>
+                      <leafNode name="status">
+                        <properties>
+                          <help>Expected response status code for the health check to pass</help>
+                          <valueHelp>
+                            <format>u32:200-399</format>
+                            <description>Expected response code</description>
+                          </valueHelp>
+                          <constraint>
+                            <validator name="numeric" argument="--range 200-399"/>
+                          </constraint>
+                          <constraintErrorMessage>Status code must be in range 200-399</constraintErrorMessage>
+                        </properties>
+                      </leafNode>
+                      <leafNode name="string">
+                        <properties>
+                          <help>Expected to be in response body for the health check to pass</help>
+                          <valueHelp>
+                            <format>txt</format>
+                            <description>A string expected to be in the response</description>
+                          </valueHelp>
+                        </properties>
+                      </leafNode>
+                    </children>
+                  </node>
+                </children>
+              </node>
+              <leafNode name="health-check">
+                <properties>
+                  <help>Non HTTP health check options</help>
+                  <completionHelp>
+                    <list>ldap mysql pgsql redis smtp</list>
+                  </completionHelp>
+                  <valueHelp>
+                    <format>ldap</format>
+                    <description>LDAP protocol check</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>mysql</format>
+                    <description>MySQL protocol check</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>pgsql</format>
+                    <description>PostgreSQL protocol check</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>redis</format>
+                    <description>Redis protocol check</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>smtp</format>
+                    <description>SMTP protocol check</description>
+                  </valueHelp>
+                  <constraint>
+                    <regex>(ldap|mysql|redis|pgsql|smtp)</regex>
+                  </constraint>
+                </properties>
+              </leafNode>
+              #include <include/haproxy/rule-backend.xml.i>
+              <tagNode name="server">
+                <properties>
+                  <help>Backend server name</help>
+                </properties>
+                <children>
+                  <leafNode name="address">
+                    <properties>
+                      <help>Backend server address</help>
+                      <valueHelp>
+                        <format>ipv4</format>
+                        <description>IPv4 unicast peer address</description>
+                      </valueHelp>
+                      <valueHelp>
+                        <format>ipv6</format>
+                        <description>IPv6 unicast peer address</description>
+                      </valueHelp>
+                      <constraint>
+                        <validator name="ip-address"/>
+                      </constraint>
+                    </properties>
+                  </leafNode>
+                  <leafNode name="backup">
+                    <properties>
+                      <help>Use backup server if other servers are not available</help>
+                      <valueless/>
+                    </properties>
+                  </leafNode>
+                  <leafNode name="check">
+                    <properties>
+                      <help>Active health check backend server</help>
+                      <valueless/>
+                    </properties>
+                  </leafNode>
+                  #include <include/port-number.xml.i>
+                  <leafNode name="send-proxy">
+                    <properties>
+                      <help>Send a Proxy Protocol version 1 header (text format)</help>
+                      <valueless/>
+                    </properties>
+                  </leafNode>
+                  <leafNode name="send-proxy-v2">
+                    <properties>
+                      <help>Send a Proxy Protocol version 2 header (binary format)</help>
+                      <valueless/>
+                    </properties>
+                  </leafNode>
+                </children>
+              </tagNode>
+              <node name="ssl">
+                <properties>
+                  <help>SSL Certificate, SSL Key and CA</help>
+                </properties>
+                <children>
+                  #include <include/pki/ca-certificate.xml.i>
+                  <leafNode name="no-verify">
+                    <properties>
+                      <help>Do not attempt to verify SSL certificates for backend servers</help>
+                      <valueless/>
+                    </properties>
+                  </leafNode>
+                </children>
+              </node>
+              #include <include/haproxy/timeout.xml.i>
+            </children>
+          </tagNode>
+          <node name="global-parameters">
+            <properties>
+              <help>Global perfomance parameters and limits</help>
+            </properties>
+            <children>
+              #include <include/haproxy/logging.xml.i>
+              <leafNode name="max-connections">
+                <properties>
+                  <help>Maximum allowed connections</help>
+                  <valueHelp>
+                    <format>u32:1-2000000</format>
+                    <description>Maximum allowed connections</description>
+                  </valueHelp>
+                  <constraint>
+                    <validator name="numeric" argument="--range 1-2000000"/>
+                  </constraint>
+                </properties>
+              </leafNode>
+              <leafNode name="ssl-bind-ciphers">
+                <properties>
+                  <help>Cipher algorithms ("cipher suite") used during SSL/TLS handshake for all frontend servers</help>
+                  <completionHelp>
+                    <list>ecdhe-ecdsa-aes128-gcm-sha256 ecdhe-rsa-aes128-gcm-sha256 ecdhe-ecdsa-aes256-gcm-sha384 ecdhe-rsa-aes256-gcm-sha384 ecdhe-ecdsa-chacha20-poly1305 ecdhe-rsa-chacha20-poly1305 dhe-rsa-aes128-gcm-sha256 dhe-rsa-aes256-gcm-sha384</list>
+                  </completionHelp>
+                  <valueHelp>
+                    <format>ecdhe-ecdsa-aes128-gcm-sha256</format>
+                    <description>ecdhe-ecdsa-aes128-gcm-sha256</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>ecdhe-rsa-aes128-gcm-sha256</format>
+                    <description>ecdhe-rsa-aes128-gcm-sha256</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>ecdhe-ecdsa-aes256-gcm-sha384</format>
+                    <description>ecdhe-ecdsa-aes256-gcm-sha384</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>ecdhe-rsa-aes256-gcm-sha384</format>
+                    <description>ecdhe-rsa-aes256-gcm-sha384</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>ecdhe-ecdsa-chacha20-poly1305</format>
+                    <description>ecdhe-ecdsa-chacha20-poly1305</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>ecdhe-rsa-chacha20-poly1305</format>
+                    <description>ecdhe-rsa-chacha20-poly1305</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>dhe-rsa-aes128-gcm-sha256</format>
+                    <description>dhe-rsa-aes128-gcm-sha256</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>dhe-rsa-aes256-gcm-sha384</format>
+                    <description>dhe-rsa-aes256-gcm-sha384</description>
+                  </valueHelp>
+                  <constraint>
+                    <regex>(ecdhe-ecdsa-aes128-gcm-sha256|ecdhe-rsa-aes128-gcm-sha256|ecdhe-ecdsa-aes256-gcm-sha384|ecdhe-rsa-aes256-gcm-sha384|ecdhe-ecdsa-chacha20-poly1305|ecdhe-rsa-chacha20-poly1305|dhe-rsa-aes128-gcm-sha256|dhe-rsa-aes256-gcm-sha384)</regex>
+                  </constraint>
+                  <multi/>
+                </properties>
+                <defaultValue>ecdhe-ecdsa-aes128-gcm-sha256 ecdhe-rsa-aes128-gcm-sha256 ecdhe-ecdsa-aes256-gcm-sha384 ecdhe-rsa-aes256-gcm-sha384 ecdhe-ecdsa-chacha20-poly1305 ecdhe-rsa-chacha20-poly1305 dhe-rsa-aes128-gcm-sha256 dhe-rsa-aes256-gcm-sha384</defaultValue>
+              </leafNode>
+              <leafNode name="tls-version-min">
+                <properties>
+                  <help>Specify the minimum required TLS version</help>
+                  <completionHelp>
+                    <list>1.2 1.3</list>
+                  </completionHelp>
+                  <valueHelp>
+                    <format>1.2</format>
+                    <description>TLS v1.2</description>
+                  </valueHelp>
+                  <valueHelp>
+                    <format>1.3</format>
+                    <description>TLS v1.3</description>
+                  </valueHelp>
+                  <constraint>
+                    <regex>(1.2|1.3)</regex>
+                  </constraint>
+                </properties>
+                <defaultValue>1.3</defaultValue>
+              </leafNode>
+            </children>
+          </node>
+          #include <include/interface/vrf.xml.i>
+        </children>
+      </node>
+    </children>
+  </node>
+</interfaceDefinition>
diff --git a/interface-definitions/load-balancing_reverse-proxy.xml.in b/interface-definitions/load-balancing_reverse-proxy.xml.in
deleted file mode 100644
index 18274622c..000000000
--- a/interface-definitions/load-balancing_reverse-proxy.xml.in
+++ /dev/null
@@ -1,344 +0,0 @@
-<?xml version="1.0"?>
-<interfaceDefinition>
-  <node name="load-balancing">
-    <children>
-      <node name="reverse-proxy" owner="${vyos_conf_scripts_dir}/load-balancing_reverse-proxy.py">
-        <properties>
-          <help>Configure reverse-proxy</help>
-          <priority>900</priority>
-        </properties>
-        <children>
-          <tagNode name="service">
-            <properties>
-              <help>Frontend service name</help>
-              <constraint>
-                #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i>
-              </constraint>
-              <constraintErrorMessage>Server name must be alphanumeric and can contain hyphen and underscores</constraintErrorMessage>
-            </properties>
-            <children>
-              <leafNode name="backend">
-                <properties>
-                  <help>Backend member</help>
-                  <constraint>
-                    #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i>
-                  </constraint>
-                  <constraintErrorMessage>Backend name must be alphanumeric and can contain hyphen and underscores</constraintErrorMessage>
-                  <valueHelp>
-                    <format>txt</format>
-                    <description>Name of reverse-proxy backend system</description>
-                  </valueHelp>
-                  <completionHelp>
-                    <path>load-balancing reverse-proxy backend</path>
-                  </completionHelp>
-                  <multi/>
-                </properties>
-              </leafNode>
-              #include <include/generic-description.xml.i>
-              #include <include/listen-address.xml.i>
-              #include <include/haproxy/logging.xml.i>
-              #include <include/haproxy/mode.xml.i>
-              #include <include/port-number.xml.i>
-              #include <include/haproxy/rule-frontend.xml.i>
-              #include <include/haproxy/tcp-request.xml.i>
-              #include <include/haproxy/http-response-headers.xml.i>
-              <leafNode name="redirect-http-to-https">
-                <properties>
-                  <help>Redirect HTTP to HTTPS</help>
-                  <valueless/>
-                </properties>
-              </leafNode>
-              <node name="ssl">
-                <properties>
-                  <help>SSL Certificate, SSL Key and CA</help>
-                </properties>
-                <children>
-                  #include <include/pki/certificate-multi.xml.i>
-                </children>
-              </node>
-            </children>
-          </tagNode>
-          <tagNode name="backend">
-            <properties>
-              <help>Backend server name</help>
-              <constraint>
-                #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i>
-              </constraint>
-              <constraintErrorMessage>Backend name must be alphanumeric and can contain hyphen and underscores</constraintErrorMessage>
-            </properties>
-            <children>
-              <leafNode name="balance">
-                <properties>
-                  <help>Load-balancing algorithm</help>
-                  <completionHelp>
-                    <list>source-address round-robin least-connection</list>
-                  </completionHelp>
-                  <valueHelp>
-                    <format>source-address</format>
-                    <description>Based on hash of source IP address</description>
-                  </valueHelp>
-                  <valueHelp>
-                    <format>round-robin</format>
-                    <description>Round robin</description>
-                  </valueHelp>
-                  <valueHelp>
-                    <format>least-connection</format>
-                    <description>Least connection</description>
-                  </valueHelp>
-                  <constraint>
-                    <regex>(source-address|round-robin|least-connection)</regex>
-                  </constraint>
-                </properties>
-                <defaultValue>round-robin</defaultValue>
-              </leafNode>
-              #include <include/generic-description.xml.i>
-              #include <include/haproxy/logging.xml.i>
-              #include <include/haproxy/mode.xml.i>
-              #include <include/haproxy/http-response-headers.xml.i>
-              <node name="http-check">
-                <properties>
-                  <help>HTTP check configuration</help>
-                </properties>
-                <children>
-                  <leafNode name="method">
-                    <properties>
-                      <help>HTTP method used for health check</help>
-                      <completionHelp>
-                        <list>options head get post put</list>
-                      </completionHelp>
-                      <valueHelp>
-                        <format>options|head|get|post|put</format>
-                        <description>HTTP method used for health checking</description>
-                      </valueHelp>
-                      <constraint>
-                        <regex>(options|head|get|post|put)</regex>
-                      </constraint>
-                    </properties>
-                  </leafNode>
-                  <leafNode name="uri">
-                    <properties>
-                      <help>URI used for HTTP health check (Example: '/' or '/health')</help>
-                      <constraint>
-                        <regex>^\/([^?#\s]*)(\?[^#\s]*)?$</regex>
-                      </constraint>
-                    </properties>
-                  </leafNode>
-                  <node name="expect">
-                    <properties>
-                      <help>Expected response for the health check to pass</help>
-                    </properties>
-                    <children>
-                      <leafNode name="status">
-                        <properties>
-                          <help>Expected response status code for the health check to pass</help>
-                          <valueHelp>
-                            <format>u32:200-399</format>
-                            <description>Expected response code</description>
-                          </valueHelp>
-                          <constraint>
-                            <validator name="numeric" argument="--range 200-399"/>
-                          </constraint>
-                          <constraintErrorMessage>Status code must be in range 200-399</constraintErrorMessage>
-                        </properties>
-                      </leafNode>
-                      <leafNode name="string">
-                        <properties>
-                          <help>Expected to be in response body for the health check to pass</help>
-                          <valueHelp>
-                            <format>txt</format>
-                            <description>A string expected to be in the response</description>
-                          </valueHelp>
-                        </properties>
-                      </leafNode>
-                    </children>
-                  </node>
-                </children>
-              </node>
-              <leafNode name="health-check">
-                <properties>
-                  <help>Non HTTP health check options</help>
-                  <completionHelp>
-                    <list>ldap mysql pgsql redis smtp</list>
-                  </completionHelp>
-                  <valueHelp>
-                    <format>ldap</format>
-                    <description>LDAP protocol check</description>
-                  </valueHelp>
-                  <valueHelp>
-                    <format>mysql</format>
-                    <description>MySQL protocol check</description>
-                  </valueHelp>
-                  <valueHelp>
-                    <format>pgsql</format>
-                    <description>PostgreSQL protocol check</description>
-                  </valueHelp>
-                  <valueHelp>
-                    <format>redis</format>
-                    <description>Redis protocol check</description>
-                  </valueHelp>
-                  <valueHelp>
-                    <format>smtp</format>
-                    <description>SMTP protocol check</description>
-                  </valueHelp>
-                  <constraint>
-                    <regex>(ldap|mysql|redis|pgsql|smtp)</regex>
-                  </constraint>
-                </properties>
-              </leafNode>
-              #include <include/haproxy/rule-backend.xml.i>
-              <tagNode name="server">
-                <properties>
-                  <help>Backend server name</help>
-                </properties>
-                <children>
-                  <leafNode name="address">
-                    <properties>
-                      <help>Backend server address</help>
-                      <valueHelp>
-                        <format>ipv4</format>
-                        <description>IPv4 unicast peer address</description>
-                      </valueHelp>
-                      <valueHelp>
-                        <format>ipv6</format>
-                        <description>IPv6 unicast peer address</description>
-                      </valueHelp>
-                      <constraint>
-                        <validator name="ip-address"/>
-                      </constraint>
-                    </properties>
-                  </leafNode>
-                  <leafNode name="backup">
-                    <properties>
-                      <help>Use backup server if other servers are not available</help>
-                      <valueless/>
-                    </properties>
-                  </leafNode>
-                  <leafNode name="check">
-                    <properties>
-                      <help>Active health check backend server</help>
-                      <valueless/>
-                    </properties>
-                  </leafNode>
-                  #include <include/port-number.xml.i>
-                  <leafNode name="send-proxy">
-                    <properties>
-                      <help>Send a Proxy Protocol version 1 header (text format)</help>
-                      <valueless/>
-                    </properties>
-                  </leafNode>
-                  <leafNode name="send-proxy-v2">
-                    <properties>
-                      <help>Send a Proxy Protocol version 2 header (binary format)</help>
-                      <valueless/>
-                    </properties>
-                  </leafNode>
-                </children>
-              </tagNode>
-              <node name="ssl">
-                <properties>
-                  <help>SSL Certificate, SSL Key and CA</help>
-                </properties>
-                <children>
-                  #include <include/pki/ca-certificate.xml.i>
-                  <leafNode name="no-verify">
-                    <properties>
-                      <help>Do not attempt to verify SSL certificates for backend servers</help>
-                      <valueless/>
-                    </properties>
-                  </leafNode>
-                </children>
-              </node>
-              #include <include/haproxy/timeout.xml.i>
-            </children>
-          </tagNode>
-          <node name="global-parameters">
-            <properties>
-              <help>Global perfomance parameters and limits</help>
-            </properties>
-            <children>
-              #include <include/haproxy/logging.xml.i>
-              <leafNode name="max-connections">
-                <properties>
-                  <help>Maximum allowed connections</help>
-                  <valueHelp>
-                    <format>u32:1-2000000</format>
-                    <description>Maximum allowed connections</description>
-                  </valueHelp>
-                  <constraint>
-                    <validator name="numeric" argument="--range 1-2000000"/>
-                  </constraint>
-                </properties>
-              </leafNode>
-              <leafNode name="ssl-bind-ciphers">
-                <properties>
-                  <help>Cipher algorithms ("cipher suite") used during SSL/TLS handshake for all frontend servers</help>
-                  <completionHelp>
-                    <list>ecdhe-ecdsa-aes128-gcm-sha256 ecdhe-rsa-aes128-gcm-sha256 ecdhe-ecdsa-aes256-gcm-sha384 ecdhe-rsa-aes256-gcm-sha384 ecdhe-ecdsa-chacha20-poly1305 ecdhe-rsa-chacha20-poly1305 dhe-rsa-aes128-gcm-sha256 dhe-rsa-aes256-gcm-sha384</list>
-                  </completionHelp>
-                  <valueHelp>
-                    <format>ecdhe-ecdsa-aes128-gcm-sha256</format>
-                    <description>ecdhe-ecdsa-aes128-gcm-sha256</description>
-                  </valueHelp>
-                  <valueHelp>
-                    <format>ecdhe-rsa-aes128-gcm-sha256</format>
-                    <description>ecdhe-rsa-aes128-gcm-sha256</description>
-                  </valueHelp>
-                  <valueHelp>
-                    <format>ecdhe-ecdsa-aes256-gcm-sha384</format>
-                    <description>ecdhe-ecdsa-aes256-gcm-sha384</description>
-                  </valueHelp>
-                  <valueHelp>
-                    <format>ecdhe-rsa-aes256-gcm-sha384</format>
-                    <description>ecdhe-rsa-aes256-gcm-sha384</description>
-                  </valueHelp>
-                  <valueHelp>
-                    <format>ecdhe-ecdsa-chacha20-poly1305</format>
-                    <description>ecdhe-ecdsa-chacha20-poly1305</description>
-                  </valueHelp>
-                  <valueHelp>
-                    <format>ecdhe-rsa-chacha20-poly1305</format>
-                    <description>ecdhe-rsa-chacha20-poly1305</description>
-                  </valueHelp>
-                  <valueHelp>
-                    <format>dhe-rsa-aes128-gcm-sha256</format>
-                    <description>dhe-rsa-aes128-gcm-sha256</description>
-                  </valueHelp>
-                  <valueHelp>
-                    <format>dhe-rsa-aes256-gcm-sha384</format>
-                    <description>dhe-rsa-aes256-gcm-sha384</description>
-                  </valueHelp>
-                  <constraint>
-                    <regex>(ecdhe-ecdsa-aes128-gcm-sha256|ecdhe-rsa-aes128-gcm-sha256|ecdhe-ecdsa-aes256-gcm-sha384|ecdhe-rsa-aes256-gcm-sha384|ecdhe-ecdsa-chacha20-poly1305|ecdhe-rsa-chacha20-poly1305|dhe-rsa-aes128-gcm-sha256|dhe-rsa-aes256-gcm-sha384)</regex>
-                  </constraint>
-                  <multi/>
-                </properties>
-                <defaultValue>ecdhe-ecdsa-aes128-gcm-sha256 ecdhe-rsa-aes128-gcm-sha256 ecdhe-ecdsa-aes256-gcm-sha384 ecdhe-rsa-aes256-gcm-sha384 ecdhe-ecdsa-chacha20-poly1305 ecdhe-rsa-chacha20-poly1305 dhe-rsa-aes128-gcm-sha256 dhe-rsa-aes256-gcm-sha384</defaultValue>
-              </leafNode>
-              <leafNode name="tls-version-min">
-                <properties>
-                  <help>Specify the minimum required TLS version</help>
-                  <completionHelp>
-                    <list>1.2 1.3</list>
-                  </completionHelp>
-                  <valueHelp>
-                    <format>1.2</format>
-                    <description>TLS v1.2</description>
-                  </valueHelp>
-                  <valueHelp>
-                    <format>1.3</format>
-                    <description>TLS v1.3</description>
-                  </valueHelp>
-                  <constraint>
-                    <regex>(1.2|1.3)</regex>
-                  </constraint>
-                </properties>
-                <defaultValue>1.3</defaultValue>
-              </leafNode>
-            </children>
-          </node>
-          #include <include/interface/vrf.xml.i>
-        </children>
-      </node>
-    </children>
-  </node>
-</interfaceDefinition>
diff --git a/op-mode-definitions/load-balacing_haproxy.in b/op-mode-definitions/load-balacing_haproxy.in
new file mode 100644
index 000000000..c3d6c799b
--- /dev/null
+++ b/op-mode-definitions/load-balacing_haproxy.in
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+  <node name="restart">
+    <children>
+      <node name="haproxy">
+        <properties>
+          <help>Restart haproxy service</help>
+        </properties>
+        <command>sudo ${vyos_op_scripts_dir}/restart.py restart_service --name haproxy</command>
+      </node>
+    </children>
+  </node>
+  <node name="show">
+    <children>
+      <node name="haproxy">
+        <properties>
+          <help>Show load-balancing haproxy</help>
+        </properties>
+        <command>sudo ${vyos_op_scripts_dir}/load-balacing_haproxy.py show</command>
+      </node>
+    </children>
+  </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/reverse-proxy.xml.in b/op-mode-definitions/reverse-proxy.xml.in
deleted file mode 100644
index b45ce107f..000000000
--- a/op-mode-definitions/reverse-proxy.xml.in
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0"?>
-<interfaceDefinition>
-  <node name="restart">
-    <children>
-      <node name="reverse-proxy">
-        <properties>
-          <help>Restart reverse-proxy service</help>
-        </properties>
-        <command>sudo ${vyos_op_scripts_dir}/restart.py restart_service --name reverse_proxy</command>
-      </node>
-    </children>
-  </node>
-  <node name="show">
-    <children>
-      <node name="reverse-proxy">
-        <properties>
-          <help>Show load-balancing reverse-proxy</help>
-        </properties>
-        <command>sudo ${vyos_op_scripts_dir}/reverseproxy.py show</command>
-      </node>
-    </children>
-  </node>
-</interfaceDefinition>
diff --git a/smoketest/scripts/cli/test_load-balancing_haproxy.py b/smoketest/scripts/cli/test_load-balancing_haproxy.py
new file mode 100755
index 000000000..967eb3869
--- /dev/null
+++ b/smoketest/scripts/cli/test_load-balancing_haproxy.py
@@ -0,0 +1,502 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+
+PROCESS_NAME = 'haproxy'
+HAPROXY_CONF = '/run/haproxy/haproxy.cfg'
+base_path = ['load-balancing', 'haproxy']
+proxy_interface = 'eth1'
+
+valid_ca_cert = """
+MIIDnTCCAoWgAwIBAgIUewSDtLiZbhg1YEslMnqRl1shoPcwDQYJKoZIhvcNAQEL
+BQAwVzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcM
+CVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzEQMA4GA1UEAwwHdnlvcy5pbzAeFw0y
+NDA0MDEwNTQ3MzJaFw0yOTAzMzEwNTQ3MzJaMFcxCzAJBgNVBAYTAkdCMRMwEQYD
+VQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoMBFZ5
+T1MxEDAOBgNVBAMMB3Z5b3MuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQC/D6W27rfpdPIf16JHs8fx/7VehyCk8m03dPAQqv6wQiHF5xhXaFZER1+c
+nf7oExp9zi/4HJ/KRbcc1loVArXtV0zwAUftBmUeezGVfxhCHKhP89GnV4NB97jj
+klHFSxjEoT/0YvJQ1IV/3Cos1T5O8x14WIi31l7WQGYAyWxUXiP8QxGVmF3odEJo
+O3e7Ew9HFkamvuL6Z6c4uAVMM7uYXme7q0OM49Wu7C9hj39ZKbjG5FFKZTj+zDKg
+SbOiQaFk3blOky/e3ifNjZelGtussYPOMBkUirLvrSGGy7s3lm8Yp5PH5+UkVQB2
+rZyxRdZTC9kh+dShR1s/qcPnDw7lAgMBAAGjYTBfMA8GA1UdEwEB/wQFMAMBAf8w
+DgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAd
+BgNVHQ4EFgQU/HE2UPn8JQB/9EL52GquPxZqr5MwDQYJKoZIhvcNAQELBQADggEB
+AIkMmqyoMqidTa3lvUPJNl4H+Ef/yPQkTkrsOd3WL8DQysyUdMLdQozr3K1bH5XB
+wRxoXX211nu4WhN18LsFJRCuHBSxmaNkBGFyl+JNvhPUSI6j0somNMCS75KJ0ZDx
+2HZsXmmJFF902VQxCR7vCIrFDrKDYq1e7GQbFS8t46FlpqivQMQWNPt18Bthj/1Y
+lO2GKRWFCX8VlOW7FtDQ6B3oC1oAGHBBGogAx7/0gh9DnYBKT14V/kuWW3RNABZJ
+ewHO1C6icQdnjtaREDyTP4oyL+uyAfXrFfbpti2hc00f8oYPQZYxj1yxl4UAdNij
+mS6YqH/WRioGMe3tBVeSdoo=
+"""
+
+valid_ca_private_key = """
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC/D6W27rfpdPIf
+16JHs8fx/7VehyCk8m03dPAQqv6wQiHF5xhXaFZER1+cnf7oExp9zi/4HJ/KRbcc
+1loVArXtV0zwAUftBmUeezGVfxhCHKhP89GnV4NB97jjklHFSxjEoT/0YvJQ1IV/
+3Cos1T5O8x14WIi31l7WQGYAyWxUXiP8QxGVmF3odEJoO3e7Ew9HFkamvuL6Z6c4
+uAVMM7uYXme7q0OM49Wu7C9hj39ZKbjG5FFKZTj+zDKgSbOiQaFk3blOky/e3ifN
+jZelGtussYPOMBkUirLvrSGGy7s3lm8Yp5PH5+UkVQB2rZyxRdZTC9kh+dShR1s/
+qcPnDw7lAgMBAAECggEAGm+j0kf9koPn7Jf9kEZD6CwlgEraLXiNvBqmDOhcDS9Z
+VPTA3XdGWHQ3uofx+VKLW9TntkDfqzEyQP83v6h8W7a0opDKzvUPkMQi/Dh1ttAY
+SdfGrozhUINiRbq9LbtSVgKpwrreJGkDf8mK3GE1Gd9xuHEnmahDvwlyE7HLF3Eh
+2xJDSAPx3OxcjR5hW7vbojhVCyCfuYTlZB86f0Sb8SqxZMt/y2zKmbzoTqpUBWbg
+lBnE7GJoNR07DWjxvEP8r6kQMh670I01SUR42CSK8X8asHhhZHUcggsNno+BBc6K
+sy4HzDIYIay6oy0atcVzKsGrlNCveeAiSEcw7x2yAQKBgQDsXz2FbhXYV5Vbt4wU
+5EWOa7if/+FG+TcVezOF3xlNBgykjXHQaYTYHrJq0qsEFrNT3ZGm9ezY4LdF3BTt
+5z/+i8QlCCw/nr3N7JZx6U5+OJl1j3NLFoFx3+DXo31pgJJEQCHHwdCkF5IuOcZ/
+b3nXkRZ80BVv7XD6F9bMHEwLYQKBgQDO7THcRDbsE6/+7VsTDf0P/JENba3DBBu1
+gjb1ItL5FHJwMgnkUadRZRo0QKye848ugribed39qSoJfNaBJrAT5T8S/9q+lXft
+vXUckcBO1CKNaP9gqF5fPIdNHf64GbmCiiHjOTE3rwJjkxJPpzLXyvgBO4aLeesK
+ThBdW+iWBQKBgD3crz08knsMcQqP/xl4pLuhdbBqR4tLrh7xH4rp2LVP3/8xBZiG
+BT6Kyicq+5cWWdiZJIWN127rYQvnjZK18wmriqomeW4tHX/Ha5hkdyaRqZga8xGz
+0iz7at0E7M2v2JgEMNMW5oQLpzZx6IFxq3G/hyMjUnj4q5jIpG7G+SABAoGBAKgT
+8Ika+4WcpDssrup2VVTT8Tp4GUkroBo6D8vkInvhiObrLi+/x2mM9tD0q4JdEbNU
+yQC454EwFA4q0c2MED/I2QfkvNhLbmO0nVi8ZvlgxEQawjzP5f/zmW8haxI9Cvsm
+mkoH3Zt+UzFwd9ItXFX97p6JrErEmA8Bw7chfXXFAoGACWR/c+s7hnX6gzyah3N1
+Db0xAaS6M9fzogcg2OM1i/6OCOcp4Sh1fmPG7tN45CCnFkhgVoRkSSA5MJAe2I/r
+xFm72VX7567T+4qIFua2iDxIBA/Z4zmj+RYfhHGPYZjdSjprKJxY6QOv5aoluBvE
+mlLy1Hmcry+ukWZtWezZfGY=
+"""
+
+valid_cert = """
+MIIDsTCCApmgAwIBAgIUDKOfYIwwtjww0vAMvJnXnGLhL+0wDQYJKoZIhvcNAQEL
+BQAwVzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcM
+CVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzEQMA4GA1UEAwwHdnlvcy5pbzAeFw0y
+NDA0MDEwNTQ5NTdaFw0yNTA0MDEwNTQ5NTdaMFcxCzAJBgNVBAYTAkdCMRMwEQYD
+VQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoMBFZ5
+T1MxEDAOBgNVBAMMB3Z5b3MuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQCHtW25Umt6rqm2gfzqAZg1/VsqefZwAqIUAm2T3VwHQZ/2tNdr8ROWASii
+W5PToC7N8StMwFl2YoIof+MXGMO00toTTJePZOJKjF9U9hL3kuYuY1+yng4fl+E0
+96xVobb2KY4lMZ2rVwmpB7jkNO2LWxbJ6vHKcwMOhlx/8NEKIoVmkBT1Zkgy5dgn
+PgTtJcdVIU75XhQWqBmAUsMmACuZfqSYJbAv3hHz5V+Ejt0dI6mlGM7TXsCC9tKM
+64paIKZooFm78IsxJ26jHpZ8eh+SDBz0VBydBFWXm8VhOJ8NlZ1opAh3AWxFZDGt
+49uOsy82VmUcHPyoZ8DKYkBFHfSpAgMBAAGjdTBzMAwGA1UdEwEB/wQCMAAwDgYD
+VR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0GA1UdDgQWBBTeTcgM
+pRxAMjVBirjzo2QUu5H5fzAfBgNVHSMEGDAWgBT8cTZQ+fwlAH/0QvnYaq4/Fmqv
+kzANBgkqhkiG9w0BAQsFAAOCAQEAi4dBcH7TIYwWRW6bWRubMA7ztonV4EYb15Zf
+9yNafMWAEEBOii/DFo+j/ky9oInl7ZHw7gTIyXfLEarX/bM6fHOgiyj4zp3u6RnH
+5qlBypu/YCnyPjE/GvV05m2rrXnxZ4rCtcoO4u/HyGbV+jGnCmjShKICKyu1FdMd
+eeZRrLKPO/yghadGH34WVQnrbaorwlbi+NjB6fxmZQx5HE/SyK/9sb6WCpLMGHoy
+MpdQo3lV1ewtL3ElIWDq6mO030Mo5pwpjIU+8yHHNBVzg6mlGVgQPAp0gbUei9aP
+CJ8SLmMEi3NDk0E/sPgVC17e6bf2bx2nRuXROZekG2dd90Iu8g==
+"""
+
+valid_cert_private_key = """
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCHtW25Umt6rqm2
+gfzqAZg1/VsqefZwAqIUAm2T3VwHQZ/2tNdr8ROWASiiW5PToC7N8StMwFl2YoIo
+f+MXGMO00toTTJePZOJKjF9U9hL3kuYuY1+yng4fl+E096xVobb2KY4lMZ2rVwmp
+B7jkNO2LWxbJ6vHKcwMOhlx/8NEKIoVmkBT1Zkgy5dgnPgTtJcdVIU75XhQWqBmA
+UsMmACuZfqSYJbAv3hHz5V+Ejt0dI6mlGM7TXsCC9tKM64paIKZooFm78IsxJ26j
+HpZ8eh+SDBz0VBydBFWXm8VhOJ8NlZ1opAh3AWxFZDGt49uOsy82VmUcHPyoZ8DK
+YkBFHfSpAgMBAAECggEABofhw0W/ACEMcAjmpNTFkFCUXPGQXWDVD7EzuIZSNdOv
+yOm4Rbys6H6/B7wwO6KVagoBf1Cw5Xh1YtFPuoZxsZ+liMD6eLc+SB/j/RTYAhPO
+0bvsyK3gSF8w4nGKWLce9M74ZRwThkG6qGijmlDdPyP3r2kn8GoTQzVOWYZbavk/
+H3uE6PsZSWjOY+Mnm3vEmeItPYKGZ5+IP+YiTqZ4NCggBwH7csnR3/kbwY5Ns7jl
+3Av+EAdIeUwDNeMfLTzN7GphJR7gL6YQIhGKxE+W0GHXL2FubnnrFx8G75HFh1ay
+GkJXEqY5Lbd+7VPS0KcQdwhMSSoJsY5GUORUqrU80QKBgQC/0wJSu+Gfe7dONIby
+mnGRppSRIQVRjCjbVIN+Y2h1Kp3aK0qDpV7KFLCiUUtz9rWHR/NB4cDaIW543T55
+/jXUMD2j3EqtbtlsVQfDLQV7DyDrMmBAs4REHmyZmWTzHjCDUO79ahdOlZs34Alz
+wfpX3L3WVYGIAJKZtsUZ8FbrGQKBgQC1HFgVZ1PqP9/pW50RMh06BbQrhWPGiWgH
+Rn5bFthLkp3uqr9bReBq9tu3sqJuAhFudH68wup+Z+fTcHAcNg2Rs+Q+IKnULdB/
+UQHYoPjeWOvHAuOmgn9iD9OD7GCIv8fZmLit09vAsOWq+NKNBKCknGM70CDrvAlQ
+lOAUa34YEQKBgQC5i8GThWiYe3Kzktt1jy6LVDYgq3AZkRl0Diui9UT1EGPfxEAv
+VqZ5kcnJOBlj8h9k25PRBi0k0XGqN1dXaS1oMcFt3ofdenuU7iqz/7htcBTHa9Lu
+wrYNreAeMuISyADlBEQnm5cvzEZ3pZ1++wLMOhjmWY8Rnnwvczrz/CYXAQKBgH+t
+vcNJFvWblkUzWuWWiNgw0TWlUhPTJs2KOuYIku+kK0bohQLZnj6KTZeRjcU0HAnc
+gsScPShkJCEBsWeSC7reMVhDOrbknYpEF6MayJgn5ABm3wqyEQ+WzKzCZcPCQCf8
+7KVPKCsOCrufsv/LdVzXC3ZNYggOhhqS+e4rYbehAoGBAIsq252o3vgrunzS5FZx
+IONA2FvYrxVbDn5aF8WfNSdKFy3CAlt0P+Fm8gYbrKylIfMXpL8Oqc9RJou5onZP
+ZXLrtgVJR9W020qTurO2f91qfU8646n11hR9ObBB1IYbagOU0Pw1Nrq/FRp/u2tx
+7i7xFz2WEiQeSCPaKYOiqM3t
+"""
+
+
+class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
+    def tearDown(self):
+        # Check for running process
+        self.assertTrue(process_named_running(PROCESS_NAME))
+
+        self.cli_delete(['interfaces', 'ethernet', proxy_interface, 'address'])
+        self.cli_delete(base_path)
+        self.cli_delete(['pki'])
+        self.cli_commit()
+
+        # Process must be terminated after deleting the config
+        self.assertFalse(process_named_running(PROCESS_NAME))
+
+    def base_config(self):
+        self.cli_set(base_path + ['service', 'https_front', 'mode', 'http'])
+        self.cli_set(base_path + ['service', 'https_front', 'port', '4433'])
+        self.cli_set(base_path + ['service', 'https_front', 'backend', 'bk-01'])
+
+        self.cli_set(base_path + ['backend', 'bk-01', 'mode', 'http'])
+        self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'address', '192.0.2.11'])
+        self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'port', '9090'])
+        self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'send-proxy'])
+
+        self.cli_set(base_path + ['global-parameters', 'max-connections', '1000'])
+
+    def configure_pki(self):
+
+        # Valid CA
+        self.cli_set(['pki', 'ca', 'smoketest', 'certificate', valid_ca_cert.replace('\n','')])
+        self.cli_set(['pki', 'ca', 'smoketest', 'private', 'key', valid_ca_private_key.replace('\n','')])
+
+        # Valid cert
+        self.cli_set(['pki', 'certificate', 'smoketest', 'certificate', valid_cert.replace('\n','')])
+        self.cli_set(['pki', 'certificate', 'smoketest', 'private', 'key', valid_cert_private_key.replace('\n','')])
+
+    def test_01_lb_reverse_proxy_domain(self):
+        domains_bk_first = ['n1.example.com', 'n2.example.com', 'n3.example.com']
+        domain_bk_second = 'n5.example.com'
+        frontend = 'https_front'
+        front_port = '4433'
+        bk_server_first = '192.0.2.11'
+        bk_server_second = '192.0.2.12'
+        bk_first_name = 'bk-01'
+        bk_second_name = 'bk-02'
+        bk_server_port = '9090'
+        mode = 'http'
+        rule_ten = '10'
+        rule_twenty = '20'
+        rule_thirty = '30'
+        send_proxy = 'send-proxy'
+        max_connections = '1000'
+
+        back_base = base_path + ['backend']
+
+        self.cli_set(base_path + ['service', frontend, 'mode', mode])
+        self.cli_set(base_path + ['service', frontend, 'port', front_port])
+        for domain in domains_bk_first:
+            self.cli_set(base_path + ['service', frontend, 'rule', rule_ten, 'domain-name', domain])
+        self.cli_set(base_path + ['service', frontend, 'rule', rule_ten, 'set', 'backend', bk_first_name])
+        self.cli_set(base_path + ['service', frontend, 'rule', rule_twenty, 'domain-name', domain_bk_second])
+        self.cli_set(base_path + ['service', frontend, 'rule', rule_twenty, 'set', 'backend', bk_second_name])
+        self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'url-path', 'end', '/test'])
+        self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'set', 'backend', bk_second_name])
+
+        self.cli_set(back_base + [bk_first_name, 'mode', mode])
+        self.cli_set(back_base + [bk_first_name, 'server', bk_first_name, 'address', bk_server_first])
+        self.cli_set(back_base + [bk_first_name, 'server', bk_first_name, 'port', bk_server_port])
+        self.cli_set(back_base + [bk_first_name, 'server', bk_first_name, send_proxy])
+
+        self.cli_set(back_base + [bk_second_name, 'mode', mode])
+        self.cli_set(back_base + [bk_second_name, 'server', bk_second_name, 'address', bk_server_second])
+        self.cli_set(back_base + [bk_second_name, 'server', bk_second_name, 'port', bk_server_port])
+        self.cli_set(back_base + [bk_second_name, 'server', bk_second_name, 'backup'])
+
+        self.cli_set(base_path + ['global-parameters', 'max-connections', max_connections])
+
+        # commit changes
+        self.cli_commit()
+
+        config = read_file(HAPROXY_CONF)
+
+        # Global
+        self.assertIn(f'maxconn {max_connections}', config)
+
+        # Frontend
+        self.assertIn(f'frontend {frontend}', config)
+        self.assertIn(f'bind [::]:{front_port} v4v6', config)
+        self.assertIn(f'mode {mode}', config)
+        for domain in domains_bk_first:
+            self.assertIn(f'acl {rule_ten} hdr(host) -i {domain}', config)
+        self.assertIn(f'use_backend {bk_first_name} if {rule_ten}', config)
+        self.assertIn(f'acl {rule_twenty} hdr(host) -i {domain_bk_second}', config)
+        self.assertIn(f'use_backend {bk_second_name} if {rule_twenty}', config)
+        self.assertIn(f'acl {rule_thirty} path -i -m end /test', config)
+        self.assertIn(f'use_backend {bk_second_name} if {rule_thirty}', config)
+
+        # Backend
+        self.assertIn(f'backend {bk_first_name}', config)
+        self.assertIn(f'balance roundrobin', config)
+        self.assertIn(f'option forwardfor', config)
+        self.assertIn('http-request add-header X-Forwarded-Proto https if { ssl_fc }', config)
+        self.assertIn(f'mode {mode}', config)
+        self.assertIn(f'server {bk_first_name} {bk_server_first}:{bk_server_port} send-proxy', config)
+
+        self.assertIn(f'backend {bk_second_name}', config)
+        self.assertIn(f'mode {mode}', config)
+        self.assertIn(f'server {bk_second_name} {bk_server_second}:{bk_server_port}', config)
+        self.assertIn(f'server {bk_second_name} {bk_server_second}:{bk_server_port} backup', config)
+
+    def test_02_lb_reverse_proxy_cert_not_exists(self):
+        self.base_config()
+        self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert'])
+
+        with self.assertRaises(ConfigSessionError) as e:
+            self.cli_commit()
+        # self.assertIn('\nCertificates does not exist in PKI\n', str(e.exception))
+
+        self.cli_delete(base_path)
+        self.configure_pki()
+
+        self.base_config()
+        self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert'])
+
+        with self.assertRaises(ConfigSessionError) as e:
+            self.cli_commit()
+        # self.assertIn('\nCertificate "cert" does not exist\n', str(e.exception))
+
+        self.cli_delete(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert'])
+        self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'smoketest'])
+        self.cli_commit()
+
+    def test_03_lb_reverse_proxy_ca_not_exists(self):
+        self.base_config()
+        self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test'])
+
+        with self.assertRaises(ConfigSessionError) as e:
+            self.cli_commit()
+        # self.assertIn('\nCA certificates does not exist in PKI\n', str(e.exception))
+
+        self.cli_delete(base_path)
+        self.configure_pki()
+
+        self.base_config()
+        self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test'])
+
+        with self.assertRaises(ConfigSessionError) as e:
+            self.cli_commit()
+        # self.assertIn('\nCA certificate "ca-test" does not exist\n', str(e.exception))
+
+        self.cli_delete(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test'])
+        self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'smoketest'])
+        self.cli_commit()
+
+    def test_04_lb_reverse_proxy_backend_ssl_no_verify(self):
+        # Setup base
+        self.configure_pki()
+        self.base_config()
+
+        # Set no-verify option
+        self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'no-verify'])
+        self.cli_commit()
+
+        # Test no-verify option
+        config = read_file(HAPROXY_CONF)
+        self.assertIn('server bk-01 192.0.2.11:9090 send-proxy ssl verify none', config)
+
+        # Test setting ca-certificate alongside no-verify option fails, to test config validation
+        self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'smoketest'])
+        with self.assertRaises(ConfigSessionError) as e:
+            self.cli_commit()
+
+    def test_05_lb_reverse_proxy_backend_http_check(self):
+        # Setup base
+        self.base_config()
+
+        # Set http-check
+        self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'method', 'get'])
+        self.cli_commit()
+
+        # Test http-check
+        config = read_file(HAPROXY_CONF)
+        self.assertIn('option httpchk', config)
+        self.assertIn('http-check send meth GET', config)
+
+        # Set http-check with uri and status
+        self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'uri', '/health'])
+        self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'status', '200'])
+        self.cli_commit()
+
+        # Test http-check with uri and status
+        config = read_file(HAPROXY_CONF)
+        self.assertIn('option httpchk', config)
+        self.assertIn('http-check send meth GET uri /health', config)
+        self.assertIn('http-check expect status 200', config)
+
+        # Set http-check with string
+        self.cli_delete(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'status', '200'])
+        self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'string', 'success'])
+        self.cli_commit()
+
+        # Test http-check with string
+        config = read_file(HAPROXY_CONF)
+        self.assertIn('option httpchk', config)
+        self.assertIn('http-check send meth GET uri /health', config)
+        self.assertIn('http-check expect string success', config)
+
+        # Test configuring both http-check & health-check fails validation script
+        self.cli_set(base_path + ['backend', 'bk-01', 'health-check', 'ldap'])
+        with self.assertRaises(ConfigSessionError) as e:
+            self.cli_commit()
+
+    def test_06_lb_reverse_proxy_tcp_mode(self):
+        frontend = 'tcp_8443'
+        mode = 'tcp'
+        front_port = '8433'
+        tcp_request_delay = "5000"
+        rule_thirty = '30'
+        domain_bk = 'n6.example.com'
+        ssl_opt = "req-ssl-sni"
+        bk_name = 'bk-03'
+        bk_server = '192.0.2.11'
+        bk_server_port = '9090'
+
+        back_base = base_path + ['backend']
+
+        self.cli_set(base_path + ['service', frontend, 'mode', mode])
+        self.cli_set(base_path + ['service', frontend, 'port', front_port])
+        self.cli_set(base_path + ['service', frontend, 'tcp-request', 'inspect-delay', tcp_request_delay])
+
+        self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'domain-name', domain_bk])
+        self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'ssl', ssl_opt])
+        self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'set', 'backend', bk_name])
+
+        self.cli_set(back_base + [bk_name, 'mode', mode])
+        self.cli_set(back_base + [bk_name, 'server', bk_name, 'address', bk_server])
+        self.cli_set(back_base + [bk_name, 'server', bk_name, 'port', bk_server_port])
+
+        # commit changes
+        self.cli_commit()
+
+        config = read_file(HAPROXY_CONF)
+
+        # Frontend
+        self.assertIn(f'frontend {frontend}', config)
+        self.assertIn(f'bind [::]:{front_port} v4v6', config)
+        self.assertIn(f'mode {mode}', config)
+
+        self.assertIn(f'tcp-request inspect-delay {tcp_request_delay}', config)
+        self.assertIn(f"tcp-request content accept if {{ req_ssl_hello_type 1 }}", config)
+        self.assertIn(f'acl {rule_thirty} req_ssl_sni -i {domain_bk}', config)
+        self.assertIn(f'use_backend {bk_name} if {rule_thirty}', config)
+
+        # Backend
+        self.assertIn(f'backend {bk_name}', config)
+        self.assertIn(f'balance roundrobin', config)
+        self.assertIn(f'mode {mode}', config)
+        self.assertIn(f'server {bk_name} {bk_server}:{bk_server_port}', config)
+
+    def test_07_lb_reverse_proxy_http_response_headers(self):
+        # Setup base
+        self.configure_pki()
+        self.base_config()
+
+        # Set example headers in both frontend and backend
+        self.cli_set(base_path + ['service', 'https_front', 'http-response-headers', 'Cache-Control', 'value', 'max-age=604800'])
+        self.cli_set(base_path + ['backend', 'bk-01',  'http-response-headers', 'Proxy-Backend-ID', 'value', 'bk-01'])
+        self.cli_commit()
+
+        # Test headers are present in generated configuration file
+        config = read_file(HAPROXY_CONF)
+        self.assertIn('http-response set-header Cache-Control \'max-age=604800\'', config)
+        self.assertIn('http-response set-header Proxy-Backend-ID \'bk-01\'', config)
+
+        # Test setting alongside modes other than http is blocked by validation conditions
+        self.cli_set(base_path + ['service', 'https_front', 'mode', 'tcp'])
+        with self.assertRaises(ConfigSessionError) as e:
+            self.cli_commit()
+
+    def test_08_lb_reverse_proxy_tcp_health_checks(self):
+        # Setup PKI
+        self.configure_pki()
+
+        # Define variables
+        frontend = 'fe_ldaps'
+        mode = 'tcp'
+        health_check = 'ldap'
+        front_port = '636'
+        bk_name = 'bk_ldap'
+        bk_servers = ['192.0.2.11', '192.0.2.12']
+        bk_server_port = '389'
+
+        # Configure frontend
+        self.cli_set(base_path + ['service', frontend, 'mode', mode])
+        self.cli_set(base_path + ['service', frontend, 'port', front_port])
+        self.cli_set(base_path + ['service', frontend, 'ssl', 'certificate', 'smoketest'])
+
+        # Configure backend
+        self.cli_set(base_path + ['backend', bk_name, 'mode', mode])
+        self.cli_set(base_path + ['backend', bk_name, 'health-check', health_check])
+        for index, bk_server in enumerate(bk_servers):
+            self.cli_set(base_path + ['backend', bk_name, 'server', f'srv-{index}', 'address', bk_server])
+            self.cli_set(base_path + ['backend', bk_name, 'server', f'srv-{index}', 'port', bk_server_port])
+
+        # Commit & read config
+        self.cli_commit()
+        config = read_file(HAPROXY_CONF)
+
+        # Validate Frontend
+        self.assertIn(f'frontend {frontend}', config)
+        self.assertIn(f'bind [::]:{front_port} v4v6 ssl crt /run/haproxy/smoketest.pem', config)
+        self.assertIn(f'mode {mode}', config)
+        self.assertIn(f'backend {bk_name}', config)
+
+        # Validate Backend
+        self.assertIn(f'backend {bk_name}', config)
+        self.assertIn(f'option {health_check}-check', config)
+        self.assertIn(f'mode {mode}', config)
+        for index, bk_server in enumerate(bk_servers):
+            self.assertIn(f'server srv-{index} {bk_server}:{bk_server_port}', config)
+
+        # Validate SMTP option renders correctly
+        self.cli_set(base_path + ['backend', bk_name, 'health-check', 'smtp'])
+        self.cli_commit()
+        config = read_file(HAPROXY_CONF)
+        self.assertIn(f'option smtpchk', config)
+
+    def test_09_lb_reverse_proxy_logging(self):
+        # Setup base
+        self.base_config()
+        self.cli_commit()
+
+        # Ensure default logging configuration is present
+        config = read_file(HAPROXY_CONF)
+
+        # Test global-parameters logging options
+        self.cli_set(base_path + ['global-parameters', 'logging', 'facility', 'local1', 'level', 'err'])
+        self.cli_set(base_path + ['global-parameters', 'logging', 'facility', 'local2', 'level', 'warning'])
+        self.cli_commit()
+
+        # Test global logging parameters are generated in configuration file
+        config = read_file(HAPROXY_CONF)
+        self.assertIn('log /dev/log local1 err', config)
+        self.assertIn('log /dev/log local2 warning', config)
+
+        # Test backend logging options
+        backend_path = base_path + ['backend', 'bk-01']
+        self.cli_set(backend_path + ['logging', 'facility', 'local3', 'level', 'debug'])
+        self.cli_set(backend_path + ['logging', 'facility', 'local4', 'level', 'info'])
+        self.cli_commit()
+
+        # Test backend logging parameters are generated in configuration file
+        config = read_file(HAPROXY_CONF)
+        self.assertIn('log /dev/log local3 debug', config)
+        self.assertIn('log /dev/log local4 info', config)
+
+        # Test service logging options
+        service_path = base_path + ['service', 'https_front']
+        self.cli_set(service_path + ['logging', 'facility', 'local5', 'level', 'notice'])
+        self.cli_set(service_path + ['logging', 'facility', 'local6', 'level', 'crit'])
+        self.cli_commit()
+
+        # Test service logging parameters are generated in configuration file
+        config = read_file(HAPROXY_CONF)
+        self.assertIn('log /dev/log local5 notice', config)
+        self.assertIn('log /dev/log local6 crit', config)
+
+if __name__ == '__main__':
+    unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py b/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py
deleted file mode 100755
index 34f77b95d..000000000
--- a/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py
+++ /dev/null
@@ -1,502 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2023 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import unittest
-
-from base_vyostest_shim import VyOSUnitTestSHIM
-
-from vyos.configsession import ConfigSessionError
-from vyos.utils.process import process_named_running
-from vyos.utils.file import read_file
-
-PROCESS_NAME = 'haproxy'
-HAPROXY_CONF = '/run/haproxy/haproxy.cfg'
-base_path = ['load-balancing', 'reverse-proxy']
-proxy_interface = 'eth1'
-
-valid_ca_cert = """
-MIIDnTCCAoWgAwIBAgIUewSDtLiZbhg1YEslMnqRl1shoPcwDQYJKoZIhvcNAQEL
-BQAwVzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcM
-CVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzEQMA4GA1UEAwwHdnlvcy5pbzAeFw0y
-NDA0MDEwNTQ3MzJaFw0yOTAzMzEwNTQ3MzJaMFcxCzAJBgNVBAYTAkdCMRMwEQYD
-VQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoMBFZ5
-T1MxEDAOBgNVBAMMB3Z5b3MuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
-AoIBAQC/D6W27rfpdPIf16JHs8fx/7VehyCk8m03dPAQqv6wQiHF5xhXaFZER1+c
-nf7oExp9zi/4HJ/KRbcc1loVArXtV0zwAUftBmUeezGVfxhCHKhP89GnV4NB97jj
-klHFSxjEoT/0YvJQ1IV/3Cos1T5O8x14WIi31l7WQGYAyWxUXiP8QxGVmF3odEJo
-O3e7Ew9HFkamvuL6Z6c4uAVMM7uYXme7q0OM49Wu7C9hj39ZKbjG5FFKZTj+zDKg
-SbOiQaFk3blOky/e3ifNjZelGtussYPOMBkUirLvrSGGy7s3lm8Yp5PH5+UkVQB2
-rZyxRdZTC9kh+dShR1s/qcPnDw7lAgMBAAGjYTBfMA8GA1UdEwEB/wQFMAMBAf8w
-DgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAd
-BgNVHQ4EFgQU/HE2UPn8JQB/9EL52GquPxZqr5MwDQYJKoZIhvcNAQELBQADggEB
-AIkMmqyoMqidTa3lvUPJNl4H+Ef/yPQkTkrsOd3WL8DQysyUdMLdQozr3K1bH5XB
-wRxoXX211nu4WhN18LsFJRCuHBSxmaNkBGFyl+JNvhPUSI6j0somNMCS75KJ0ZDx
-2HZsXmmJFF902VQxCR7vCIrFDrKDYq1e7GQbFS8t46FlpqivQMQWNPt18Bthj/1Y
-lO2GKRWFCX8VlOW7FtDQ6B3oC1oAGHBBGogAx7/0gh9DnYBKT14V/kuWW3RNABZJ
-ewHO1C6icQdnjtaREDyTP4oyL+uyAfXrFfbpti2hc00f8oYPQZYxj1yxl4UAdNij
-mS6YqH/WRioGMe3tBVeSdoo=
-"""
-
-valid_ca_private_key = """
-MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC/D6W27rfpdPIf
-16JHs8fx/7VehyCk8m03dPAQqv6wQiHF5xhXaFZER1+cnf7oExp9zi/4HJ/KRbcc
-1loVArXtV0zwAUftBmUeezGVfxhCHKhP89GnV4NB97jjklHFSxjEoT/0YvJQ1IV/
-3Cos1T5O8x14WIi31l7WQGYAyWxUXiP8QxGVmF3odEJoO3e7Ew9HFkamvuL6Z6c4
-uAVMM7uYXme7q0OM49Wu7C9hj39ZKbjG5FFKZTj+zDKgSbOiQaFk3blOky/e3ifN
-jZelGtussYPOMBkUirLvrSGGy7s3lm8Yp5PH5+UkVQB2rZyxRdZTC9kh+dShR1s/
-qcPnDw7lAgMBAAECggEAGm+j0kf9koPn7Jf9kEZD6CwlgEraLXiNvBqmDOhcDS9Z
-VPTA3XdGWHQ3uofx+VKLW9TntkDfqzEyQP83v6h8W7a0opDKzvUPkMQi/Dh1ttAY
-SdfGrozhUINiRbq9LbtSVgKpwrreJGkDf8mK3GE1Gd9xuHEnmahDvwlyE7HLF3Eh
-2xJDSAPx3OxcjR5hW7vbojhVCyCfuYTlZB86f0Sb8SqxZMt/y2zKmbzoTqpUBWbg
-lBnE7GJoNR07DWjxvEP8r6kQMh670I01SUR42CSK8X8asHhhZHUcggsNno+BBc6K
-sy4HzDIYIay6oy0atcVzKsGrlNCveeAiSEcw7x2yAQKBgQDsXz2FbhXYV5Vbt4wU
-5EWOa7if/+FG+TcVezOF3xlNBgykjXHQaYTYHrJq0qsEFrNT3ZGm9ezY4LdF3BTt
-5z/+i8QlCCw/nr3N7JZx6U5+OJl1j3NLFoFx3+DXo31pgJJEQCHHwdCkF5IuOcZ/
-b3nXkRZ80BVv7XD6F9bMHEwLYQKBgQDO7THcRDbsE6/+7VsTDf0P/JENba3DBBu1
-gjb1ItL5FHJwMgnkUadRZRo0QKye848ugribed39qSoJfNaBJrAT5T8S/9q+lXft
-vXUckcBO1CKNaP9gqF5fPIdNHf64GbmCiiHjOTE3rwJjkxJPpzLXyvgBO4aLeesK
-ThBdW+iWBQKBgD3crz08knsMcQqP/xl4pLuhdbBqR4tLrh7xH4rp2LVP3/8xBZiG
-BT6Kyicq+5cWWdiZJIWN127rYQvnjZK18wmriqomeW4tHX/Ha5hkdyaRqZga8xGz
-0iz7at0E7M2v2JgEMNMW5oQLpzZx6IFxq3G/hyMjUnj4q5jIpG7G+SABAoGBAKgT
-8Ika+4WcpDssrup2VVTT8Tp4GUkroBo6D8vkInvhiObrLi+/x2mM9tD0q4JdEbNU
-yQC454EwFA4q0c2MED/I2QfkvNhLbmO0nVi8ZvlgxEQawjzP5f/zmW8haxI9Cvsm
-mkoH3Zt+UzFwd9ItXFX97p6JrErEmA8Bw7chfXXFAoGACWR/c+s7hnX6gzyah3N1
-Db0xAaS6M9fzogcg2OM1i/6OCOcp4Sh1fmPG7tN45CCnFkhgVoRkSSA5MJAe2I/r
-xFm72VX7567T+4qIFua2iDxIBA/Z4zmj+RYfhHGPYZjdSjprKJxY6QOv5aoluBvE
-mlLy1Hmcry+ukWZtWezZfGY=
-"""
-
-valid_cert = """
-MIIDsTCCApmgAwIBAgIUDKOfYIwwtjww0vAMvJnXnGLhL+0wDQYJKoZIhvcNAQEL
-BQAwVzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcM
-CVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzEQMA4GA1UEAwwHdnlvcy5pbzAeFw0y
-NDA0MDEwNTQ5NTdaFw0yNTA0MDEwNTQ5NTdaMFcxCzAJBgNVBAYTAkdCMRMwEQYD
-VQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoMBFZ5
-T1MxEDAOBgNVBAMMB3Z5b3MuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
-AoIBAQCHtW25Umt6rqm2gfzqAZg1/VsqefZwAqIUAm2T3VwHQZ/2tNdr8ROWASii
-W5PToC7N8StMwFl2YoIof+MXGMO00toTTJePZOJKjF9U9hL3kuYuY1+yng4fl+E0
-96xVobb2KY4lMZ2rVwmpB7jkNO2LWxbJ6vHKcwMOhlx/8NEKIoVmkBT1Zkgy5dgn
-PgTtJcdVIU75XhQWqBmAUsMmACuZfqSYJbAv3hHz5V+Ejt0dI6mlGM7TXsCC9tKM
-64paIKZooFm78IsxJ26jHpZ8eh+SDBz0VBydBFWXm8VhOJ8NlZ1opAh3AWxFZDGt
-49uOsy82VmUcHPyoZ8DKYkBFHfSpAgMBAAGjdTBzMAwGA1UdEwEB/wQCMAAwDgYD
-VR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0GA1UdDgQWBBTeTcgM
-pRxAMjVBirjzo2QUu5H5fzAfBgNVHSMEGDAWgBT8cTZQ+fwlAH/0QvnYaq4/Fmqv
-kzANBgkqhkiG9w0BAQsFAAOCAQEAi4dBcH7TIYwWRW6bWRubMA7ztonV4EYb15Zf
-9yNafMWAEEBOii/DFo+j/ky9oInl7ZHw7gTIyXfLEarX/bM6fHOgiyj4zp3u6RnH
-5qlBypu/YCnyPjE/GvV05m2rrXnxZ4rCtcoO4u/HyGbV+jGnCmjShKICKyu1FdMd
-eeZRrLKPO/yghadGH34WVQnrbaorwlbi+NjB6fxmZQx5HE/SyK/9sb6WCpLMGHoy
-MpdQo3lV1ewtL3ElIWDq6mO030Mo5pwpjIU+8yHHNBVzg6mlGVgQPAp0gbUei9aP
-CJ8SLmMEi3NDk0E/sPgVC17e6bf2bx2nRuXROZekG2dd90Iu8g==
-"""
-
-valid_cert_private_key = """
-MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCHtW25Umt6rqm2
-gfzqAZg1/VsqefZwAqIUAm2T3VwHQZ/2tNdr8ROWASiiW5PToC7N8StMwFl2YoIo
-f+MXGMO00toTTJePZOJKjF9U9hL3kuYuY1+yng4fl+E096xVobb2KY4lMZ2rVwmp
-B7jkNO2LWxbJ6vHKcwMOhlx/8NEKIoVmkBT1Zkgy5dgnPgTtJcdVIU75XhQWqBmA
-UsMmACuZfqSYJbAv3hHz5V+Ejt0dI6mlGM7TXsCC9tKM64paIKZooFm78IsxJ26j
-HpZ8eh+SDBz0VBydBFWXm8VhOJ8NlZ1opAh3AWxFZDGt49uOsy82VmUcHPyoZ8DK
-YkBFHfSpAgMBAAECggEABofhw0W/ACEMcAjmpNTFkFCUXPGQXWDVD7EzuIZSNdOv
-yOm4Rbys6H6/B7wwO6KVagoBf1Cw5Xh1YtFPuoZxsZ+liMD6eLc+SB/j/RTYAhPO
-0bvsyK3gSF8w4nGKWLce9M74ZRwThkG6qGijmlDdPyP3r2kn8GoTQzVOWYZbavk/
-H3uE6PsZSWjOY+Mnm3vEmeItPYKGZ5+IP+YiTqZ4NCggBwH7csnR3/kbwY5Ns7jl
-3Av+EAdIeUwDNeMfLTzN7GphJR7gL6YQIhGKxE+W0GHXL2FubnnrFx8G75HFh1ay
-GkJXEqY5Lbd+7VPS0KcQdwhMSSoJsY5GUORUqrU80QKBgQC/0wJSu+Gfe7dONIby
-mnGRppSRIQVRjCjbVIN+Y2h1Kp3aK0qDpV7KFLCiUUtz9rWHR/NB4cDaIW543T55
-/jXUMD2j3EqtbtlsVQfDLQV7DyDrMmBAs4REHmyZmWTzHjCDUO79ahdOlZs34Alz
-wfpX3L3WVYGIAJKZtsUZ8FbrGQKBgQC1HFgVZ1PqP9/pW50RMh06BbQrhWPGiWgH
-Rn5bFthLkp3uqr9bReBq9tu3sqJuAhFudH68wup+Z+fTcHAcNg2Rs+Q+IKnULdB/
-UQHYoPjeWOvHAuOmgn9iD9OD7GCIv8fZmLit09vAsOWq+NKNBKCknGM70CDrvAlQ
-lOAUa34YEQKBgQC5i8GThWiYe3Kzktt1jy6LVDYgq3AZkRl0Diui9UT1EGPfxEAv
-VqZ5kcnJOBlj8h9k25PRBi0k0XGqN1dXaS1oMcFt3ofdenuU7iqz/7htcBTHa9Lu
-wrYNreAeMuISyADlBEQnm5cvzEZ3pZ1++wLMOhjmWY8Rnnwvczrz/CYXAQKBgH+t
-vcNJFvWblkUzWuWWiNgw0TWlUhPTJs2KOuYIku+kK0bohQLZnj6KTZeRjcU0HAnc
-gsScPShkJCEBsWeSC7reMVhDOrbknYpEF6MayJgn5ABm3wqyEQ+WzKzCZcPCQCf8
-7KVPKCsOCrufsv/LdVzXC3ZNYggOhhqS+e4rYbehAoGBAIsq252o3vgrunzS5FZx
-IONA2FvYrxVbDn5aF8WfNSdKFy3CAlt0P+Fm8gYbrKylIfMXpL8Oqc9RJou5onZP
-ZXLrtgVJR9W020qTurO2f91qfU8646n11hR9ObBB1IYbagOU0Pw1Nrq/FRp/u2tx
-7i7xFz2WEiQeSCPaKYOiqM3t
-"""
-
-
-class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
-    def tearDown(self):
-        # Check for running process
-        self.assertTrue(process_named_running(PROCESS_NAME))
-
-        self.cli_delete(['interfaces', 'ethernet', proxy_interface, 'address'])
-        self.cli_delete(base_path)
-        self.cli_delete(['pki'])
-        self.cli_commit()
-
-        # Process must be terminated after deleting the config
-        self.assertFalse(process_named_running(PROCESS_NAME))
-
-    def base_config(self):
-        self.cli_set(base_path + ['service', 'https_front', 'mode', 'http'])
-        self.cli_set(base_path + ['service', 'https_front', 'port', '4433'])
-        self.cli_set(base_path + ['service', 'https_front', 'backend', 'bk-01'])
-
-        self.cli_set(base_path + ['backend', 'bk-01', 'mode', 'http'])
-        self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'address', '192.0.2.11'])
-        self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'port', '9090'])
-        self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'send-proxy'])
-
-        self.cli_set(base_path + ['global-parameters', 'max-connections', '1000'])
-
-    def configure_pki(self):
-
-        # Valid CA
-        self.cli_set(['pki', 'ca', 'smoketest', 'certificate', valid_ca_cert.replace('\n','')])
-        self.cli_set(['pki', 'ca', 'smoketest', 'private', 'key', valid_ca_private_key.replace('\n','')])
-
-        # Valid cert
-        self.cli_set(['pki', 'certificate', 'smoketest', 'certificate', valid_cert.replace('\n','')])
-        self.cli_set(['pki', 'certificate', 'smoketest', 'private', 'key', valid_cert_private_key.replace('\n','')])
-
-    def test_01_lb_reverse_proxy_domain(self):
-        domains_bk_first = ['n1.example.com', 'n2.example.com', 'n3.example.com']
-        domain_bk_second = 'n5.example.com'
-        frontend = 'https_front'
-        front_port = '4433'
-        bk_server_first = '192.0.2.11'
-        bk_server_second = '192.0.2.12'
-        bk_first_name = 'bk-01'
-        bk_second_name = 'bk-02'
-        bk_server_port = '9090'
-        mode = 'http'
-        rule_ten = '10'
-        rule_twenty = '20'
-        rule_thirty = '30'
-        send_proxy = 'send-proxy'
-        max_connections = '1000'
-
-        back_base = base_path + ['backend']
-
-        self.cli_set(base_path + ['service', frontend, 'mode', mode])
-        self.cli_set(base_path + ['service', frontend, 'port', front_port])
-        for domain in domains_bk_first:
-            self.cli_set(base_path + ['service', frontend, 'rule', rule_ten, 'domain-name', domain])
-        self.cli_set(base_path + ['service', frontend, 'rule', rule_ten, 'set', 'backend', bk_first_name])
-        self.cli_set(base_path + ['service', frontend, 'rule', rule_twenty, 'domain-name', domain_bk_second])
-        self.cli_set(base_path + ['service', frontend, 'rule', rule_twenty, 'set', 'backend', bk_second_name])
-        self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'url-path', 'end', '/test'])
-        self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'set', 'backend', bk_second_name])
-
-        self.cli_set(back_base + [bk_first_name, 'mode', mode])
-        self.cli_set(back_base + [bk_first_name, 'server', bk_first_name, 'address', bk_server_first])
-        self.cli_set(back_base + [bk_first_name, 'server', bk_first_name, 'port', bk_server_port])
-        self.cli_set(back_base + [bk_first_name, 'server', bk_first_name, send_proxy])
-
-        self.cli_set(back_base + [bk_second_name, 'mode', mode])
-        self.cli_set(back_base + [bk_second_name, 'server', bk_second_name, 'address', bk_server_second])
-        self.cli_set(back_base + [bk_second_name, 'server', bk_second_name, 'port', bk_server_port])
-        self.cli_set(back_base + [bk_second_name, 'server', bk_second_name, 'backup'])
-
-        self.cli_set(base_path + ['global-parameters', 'max-connections', max_connections])
-
-        # commit changes
-        self.cli_commit()
-
-        config = read_file(HAPROXY_CONF)
-
-        # Global
-        self.assertIn(f'maxconn {max_connections}', config)
-
-        # Frontend
-        self.assertIn(f'frontend {frontend}', config)
-        self.assertIn(f'bind [::]:{front_port} v4v6', config)
-        self.assertIn(f'mode {mode}', config)
-        for domain in domains_bk_first:
-            self.assertIn(f'acl {rule_ten} hdr(host) -i {domain}', config)
-        self.assertIn(f'use_backend {bk_first_name} if {rule_ten}', config)
-        self.assertIn(f'acl {rule_twenty} hdr(host) -i {domain_bk_second}', config)
-        self.assertIn(f'use_backend {bk_second_name} if {rule_twenty}', config)
-        self.assertIn(f'acl {rule_thirty} path -i -m end /test', config)
-        self.assertIn(f'use_backend {bk_second_name} if {rule_thirty}', config)
-
-        # Backend
-        self.assertIn(f'backend {bk_first_name}', config)
-        self.assertIn(f'balance roundrobin', config)
-        self.assertIn(f'option forwardfor', config)
-        self.assertIn('http-request add-header X-Forwarded-Proto https if { ssl_fc }', config)
-        self.assertIn(f'mode {mode}', config)
-        self.assertIn(f'server {bk_first_name} {bk_server_first}:{bk_server_port} send-proxy', config)
-
-        self.assertIn(f'backend {bk_second_name}', config)
-        self.assertIn(f'mode {mode}', config)
-        self.assertIn(f'server {bk_second_name} {bk_server_second}:{bk_server_port}', config)
-        self.assertIn(f'server {bk_second_name} {bk_server_second}:{bk_server_port} backup', config)
-
-    def test_02_lb_reverse_proxy_cert_not_exists(self):
-        self.base_config()
-        self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert'])
-
-        with self.assertRaises(ConfigSessionError) as e:
-            self.cli_commit()
-        # self.assertIn('\nCertificates does not exist in PKI\n', str(e.exception))
-
-        self.cli_delete(base_path)
-        self.configure_pki()
-
-        self.base_config()
-        self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert'])
-
-        with self.assertRaises(ConfigSessionError) as e:
-            self.cli_commit()
-        # self.assertIn('\nCertificate "cert" does not exist\n', str(e.exception))
-
-        self.cli_delete(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert'])
-        self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'smoketest'])
-        self.cli_commit()
-
-    def test_03_lb_reverse_proxy_ca_not_exists(self):
-        self.base_config()
-        self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test'])
-
-        with self.assertRaises(ConfigSessionError) as e:
-            self.cli_commit()
-        # self.assertIn('\nCA certificates does not exist in PKI\n', str(e.exception))
-
-        self.cli_delete(base_path)
-        self.configure_pki()
-
-        self.base_config()
-        self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test'])
-
-        with self.assertRaises(ConfigSessionError) as e:
-            self.cli_commit()
-        # self.assertIn('\nCA certificate "ca-test" does not exist\n', str(e.exception))
-
-        self.cli_delete(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test'])
-        self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'smoketest'])
-        self.cli_commit()
-
-    def test_04_lb_reverse_proxy_backend_ssl_no_verify(self):
-        # Setup base
-        self.configure_pki()
-        self.base_config()
-
-        # Set no-verify option
-        self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'no-verify'])
-        self.cli_commit()
-
-        # Test no-verify option
-        config = read_file(HAPROXY_CONF)
-        self.assertIn('server bk-01 192.0.2.11:9090 send-proxy ssl verify none', config)
-
-        # Test setting ca-certificate alongside no-verify option fails, to test config validation
-        self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'smoketest'])
-        with self.assertRaises(ConfigSessionError) as e:
-            self.cli_commit()
-
-    def test_05_lb_reverse_proxy_backend_http_check(self):
-        # Setup base
-        self.base_config()
-
-        # Set http-check
-        self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'method', 'get'])
-        self.cli_commit()
-
-        # Test http-check
-        config = read_file(HAPROXY_CONF)
-        self.assertIn('option httpchk', config)
-        self.assertIn('http-check send meth GET', config)
-
-        # Set http-check with uri and status
-        self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'uri', '/health'])
-        self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'status', '200'])
-        self.cli_commit()
-
-        # Test http-check with uri and status
-        config = read_file(HAPROXY_CONF)
-        self.assertIn('option httpchk', config)
-        self.assertIn('http-check send meth GET uri /health', config)
-        self.assertIn('http-check expect status 200', config)
-
-        # Set http-check with string
-        self.cli_delete(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'status', '200'])
-        self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'string', 'success'])
-        self.cli_commit()
-
-        # Test http-check with string
-        config = read_file(HAPROXY_CONF)
-        self.assertIn('option httpchk', config)
-        self.assertIn('http-check send meth GET uri /health', config)
-        self.assertIn('http-check expect string success', config)
-
-        # Test configuring both http-check & health-check fails validation script
-        self.cli_set(base_path + ['backend', 'bk-01', 'health-check', 'ldap'])
-        with self.assertRaises(ConfigSessionError) as e:
-            self.cli_commit()
-
-    def test_06_lb_reverse_proxy_tcp_mode(self):
-        frontend = 'tcp_8443'
-        mode = 'tcp'
-        front_port = '8433'
-        tcp_request_delay = "5000"
-        rule_thirty = '30'
-        domain_bk = 'n6.example.com'
-        ssl_opt = "req-ssl-sni"
-        bk_name = 'bk-03'
-        bk_server = '192.0.2.11'
-        bk_server_port = '9090'
-
-        back_base = base_path + ['backend']
-
-        self.cli_set(base_path + ['service', frontend, 'mode', mode])
-        self.cli_set(base_path + ['service', frontend, 'port', front_port])
-        self.cli_set(base_path + ['service', frontend, 'tcp-request', 'inspect-delay', tcp_request_delay])
-
-        self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'domain-name', domain_bk])
-        self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'ssl', ssl_opt])
-        self.cli_set(base_path + ['service', frontend, 'rule', rule_thirty, 'set', 'backend', bk_name])
-
-        self.cli_set(back_base + [bk_name, 'mode', mode])
-        self.cli_set(back_base + [bk_name, 'server', bk_name, 'address', bk_server])
-        self.cli_set(back_base + [bk_name, 'server', bk_name, 'port', bk_server_port])
-
-        # commit changes
-        self.cli_commit()
-
-        config = read_file(HAPROXY_CONF)
-
-        # Frontend
-        self.assertIn(f'frontend {frontend}', config)
-        self.assertIn(f'bind [::]:{front_port} v4v6', config)
-        self.assertIn(f'mode {mode}', config)
-
-        self.assertIn(f'tcp-request inspect-delay {tcp_request_delay}', config)
-        self.assertIn(f"tcp-request content accept if {{ req_ssl_hello_type 1 }}", config)
-        self.assertIn(f'acl {rule_thirty} req_ssl_sni -i {domain_bk}', config)
-        self.assertIn(f'use_backend {bk_name} if {rule_thirty}', config)
-
-        # Backend
-        self.assertIn(f'backend {bk_name}', config)
-        self.assertIn(f'balance roundrobin', config)
-        self.assertIn(f'mode {mode}', config)
-        self.assertIn(f'server {bk_name} {bk_server}:{bk_server_port}', config)
-
-    def test_07_lb_reverse_proxy_http_response_headers(self):
-        # Setup base
-        self.configure_pki()
-        self.base_config()
-
-        # Set example headers in both frontend and backend
-        self.cli_set(base_path + ['service', 'https_front', 'http-response-headers', 'Cache-Control', 'value', 'max-age=604800'])
-        self.cli_set(base_path + ['backend', 'bk-01',  'http-response-headers', 'Proxy-Backend-ID', 'value', 'bk-01'])
-        self.cli_commit()
-
-        # Test headers are present in generated configuration file
-        config = read_file(HAPROXY_CONF)
-        self.assertIn('http-response set-header Cache-Control \'max-age=604800\'', config)
-        self.assertIn('http-response set-header Proxy-Backend-ID \'bk-01\'', config)
-
-        # Test setting alongside modes other than http is blocked by validation conditions
-        self.cli_set(base_path + ['service', 'https_front', 'mode', 'tcp'])
-        with self.assertRaises(ConfigSessionError) as e:
-            self.cli_commit()
-
-    def test_08_lb_reverse_proxy_tcp_health_checks(self):
-        # Setup PKI
-        self.configure_pki()
-
-        # Define variables
-        frontend = 'fe_ldaps'
-        mode = 'tcp'
-        health_check = 'ldap'
-        front_port = '636'
-        bk_name = 'bk_ldap'
-        bk_servers = ['192.0.2.11', '192.0.2.12']
-        bk_server_port = '389'
-
-        # Configure frontend
-        self.cli_set(base_path + ['service', frontend, 'mode', mode])
-        self.cli_set(base_path + ['service', frontend, 'port', front_port])
-        self.cli_set(base_path + ['service', frontend, 'ssl', 'certificate', 'smoketest'])
-
-        # Configure backend
-        self.cli_set(base_path + ['backend', bk_name, 'mode', mode])
-        self.cli_set(base_path + ['backend', bk_name, 'health-check', health_check])
-        for index, bk_server in enumerate(bk_servers):
-            self.cli_set(base_path + ['backend', bk_name, 'server', f'srv-{index}', 'address', bk_server])
-            self.cli_set(base_path + ['backend', bk_name, 'server', f'srv-{index}', 'port', bk_server_port])
-
-        # Commit & read config
-        self.cli_commit()
-        config = read_file(HAPROXY_CONF)
-
-        # Validate Frontend
-        self.assertIn(f'frontend {frontend}', config)
-        self.assertIn(f'bind [::]:{front_port} v4v6 ssl crt /run/haproxy/smoketest.pem', config)
-        self.assertIn(f'mode {mode}', config)
-        self.assertIn(f'backend {bk_name}', config)
-
-        # Validate Backend
-        self.assertIn(f'backend {bk_name}', config)
-        self.assertIn(f'option {health_check}-check', config)
-        self.assertIn(f'mode {mode}', config)
-        for index, bk_server in enumerate(bk_servers):
-            self.assertIn(f'server srv-{index} {bk_server}:{bk_server_port}', config)
-
-        # Validate SMTP option renders correctly
-        self.cli_set(base_path + ['backend', bk_name, 'health-check', 'smtp'])
-        self.cli_commit()
-        config = read_file(HAPROXY_CONF)
-        self.assertIn(f'option smtpchk', config)
-
-    def test_09_lb_reverse_proxy_logging(self):
-        # Setup base
-        self.base_config()
-        self.cli_commit()
-
-        # Ensure default logging configuration is present
-        config = read_file(HAPROXY_CONF)
-
-        # Test global-parameters logging options
-        self.cli_set(base_path + ['global-parameters', 'logging', 'facility', 'local1', 'level', 'err'])
-        self.cli_set(base_path + ['global-parameters', 'logging', 'facility', 'local2', 'level', 'warning'])
-        self.cli_commit()
-
-        # Test global logging parameters are generated in configuration file
-        config = read_file(HAPROXY_CONF)
-        self.assertIn('log /dev/log local1 err', config)
-        self.assertIn('log /dev/log local2 warning', config)
-
-        # Test backend logging options
-        backend_path = base_path + ['backend', 'bk-01']
-        self.cli_set(backend_path + ['logging', 'facility', 'local3', 'level', 'debug'])
-        self.cli_set(backend_path + ['logging', 'facility', 'local4', 'level', 'info'])
-        self.cli_commit()
-
-        # Test backend logging parameters are generated in configuration file
-        config = read_file(HAPROXY_CONF)
-        self.assertIn('log /dev/log local3 debug', config)
-        self.assertIn('log /dev/log local4 info', config)
-
-        # Test service logging options
-        service_path = base_path + ['service', 'https_front']
-        self.cli_set(service_path + ['logging', 'facility', 'local5', 'level', 'notice'])
-        self.cli_set(service_path + ['logging', 'facility', 'local6', 'level', 'crit'])
-        self.cli_commit()
-
-        # Test service logging parameters are generated in configuration file
-        config = read_file(HAPROXY_CONF)
-        self.assertIn('log /dev/log local5 notice', config)
-        self.assertIn('log /dev/log local6 crit', config)
-
-if __name__ == '__main__':
-    unittest.main(verbosity=2)
diff --git a/src/conf_mode/load-balancing_haproxy.py b/src/conf_mode/load-balancing_haproxy.py
new file mode 100644
index 000000000..45042dd52
--- /dev/null
+++ b/src/conf_mode/load-balancing_haproxy.py
@@ -0,0 +1,206 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit
+from shutil import rmtree
+
+from vyos.config import Config
+from vyos.configverify import verify_pki_certificate
+from vyos.configverify import verify_pki_ca_certificate
+from vyos.utils.dict import dict_search
+from vyos.utils.process import call
+from vyos.utils.network import check_port_availability
+from vyos.utils.network import is_listen_port_bind_service
+from vyos.pki import find_chain
+from vyos.pki import load_certificate
+from vyos.pki import load_private_key
+from vyos.pki import encode_certificate
+from vyos.pki import encode_private_key
+from vyos.template import render
+from vyos.utils.file import write_file
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+load_balancing_dir = '/run/haproxy'
+load_balancing_conf_file = f'{load_balancing_dir}/haproxy.cfg'
+systemd_service = 'haproxy.service'
+systemd_override = '/run/systemd/system/haproxy.service.d/10-override.conf'
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+
+    base = ['load-balancing', 'haproxy']
+    if not conf.exists(base):
+        return None
+    lb = conf.get_config_dict(base,
+                              get_first_key=True,
+                              key_mangling=('-', '_'),
+                              no_tag_node_value_mangle=True,
+                              with_recursive_defaults=True,
+                              with_pki=True)
+
+    return lb
+
+def verify(lb):
+    if not lb:
+        return None
+
+    if 'backend' not in lb or 'service' not in lb:
+        raise ConfigError(f'"service" and "backend" must be configured!')
+
+    for front, front_config in lb['service'].items():
+        if 'port' not in front_config:
+            raise ConfigError(f'"{front} service port" must be configured!')
+
+        # Check if bind address:port are used by another service
+        tmp_address = front_config.get('address', '0.0.0.0')
+        tmp_port = front_config['port']
+        if check_port_availability(tmp_address, int(tmp_port), 'tcp') is not True and \
+                not is_listen_port_bind_service(int(tmp_port), 'haproxy'):
+            raise ConfigError(f'"TCP" port "{tmp_port}" is used by another service')
+
+    for back, back_config in lb['backend'].items():
+        if 'http_check' in back_config:
+            http_check = back_config['http_check']
+            if 'expect' in http_check and 'status' in http_check['expect'] and 'string' in http_check['expect']:
+                raise ConfigError(f'"expect status" and "expect string" can not be configured together!')
+
+        if 'health_check' in back_config:
+            if back_config['mode'] != 'tcp':
+                raise ConfigError(f'backend "{back}" can only be configured with {back_config["health_check"]} ' +
+                                  f'health-check whilst in TCP mode!')
+            if 'http_check' in back_config:
+                raise ConfigError(f'backend "{back}" cannot be configured with both http-check and health-check!')
+
+        if 'server' not in back_config:
+            raise ConfigError(f'"{back} server" must be configured!')
+
+        for bk_server, bk_server_conf in back_config['server'].items():
+            if 'address' not in bk_server_conf or 'port' not in bk_server_conf:
+                raise ConfigError(f'"backend {back} server {bk_server} address and port" must be configured!')
+
+            if {'send_proxy', 'send_proxy_v2'} <= set(bk_server_conf):
+                raise ConfigError(f'Cannot use both "send-proxy" and "send-proxy-v2" for server "{bk_server}"')
+
+        if 'ssl' in back_config:
+            if {'no_verify', 'ca_certificate'} <= set(back_config['ssl']):
+                raise ConfigError(f'backend {back} cannot have both ssl options no-verify and ca-certificate set!')
+
+    # Check if http-response-headers are configured in any frontend/backend where mode != http
+    for group in ['service', 'backend']:
+        for config_name, config in lb[group].items():
+            if 'http_response_headers' in config and config['mode'] != 'http':
+                raise ConfigError(f'{group} {config_name} must be set to http mode to use http_response_headers!')
+
+    for front, front_config in lb['service'].items():
+        for cert in dict_search('ssl.certificate', front_config) or []:
+            verify_pki_certificate(lb, cert)
+
+    for back, back_config in lb['backend'].items():
+        tmp = dict_search('ssl.ca_certificate', back_config)
+        if tmp: verify_pki_ca_certificate(lb, tmp)
+
+
+def generate(lb):
+    if not lb:
+        # Delete /run/haproxy/haproxy.cfg
+        config_files = [load_balancing_conf_file, systemd_override]
+        for file in config_files:
+            if os.path.isfile(file):
+                os.unlink(file)
+        # Delete old directories
+        if os.path.isdir(load_balancing_dir):
+            rmtree(load_balancing_dir, ignore_errors=True)
+
+        return None
+
+    # Create load-balance dir
+    if not os.path.isdir(load_balancing_dir):
+        os.mkdir(load_balancing_dir)
+
+    loaded_ca_certs = {load_certificate(c['certificate'])
+        for c in lb['pki']['ca'].values()} if 'ca' in lb['pki'] else {}
+
+    # SSL Certificates for frontend
+    for front, front_config in lb['service'].items():
+        if 'ssl' not in front_config:
+            continue
+
+        if 'certificate' in front_config['ssl']:
+            cert_names = front_config['ssl']['certificate']
+
+            for cert_name in cert_names:
+                pki_cert = lb['pki']['certificate'][cert_name]
+                cert_file_path = os.path.join(load_balancing_dir, f'{cert_name}.pem')
+                cert_key_path = os.path.join(load_balancing_dir, f'{cert_name}.pem.key')
+
+                loaded_pki_cert = load_certificate(pki_cert['certificate'])
+                cert_full_chain = find_chain(loaded_pki_cert, loaded_ca_certs)
+
+                write_file(cert_file_path,
+                   '\n'.join(encode_certificate(c) for c in cert_full_chain))
+
+                if 'private' in pki_cert and 'key' in pki_cert['private']:
+                    loaded_key = load_private_key(pki_cert['private']['key'], passphrase=None, wrap_tags=True)
+                    key_pem = encode_private_key(loaded_key, passphrase=None)
+                    write_file(cert_key_path, key_pem)
+
+    # SSL Certificates for backend
+    for back, back_config in lb['backend'].items():
+        if 'ssl' not in back_config:
+            continue
+
+        if 'ca_certificate' in back_config['ssl']:
+            ca_name = back_config['ssl']['ca_certificate']
+            ca_cert_file_path = os.path.join(load_balancing_dir, f'{ca_name}.pem')
+            ca_chains = []
+
+            pki_ca_cert = lb['pki']['ca'][ca_name]
+            loaded_ca_cert = load_certificate(pki_ca_cert['certificate'])
+            ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs)
+            ca_chains.append('\n'.join(encode_certificate(c) for c in ca_full_chain))
+            write_file(ca_cert_file_path, '\n'.join(ca_chains))
+
+    render(load_balancing_conf_file, 'load-balancing/haproxy.cfg.j2', lb)
+    render(systemd_override, 'load-balancing/override_haproxy.conf.j2', lb)
+
+    return None
+
+def apply(lb):
+    call('systemctl daemon-reload')
+    if not lb:
+        call(f'systemctl stop {systemd_service}')
+    else:
+        call(f'systemctl reload-or-restart {systemd_service}')
+
+    return None
+
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/load-balancing_reverse-proxy.py b/src/conf_mode/load-balancing_reverse-proxy.py
deleted file mode 100755
index 17226efe9..000000000
--- a/src/conf_mode/load-balancing_reverse-proxy.py
+++ /dev/null
@@ -1,206 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2023-2024 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import os
-
-from sys import exit
-from shutil import rmtree
-
-from vyos.config import Config
-from vyos.configverify import verify_pki_certificate
-from vyos.configverify import verify_pki_ca_certificate
-from vyos.utils.dict import dict_search
-from vyos.utils.process import call
-from vyos.utils.network import check_port_availability
-from vyos.utils.network import is_listen_port_bind_service
-from vyos.pki import find_chain
-from vyos.pki import load_certificate
-from vyos.pki import load_private_key
-from vyos.pki import encode_certificate
-from vyos.pki import encode_private_key
-from vyos.template import render
-from vyos.utils.file import write_file
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-load_balancing_dir = '/run/haproxy'
-load_balancing_conf_file = f'{load_balancing_dir}/haproxy.cfg'
-systemd_service = 'haproxy.service'
-systemd_override = '/run/systemd/system/haproxy.service.d/10-override.conf'
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-
-    base = ['load-balancing', 'reverse-proxy']
-    if not conf.exists(base):
-        return None
-    lb = conf.get_config_dict(base,
-                              get_first_key=True,
-                              key_mangling=('-', '_'),
-                              no_tag_node_value_mangle=True,
-                              with_recursive_defaults=True,
-                              with_pki=True)
-
-    return lb
-
-def verify(lb):
-    if not lb:
-        return None
-
-    if 'backend' not in lb or 'service' not in lb:
-        raise ConfigError(f'"service" and "backend" must be configured!')
-
-    for front, front_config in lb['service'].items():
-        if 'port' not in front_config:
-            raise ConfigError(f'"{front} service port" must be configured!')
-
-        # Check if bind address:port are used by another service
-        tmp_address = front_config.get('address', '0.0.0.0')
-        tmp_port = front_config['port']
-        if check_port_availability(tmp_address, int(tmp_port), 'tcp') is not True and \
-                not is_listen_port_bind_service(int(tmp_port), 'haproxy'):
-            raise ConfigError(f'"TCP" port "{tmp_port}" is used by another service')
-
-    for back, back_config in lb['backend'].items():
-        if 'http_check' in back_config:
-            http_check = back_config['http_check']
-            if 'expect' in http_check and 'status' in http_check['expect'] and 'string' in http_check['expect']:
-                raise ConfigError(f'"expect status" and "expect string" can not be configured together!')
-
-        if 'health_check' in back_config:
-            if back_config['mode'] != 'tcp':
-                raise ConfigError(f'backend "{back}" can only be configured with {back_config["health_check"]} ' +
-                                  f'health-check whilst in TCP mode!')
-            if 'http_check' in back_config:
-                raise ConfigError(f'backend "{back}" cannot be configured with both http-check and health-check!')
-
-        if 'server' not in back_config:
-            raise ConfigError(f'"{back} server" must be configured!')
-
-        for bk_server, bk_server_conf in back_config['server'].items():
-            if 'address' not in bk_server_conf or 'port' not in bk_server_conf:
-                raise ConfigError(f'"backend {back} server {bk_server} address and port" must be configured!')
-
-            if {'send_proxy', 'send_proxy_v2'} <= set(bk_server_conf):
-                raise ConfigError(f'Cannot use both "send-proxy" and "send-proxy-v2" for server "{bk_server}"')
-
-        if 'ssl' in back_config:
-            if {'no_verify', 'ca_certificate'} <= set(back_config['ssl']):
-                raise ConfigError(f'backend {back} cannot have both ssl options no-verify and ca-certificate set!')
-
-    # Check if http-response-headers are configured in any frontend/backend where mode != http
-    for group in ['service', 'backend']:
-        for config_name, config in lb[group].items():
-            if 'http_response_headers' in config and config['mode'] != 'http':
-                raise ConfigError(f'{group} {config_name} must be set to http mode to use http_response_headers!')
-
-    for front, front_config in lb['service'].items():
-        for cert in dict_search('ssl.certificate', front_config) or []:
-            verify_pki_certificate(lb, cert)
-
-    for back, back_config in lb['backend'].items():
-        tmp = dict_search('ssl.ca_certificate', back_config)
-        if tmp: verify_pki_ca_certificate(lb, tmp)
-
-
-def generate(lb):
-    if not lb:
-        # Delete /run/haproxy/haproxy.cfg
-        config_files = [load_balancing_conf_file, systemd_override]
-        for file in config_files:
-            if os.path.isfile(file):
-                os.unlink(file)
-        # Delete old directories
-        if os.path.isdir(load_balancing_dir):
-            rmtree(load_balancing_dir, ignore_errors=True)
-
-        return None
-
-    # Create load-balance dir
-    if not os.path.isdir(load_balancing_dir):
-        os.mkdir(load_balancing_dir)
-
-    loaded_ca_certs = {load_certificate(c['certificate'])
-        for c in lb['pki']['ca'].values()} if 'ca' in lb['pki'] else {}
-
-    # SSL Certificates for frontend
-    for front, front_config in lb['service'].items():
-        if 'ssl' not in front_config:
-            continue
-
-        if 'certificate' in front_config['ssl']:
-            cert_names = front_config['ssl']['certificate']
-
-            for cert_name in cert_names:
-                pki_cert = lb['pki']['certificate'][cert_name]
-                cert_file_path = os.path.join(load_balancing_dir, f'{cert_name}.pem')
-                cert_key_path = os.path.join(load_balancing_dir, f'{cert_name}.pem.key')
-
-                loaded_pki_cert = load_certificate(pki_cert['certificate'])
-                cert_full_chain = find_chain(loaded_pki_cert, loaded_ca_certs)
-
-                write_file(cert_file_path,
-                   '\n'.join(encode_certificate(c) for c in cert_full_chain))
-
-                if 'private' in pki_cert and 'key' in pki_cert['private']:
-                    loaded_key = load_private_key(pki_cert['private']['key'], passphrase=None, wrap_tags=True)
-                    key_pem = encode_private_key(loaded_key, passphrase=None)
-                    write_file(cert_key_path, key_pem)
-
-    # SSL Certificates for backend
-    for back, back_config in lb['backend'].items():
-        if 'ssl' not in back_config:
-            continue
-
-        if 'ca_certificate' in back_config['ssl']:
-            ca_name = back_config['ssl']['ca_certificate']
-            ca_cert_file_path = os.path.join(load_balancing_dir, f'{ca_name}.pem')
-            ca_chains = []
-
-            pki_ca_cert = lb['pki']['ca'][ca_name]
-            loaded_ca_cert = load_certificate(pki_ca_cert['certificate'])
-            ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs)
-            ca_chains.append('\n'.join(encode_certificate(c) for c in ca_full_chain))
-            write_file(ca_cert_file_path, '\n'.join(ca_chains))
-
-    render(load_balancing_conf_file, 'load-balancing/haproxy.cfg.j2', lb)
-    render(systemd_override, 'load-balancing/override_haproxy.conf.j2', lb)
-
-    return None
-
-def apply(lb):
-    call('systemctl daemon-reload')
-    if not lb:
-        call(f'systemctl stop {systemd_service}')
-    else:
-        call(f'systemctl reload-or-restart {systemd_service}')
-
-    return None
-
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py
index 233d73ba8..45e0129a3 100755
--- a/src/conf_mode/pki.py
+++ b/src/conf_mode/pki.py
@@ -71,7 +71,7 @@ sync_search = [
     },
     {
         'keys': ['certificate', 'ca_certificate'],
-        'path': ['load_balancing', 'reverse_proxy'],
+        'path': ['load_balancing', 'haproxy'],
     },
     {
         'keys': ['key'],
diff --git a/src/migration-scripts/reverse-proxy/1-to-2 b/src/migration-scripts/reverse-proxy/1-to-2
new file mode 100755
index 000000000..61612bc36
--- /dev/null
+++ b/src/migration-scripts/reverse-proxy/1-to-2
@@ -0,0 +1,27 @@
+# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library.  If not, see <http://www.gnu.org/licenses/>.
+
+# T6745: Rename base node to haproxy
+
+from vyos.configtree import ConfigTree
+
+base = ['load-balancing', 'reverse-proxy']
+
+def migrate(config: ConfigTree) -> None:
+    if not config.exists(base):
+        # Nothing to do
+        return
+
+    config.rename(base, 'haproxy')
diff --git a/src/op_mode/load-balancing_haproxy.py b/src/op_mode/load-balancing_haproxy.py
new file mode 100755
index 000000000..ae6734e16
--- /dev/null
+++ b/src/op_mode/load-balancing_haproxy.py
@@ -0,0 +1,237 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023-2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import socket
+import sys
+
+from tabulate import tabulate
+from vyos.configquery import ConfigTreeQuery
+
+import vyos.opmode
+
+socket_path = '/run/haproxy/admin.sock'
+timeout = 5
+
+
+def _execute_haproxy_command(command):
+    """Execute a command on the HAProxy UNIX socket and retrieve the response.
+
+    Args:
+        command (str): The command to be executed.
+
+    Returns:
+        str: The response received from the HAProxy UNIX socket.
+
+    Raises:
+        socket.error: If there is an error while connecting or communicating with the socket.
+
+    Finally:
+        Closes the socket connection.
+
+    Notes:
+        - HAProxy expects a newline character at the end of the command.
+        - The socket connection is established using the HAProxy UNIX socket.
+        - The response from the socket is received and decoded.
+
+    Example:
+        response = _execute_haproxy_command('show stat')
+        print(response)
+    """
+    try:
+        # HAProxy expects new line for command
+        command = f'{command}\n'
+
+        # Connect to the HAProxy UNIX socket
+        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+        sock.connect(socket_path)
+
+        # Set the socket timeout
+        sock.settimeout(timeout)
+
+        # Send the command
+        sock.sendall(command.encode())
+
+        # Receive and decode the response
+        response = b''
+        while True:
+            data = sock.recv(4096)
+            if not data:
+                break
+            response += data
+        response = response.decode()
+
+        return (response)
+
+    except socket.error as e:
+        print(f"Error: {e}")
+
+    finally:
+        # Close the socket
+        sock.close()
+
+
+def _convert_seconds(seconds):
+    """Convert seconds to days, hours, minutes, and seconds.
+
+    Args:
+        seconds (int): The number of seconds to convert.
+
+    Returns:
+        tuple: A tuple containing the number of days, hours, minutes, and seconds.
+    """
+    minutes = seconds // 60
+    hours = minutes // 60
+    days = hours // 24
+
+    return days, hours % 24, minutes % 60, seconds % 60
+
+
+def _last_change_format(seconds):
+    """Format the time components into a string representation.
+
+    Args:
+        seconds (int): The total number of seconds.
+
+    Returns:
+        str: The formatted time string with days, hours, minutes, and seconds.
+
+    Examples:
+        >>> _last_change_format(1434)
+        '23m54s'
+        >>> _last_change_format(93734)
+        '1d0h23m54s'
+        >>> _last_change_format(85434)
+        '23h23m54s'
+    """
+    days, hours, minutes, seconds = _convert_seconds(seconds)
+    time_format = ""
+
+    if days:
+        time_format += f"{days}d"
+    if hours:
+        time_format += f"{hours}h"
+    if minutes:
+        time_format += f"{minutes}m"
+    if seconds:
+        time_format += f"{seconds}s"
+
+    return time_format
+
+
+def _get_json_data():
+    """Get haproxy data format JSON"""
+    return _execute_haproxy_command('show stat json')
+
+
+def _get_raw_data():
+    """Retrieve raw data from JSON and organize it into a dictionary.
+
+    Returns:
+        dict: A dictionary containing the organized data categorized
+              into frontend, backend, and server.
+    """
+
+    data = json.loads(_get_json_data())
+    lb_dict = {'frontend': [], 'backend': [], 'server': []}
+
+    for key in data:
+        frontend = []
+        backend = []
+        server = []
+        for entry in key:
+            obj_type = entry['objType'].lower()
+            position = entry['field']['pos']
+            name = entry['field']['name']
+            value = entry['value']['value']
+
+            dict_entry = {'pos': position, 'name': {name: value}}
+
+            if obj_type == 'frontend':
+                frontend.append(dict_entry)
+            elif obj_type == 'backend':
+                backend.append(dict_entry)
+            elif obj_type == 'server':
+                server.append(dict_entry)
+
+        if len(frontend) > 0:
+            lb_dict['frontend'].append(frontend)
+        if len(backend) > 0:
+            lb_dict['backend'].append(backend)
+        if len(server) > 0:
+            lb_dict['server'].append(server)
+
+    return lb_dict
+
+
+def _get_formatted_output(data):
+    """
+    Format the data into a tabulated output.
+
+    Args:
+        data (dict): The data to be formatted.
+
+    Returns:
+        str: The tabulated output representing the formatted data.
+    """
+    table = []
+    headers = [
+        "Proxy name", "Role", "Status", "Req rate", "Resp time", "Last change"
+    ]
+
+    for key in data:
+        for item in data[key]:
+            row = [None] * len(headers)
+
+            for element in item:
+                if 'pxname' in element['name']:
+                    row[0] = element['name']['pxname']
+                elif 'svname' in element['name']:
+                    row[1] = element['name']['svname']
+                elif 'status' in element['name']:
+                    row[2] = element['name']['status']
+                elif 'req_rate' in element['name']:
+                    row[3] = element['name']['req_rate']
+                elif 'rtime' in element['name']:
+                    row[4] = f"{element['name']['rtime']} ms"
+                elif 'lastchg' in element['name']:
+                    row[5] = _last_change_format(element['name']['lastchg'])
+            table.append(row)
+
+    out = tabulate(table, headers, numalign="left")
+    return out
+
+
+def show(raw: bool):
+    config = ConfigTreeQuery()
+    if not config.exists('load-balancing haproxy'):
+        raise vyos.opmode.UnconfiguredSubsystem('Haproxy is not configured')
+
+    data = _get_raw_data()
+    if raw:
+        return data
+    else:
+        return _get_formatted_output(data)
+
+
+if __name__ == '__main__':
+    try:
+        res = vyos.opmode.run(sys.modules[__name__])
+        if res:
+            print(res)
+    except (ValueError, vyos.opmode.Error) as e:
+        print(e)
+        sys.exit(1)
diff --git a/src/op_mode/restart.py b/src/op_mode/restart.py
index a83c8b9d8..3b0031f34 100755
--- a/src/op_mode/restart.py
+++ b/src/op_mode/restart.py
@@ -41,6 +41,10 @@ service_map = {
         'systemd_service': 'pdns-recursor',
         'path': ['service', 'dns', 'forwarding'],
     },
+    'haproxy': {
+        'systemd_service': 'haproxy',
+        'path': ['load-balancing', 'haproxy'],
+    },
     'igmp_proxy': {
         'systemd_service': 'igmpproxy',
         'path': ['protocols', 'igmp-proxy'],
@@ -53,10 +57,6 @@ service_map = {
         'systemd_service': 'avahi-daemon',
         'path': ['service', 'mdns', 'repeater'],
     },
-    'reverse_proxy': {
-        'systemd_service': 'haproxy',
-        'path': ['load-balancing', 'reverse-proxy'],
-    },
     'router_advert': {
         'systemd_service': 'radvd',
         'path': ['service', 'router-advert'],
@@ -83,10 +83,10 @@ services = typing.Literal[
     'dhcpv6',
     'dns_dynamic',
     'dns_forwarding',
+    'haproxy',
     'igmp_proxy',
     'ipsec',
     'mdns_repeater',
-    'reverse_proxy',
     'router_advert',
     'snmp',
     'ssh',
diff --git a/src/op_mode/reverseproxy.py b/src/op_mode/reverseproxy.py
deleted file mode 100755
index 19704182a..000000000
--- a/src/op_mode/reverseproxy.py
+++ /dev/null
@@ -1,237 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2023-2024 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import json
-import socket
-import sys
-
-from tabulate import tabulate
-from vyos.configquery import ConfigTreeQuery
-
-import vyos.opmode
-
-socket_path = '/run/haproxy/admin.sock'
-timeout = 5
-
-
-def _execute_haproxy_command(command):
-    """Execute a command on the HAProxy UNIX socket and retrieve the response.
-
-    Args:
-        command (str): The command to be executed.
-
-    Returns:
-        str: The response received from the HAProxy UNIX socket.
-
-    Raises:
-        socket.error: If there is an error while connecting or communicating with the socket.
-
-    Finally:
-        Closes the socket connection.
-
-    Notes:
-        - HAProxy expects a newline character at the end of the command.
-        - The socket connection is established using the HAProxy UNIX socket.
-        - The response from the socket is received and decoded.
-
-    Example:
-        response = _execute_haproxy_command('show stat')
-        print(response)
-    """
-    try:
-        # HAProxy expects new line for command
-        command = f'{command}\n'
-
-        # Connect to the HAProxy UNIX socket
-        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
-        sock.connect(socket_path)
-
-        # Set the socket timeout
-        sock.settimeout(timeout)
-
-        # Send the command
-        sock.sendall(command.encode())
-
-        # Receive and decode the response
-        response = b''
-        while True:
-            data = sock.recv(4096)
-            if not data:
-                break
-            response += data
-        response = response.decode()
-
-        return (response)
-
-    except socket.error as e:
-        print(f"Error: {e}")
-
-    finally:
-        # Close the socket
-        sock.close()
-
-
-def _convert_seconds(seconds):
-    """Convert seconds to days, hours, minutes, and seconds.
-
-    Args:
-        seconds (int): The number of seconds to convert.
-
-    Returns:
-        tuple: A tuple containing the number of days, hours, minutes, and seconds.
-    """
-    minutes = seconds // 60
-    hours = minutes // 60
-    days = hours // 24
-
-    return days, hours % 24, minutes % 60, seconds % 60
-
-
-def _last_change_format(seconds):
-    """Format the time components into a string representation.
-
-    Args:
-        seconds (int): The total number of seconds.
-
-    Returns:
-        str: The formatted time string with days, hours, minutes, and seconds.
-
-    Examples:
-        >>> _last_change_format(1434)
-        '23m54s'
-        >>> _last_change_format(93734)
-        '1d0h23m54s'
-        >>> _last_change_format(85434)
-        '23h23m54s'
-    """
-    days, hours, minutes, seconds = _convert_seconds(seconds)
-    time_format = ""
-
-    if days:
-        time_format += f"{days}d"
-    if hours:
-        time_format += f"{hours}h"
-    if minutes:
-        time_format += f"{minutes}m"
-    if seconds:
-        time_format += f"{seconds}s"
-
-    return time_format
-
-
-def _get_json_data():
-    """Get haproxy data format JSON"""
-    return _execute_haproxy_command('show stat json')
-
-
-def _get_raw_data():
-    """Retrieve raw data from JSON and organize it into a dictionary.
-
-    Returns:
-        dict: A dictionary containing the organized data categorized
-              into frontend, backend, and server.
-    """
-
-    data = json.loads(_get_json_data())
-    lb_dict = {'frontend': [], 'backend': [], 'server': []}
-
-    for key in data:
-        frontend = []
-        backend = []
-        server = []
-        for entry in key:
-            obj_type = entry['objType'].lower()
-            position = entry['field']['pos']
-            name = entry['field']['name']
-            value = entry['value']['value']
-
-            dict_entry = {'pos': position, 'name': {name: value}}
-
-            if obj_type == 'frontend':
-                frontend.append(dict_entry)
-            elif obj_type == 'backend':
-                backend.append(dict_entry)
-            elif obj_type == 'server':
-                server.append(dict_entry)
-
-        if len(frontend) > 0:
-            lb_dict['frontend'].append(frontend)
-        if len(backend) > 0:
-            lb_dict['backend'].append(backend)
-        if len(server) > 0:
-            lb_dict['server'].append(server)
-
-    return lb_dict
-
-
-def _get_formatted_output(data):
-    """
-    Format the data into a tabulated output.
-
-    Args:
-        data (dict): The data to be formatted.
-
-    Returns:
-        str: The tabulated output representing the formatted data.
-    """
-    table = []
-    headers = [
-        "Proxy name", "Role", "Status", "Req rate", "Resp time", "Last change"
-    ]
-
-    for key in data:
-        for item in data[key]:
-            row = [None] * len(headers)
-
-            for element in item:
-                if 'pxname' in element['name']:
-                    row[0] = element['name']['pxname']
-                elif 'svname' in element['name']:
-                    row[1] = element['name']['svname']
-                elif 'status' in element['name']:
-                    row[2] = element['name']['status']
-                elif 'req_rate' in element['name']:
-                    row[3] = element['name']['req_rate']
-                elif 'rtime' in element['name']:
-                    row[4] = f"{element['name']['rtime']} ms"
-                elif 'lastchg' in element['name']:
-                    row[5] = _last_change_format(element['name']['lastchg'])
-            table.append(row)
-
-    out = tabulate(table, headers, numalign="left")
-    return out
-
-
-def show(raw: bool):
-    config = ConfigTreeQuery()
-    if not config.exists('load-balancing reverse-proxy'):
-        raise vyos.opmode.UnconfiguredSubsystem('Reverse-proxy is not configured')
-
-    data = _get_raw_data()
-    if raw:
-        return data
-    else:
-        return _get_formatted_output(data)
-
-
-if __name__ == '__main__':
-    try:
-        res = vyos.opmode.run(sys.modules[__name__])
-        if res:
-            print(res)
-    except (ValueError, vyos.opmode.Error) as e:
-        print(e)
-        sys.exit(1)
-- 
cgit v1.2.3


From 3f933f1642debfca5c9e3873790a5742ef242c75 Mon Sep 17 00:00:00 2001
From: Robert Göhler <github@ghlr.de>
Date: Thu, 17 Oct 2024 08:35:47 +0200
Subject: T973: add basic frr_exporter implementation (#4150)

---
 .../templates/frr_exporter/frr_exporter.service.j2 |  20 ++++
 debian/control                                     |   3 +
 .../service_monitoring_frr_exporter.xml.in         |  25 +++++
 .../cli/test_service_monitoring_frr-exporter.py    |  64 +++++++++++++
 src/conf_mode/service_monitoring_frr-exporter.py   | 101 +++++++++++++++++++++
 5 files changed, 213 insertions(+)
 create mode 100644 data/templates/frr_exporter/frr_exporter.service.j2
 create mode 100644 interface-definitions/service_monitoring_frr_exporter.xml.in
 create mode 100755 smoketest/scripts/cli/test_service_monitoring_frr-exporter.py
 create mode 100755 src/conf_mode/service_monitoring_frr-exporter.py

(limited to 'interface-definitions')

diff --git a/data/templates/frr_exporter/frr_exporter.service.j2 b/data/templates/frr_exporter/frr_exporter.service.j2
new file mode 100644
index 000000000..c3892e42b
--- /dev/null
+++ b/data/templates/frr_exporter/frr_exporter.service.j2
@@ -0,0 +1,20 @@
+{% set vrf_command = 'ip vrf exec ' ~ vrf ~ ' runuser -u frr -- ' if vrf is vyos_defined else '' %}
+[Unit]
+Description=FRR Exporter
+Documentation=https://github.com/tynany/frr_exporter
+After=network.target
+
+[Service]
+{% if vrf is not vyos_defined %}
+User=frr
+{% endif %}
+ExecStart={{ vrf_command }}/usr/sbin/frr_exporter \
+{% if listen_address is vyos_defined %}
+{%     for address in listen_address %}
+        --web.listen-address={{ address }}:{{ port }}
+{%     endfor %}
+{% else %}
+        --web.listen-address=:{{ port }}
+{% endif %}
+[Install]
+WantedBy=multi-user.target
diff --git a/debian/control b/debian/control
index 20cfcdc43..a19461412 100644
--- a/debian/control
+++ b/debian/control
@@ -238,6 +238,9 @@ Depends:
 # For "service monitoring node-exporter"
   node-exporter,
 # End "service monitoring node-exporter"
+# For "service monitoring frr-exporter"
+  frr-exporter,
+# End "service monitoring frr-exporter"
 # For "service monitoring telegraf"
   telegraf (>= 1.20),
 # End "service monitoring telegraf"
diff --git a/interface-definitions/service_monitoring_frr_exporter.xml.in b/interface-definitions/service_monitoring_frr_exporter.xml.in
new file mode 100644
index 000000000..96aee3ab4
--- /dev/null
+++ b/interface-definitions/service_monitoring_frr_exporter.xml.in
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+  <node name="service">
+    <children>
+      <node name="monitoring">
+        <children>
+          <node name="frr-exporter" owner="${vyos_conf_scripts_dir}/service_monitoring_frr-exporter.py">
+            <properties>
+              <help>Prometheus exporter for FRR metrics</help>
+              <priority>1280</priority>
+            </properties>
+            <children>
+              #include <include/listen-address.xml.i>
+              #include <include/port-number.xml.i>
+              <leafNode name="port">
+                <defaultValue>9342</defaultValue>
+              </leafNode>
+              #include <include/interface/vrf.xml.i>
+            </children>
+          </node>
+        </children>
+      </node>
+    </children>
+  </node>
+</interfaceDefinition>
diff --git a/smoketest/scripts/cli/test_service_monitoring_frr-exporter.py b/smoketest/scripts/cli/test_service_monitoring_frr-exporter.py
new file mode 100755
index 000000000..230171c11
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_monitoring_frr-exporter.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+
+PROCESS_NAME = 'frr_exporter'
+base_path = ['service', 'monitoring', 'frr-exporter']
+service_file = '/etc/systemd/system/frr_exporter.service'
+listen_if = 'dum3421'
+listen_ip = '192.0.2.1'
+
+
+class TestMonitoringFrrExporter(VyOSUnitTestSHIM.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        # call base-classes classmethod
+        super(TestMonitoringFrrExporter, cls).setUpClass()
+        # create a test interfaces
+        cls.cli_set(
+            cls, ['interfaces', 'dummy', listen_if, 'address', listen_ip + '/32']
+        )
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.cli_delete(cls, ['interfaces', 'dummy', listen_if])
+        super(TestMonitoringFrrExporter, cls).tearDownClass()
+
+    def tearDown(self):
+        self.cli_delete(base_path)
+        self.cli_commit()
+        self.assertFalse(process_named_running(PROCESS_NAME))
+
+    def test_01_basic_config(self):
+        self.cli_set(base_path + ['listen-address', listen_ip])
+
+        # commit changes
+        self.cli_commit()
+
+        file_content = read_file(service_file)
+        self.assertIn(f'{listen_ip}:9342', file_content)
+
+        # Check for running process
+        self.assertTrue(process_named_running(PROCESS_NAME))
+
+
+if __name__ == '__main__':
+    unittest.main(verbosity=2)
diff --git a/src/conf_mode/service_monitoring_frr-exporter.py b/src/conf_mode/service_monitoring_frr-exporter.py
new file mode 100755
index 000000000..01527d579
--- /dev/null
+++ b/src/conf_mode/service_monitoring_frr-exporter.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import is_node_changed
+from vyos.configverify import verify_vrf
+from vyos.template import render
+from vyos.utils.process import call
+from vyos import ConfigError
+from vyos import airbag
+
+
+airbag.enable()
+
+service_file = '/etc/systemd/system/frr_exporter.service'
+systemd_service = 'frr_exporter.service'
+
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+    base = ['service', 'monitoring', 'frr-exporter']
+    if not conf.exists(base):
+        return None
+
+    config_data = conf.get_config_dict(
+        base, key_mangling=('-', '_'), get_first_key=True
+    )
+    config_data = conf.merge_defaults(config_data, recursive=True)
+
+    tmp = is_node_changed(conf, base + ['vrf'])
+    if tmp:
+        config_data.update({'restart_required': {}})
+
+    return config_data
+
+
+def verify(config_data):
+    # bail out early - looks like removal from running config
+    if not config_data:
+        return None
+
+    verify_vrf(config_data)
+    return None
+
+
+def generate(config_data):
+    if not config_data:
+        # Delete systemd files
+        if os.path.isfile(service_file):
+            os.unlink(service_file)
+        return None
+
+    # Render frr_exporter service_file
+    render(service_file, 'frr_exporter/frr_exporter.service.j2', config_data)
+    return None
+
+
+def apply(config_data):
+    # Reload systemd manager configuration
+    call('systemctl daemon-reload')
+    if not config_data:
+        call(f'systemctl stop {systemd_service}')
+        return
+
+    # we need to restart the service if e.g. the VRF name changed
+    systemd_action = 'reload-or-restart'
+    if 'restart_required' in config_data:
+        systemd_action = 'restart'
+
+    call(f'systemctl {systemd_action} {systemd_service}')
+
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
-- 
cgit v1.2.3