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
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 @@
-
+
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 @@
+
+
+
+
+
+
+ Configure haproxy
+ 900
+
+
+
+
+ Frontend service name
+
+ #include
+
+ Server name must be alphanumeric and can contain hyphen and underscores
+
+
+
+
+ Backend member
+
+ #include
+
+ Backend name must be alphanumeric and can contain hyphen and underscores
+
+ txt
+ Name of haproxy backend system
+
+
+ load-balancing haproxy backend
+
+
+
+
+ #include
+ #include
+ #include
+ #include
+ #include
+ #include
+ #include
+ #include
+
+
+ Redirect HTTP to HTTPS
+
+
+
+
+
+ SSL Certificate, SSL Key and CA
+
+
+ #include
+
+
+
+
+
+
+ Backend server name
+
+ #include
+
+ Backend name must be alphanumeric and can contain hyphen and underscores
+
+
+
+
+ Load-balancing algorithm
+
+ source-address round-robin least-connection
+
+
+ source-address
+ Based on hash of source IP address
+
+
+ round-robin
+ Round robin
+
+
+ least-connection
+ Least connection
+
+
+ (source-address|round-robin|least-connection)
+
+
+ round-robin
+
+ #include
+ #include
+ #include
+ #include
+
+
+ HTTP check configuration
+
+
+
+
+ HTTP method used for health check
+
+ options head get post put
+
+
+ options|head|get|post|put
+ HTTP method used for health checking
+
+
+ (options|head|get|post|put)
+
+
+
+
+
+ URI used for HTTP health check (Example: '/' or '/health')
+
+ ^\/([^?#\s]*)(\?[^#\s]*)?$
+
+
+
+
+
+ Expected response for the health check to pass
+
+
+
+
+ Expected response status code for the health check to pass
+
+ u32:200-399
+ Expected response code
+
+
+
+
+ Status code must be in range 200-399
+
+
+
+
+ Expected to be in response body for the health check to pass
+
+ txt
+ A string expected to be in the response
+
+
+
+
+
+
+
+
+
+ Non HTTP health check options
+
+ ldap mysql pgsql redis smtp
+
+
+ ldap
+ LDAP protocol check
+
+
+ mysql
+ MySQL protocol check
+
+
+ pgsql
+ PostgreSQL protocol check
+
+
+ redis
+ Redis protocol check
+
+
+ smtp
+ SMTP protocol check
+
+
+ (ldap|mysql|redis|pgsql|smtp)
+
+
+
+ #include
+
+
+ Backend server name
+
+
+
+
+ Backend server address
+
+ ipv4
+ IPv4 unicast peer address
+
+
+ ipv6
+ IPv6 unicast peer address
+
+
+
+
+
+
+
+
+ Use backup server if other servers are not available
+
+
+
+
+
+ Active health check backend server
+
+
+
+ #include
+
+
+ Send a Proxy Protocol version 1 header (text format)
+
+
+
+
+
+ Send a Proxy Protocol version 2 header (binary format)
+
+
+
+
+
+
+
+ SSL Certificate, SSL Key and CA
+
+
+ #include
+
+
+ Do not attempt to verify SSL certificates for backend servers
+
+
+
+
+
+ #include
+
+
+
+
+ Global perfomance parameters and limits
+
+
+ #include
+
+
+ Maximum allowed connections
+
+ u32:1-2000000
+ Maximum allowed connections
+
+
+
+
+
+
+
+
+ Cipher algorithms ("cipher suite") used during SSL/TLS handshake for all frontend servers
+
+ 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
+
+
+ ecdhe-ecdsa-aes128-gcm-sha256
+ ecdhe-ecdsa-aes128-gcm-sha256
+
+
+ ecdhe-rsa-aes128-gcm-sha256
+ ecdhe-rsa-aes128-gcm-sha256
+
+
+ ecdhe-ecdsa-aes256-gcm-sha384
+ ecdhe-ecdsa-aes256-gcm-sha384
+
+
+ ecdhe-rsa-aes256-gcm-sha384
+ ecdhe-rsa-aes256-gcm-sha384
+
+
+ ecdhe-ecdsa-chacha20-poly1305
+ ecdhe-ecdsa-chacha20-poly1305
+
+
+ ecdhe-rsa-chacha20-poly1305
+ ecdhe-rsa-chacha20-poly1305
+
+
+ dhe-rsa-aes128-gcm-sha256
+ dhe-rsa-aes128-gcm-sha256
+
+
+ dhe-rsa-aes256-gcm-sha384
+ dhe-rsa-aes256-gcm-sha384
+
+
+ (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)
+
+
+
+ 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
+
+
+
+ Specify the minimum required TLS version
+
+ 1.2 1.3
+
+
+ 1.2
+ TLS v1.2
+
+
+ 1.3
+ TLS v1.3
+
+
+ (1.2|1.3)
+
+
+ 1.3
+
+
+
+ #include
+
+
+
+
+
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 @@
-
-
-
-
-
-
- Configure reverse-proxy
- 900
-
-
-
-
- Frontend service name
-
- #include
-
- Server name must be alphanumeric and can contain hyphen and underscores
-
-
-
-
- Backend member
-
- #include
-
- Backend name must be alphanumeric and can contain hyphen and underscores
-
- txt
- Name of reverse-proxy backend system
-
-
- load-balancing reverse-proxy backend
-
-
-
-
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
-
-
- Redirect HTTP to HTTPS
-
-
-
-
-
- SSL Certificate, SSL Key and CA
-
-
- #include
-
-
-
-
-
-
- Backend server name
-
- #include
-
- Backend name must be alphanumeric and can contain hyphen and underscores
-
-
-
-
- Load-balancing algorithm
-
- source-address round-robin least-connection
-
-
- source-address
- Based on hash of source IP address
-
-
- round-robin
- Round robin
-
-
- least-connection
- Least connection
-
-
- (source-address|round-robin|least-connection)
-
-
- round-robin
-
- #include
- #include
- #include
- #include
-
-
- HTTP check configuration
-
-
-
-
- HTTP method used for health check
-
- options head get post put
-
-
- options|head|get|post|put
- HTTP method used for health checking
-
-
- (options|head|get|post|put)
-
-
-
-
-
- URI used for HTTP health check (Example: '/' or '/health')
-
- ^\/([^?#\s]*)(\?[^#\s]*)?$
-
-
-
-
-
- Expected response for the health check to pass
-
-
-
-
- Expected response status code for the health check to pass
-
- u32:200-399
- Expected response code
-
-
-
-
- Status code must be in range 200-399
-
-
-
-
- Expected to be in response body for the health check to pass
-
- txt
- A string expected to be in the response
-
-
-
-
-
-
-
-
-
- Non HTTP health check options
-
- ldap mysql pgsql redis smtp
-
-
- ldap
- LDAP protocol check
-
-
- mysql
- MySQL protocol check
-
-
- pgsql
- PostgreSQL protocol check
-
-
- redis
- Redis protocol check
-
-
- smtp
- SMTP protocol check
-
-
- (ldap|mysql|redis|pgsql|smtp)
-
-
-
- #include
-
-
- Backend server name
-
-
-
-
- Backend server address
-
- ipv4
- IPv4 unicast peer address
-
-
- ipv6
- IPv6 unicast peer address
-
-
-
-
-
-
-
-
- Use backup server if other servers are not available
-
-
-
-
-
- Active health check backend server
-
-
-
- #include
-
-
- Send a Proxy Protocol version 1 header (text format)
-
-
-
-
-
- Send a Proxy Protocol version 2 header (binary format)
-
-
-
-
-
-
-
- SSL Certificate, SSL Key and CA
-
-
- #include
-
-
- Do not attempt to verify SSL certificates for backend servers
-
-
-
-
-
- #include
-
-
-
-
- Global perfomance parameters and limits
-
-
- #include
-
-
- Maximum allowed connections
-
- u32:1-2000000
- Maximum allowed connections
-
-
-
-
-
-
-
-
- Cipher algorithms ("cipher suite") used during SSL/TLS handshake for all frontend servers
-
- 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
-
-
- ecdhe-ecdsa-aes128-gcm-sha256
- ecdhe-ecdsa-aes128-gcm-sha256
-
-
- ecdhe-rsa-aes128-gcm-sha256
- ecdhe-rsa-aes128-gcm-sha256
-
-
- ecdhe-ecdsa-aes256-gcm-sha384
- ecdhe-ecdsa-aes256-gcm-sha384
-
-
- ecdhe-rsa-aes256-gcm-sha384
- ecdhe-rsa-aes256-gcm-sha384
-
-
- ecdhe-ecdsa-chacha20-poly1305
- ecdhe-ecdsa-chacha20-poly1305
-
-
- ecdhe-rsa-chacha20-poly1305
- ecdhe-rsa-chacha20-poly1305
-
-
- dhe-rsa-aes128-gcm-sha256
- dhe-rsa-aes128-gcm-sha256
-
-
- dhe-rsa-aes256-gcm-sha384
- dhe-rsa-aes256-gcm-sha384
-
-
- (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)
-
-
-
- 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
-
-
-
- Specify the minimum required TLS version
-
- 1.2 1.3
-
-
- 1.2
- TLS v1.2
-
-
- 1.3
- TLS v1.3
-
-
- (1.2|1.3)
-
-
- 1.3
-
-
-
- #include
-
-
-
-
-
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 @@
+
+
+
+
+
+
+ Restart haproxy service
+
+ sudo ${vyos_op_scripts_dir}/restart.py restart_service --name haproxy
+
+
+
+
+
+
+
+ Show load-balancing haproxy
+
+ sudo ${vyos_op_scripts_dir}/load-balacing_haproxy.py show
+
+
+
+
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 @@
-
-
-
-
-
-
- Restart reverse-proxy service
-
- sudo ${vyos_op_scripts_dir}/restart.py restart_service --name reverse_proxy
-
-
-
-
-
-
-
- Show load-balancing reverse-proxy
-
- sudo ${vyos_op_scripts_dir}/reverseproxy.py show
-
-
-
-
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 .
+
+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 .
-
-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 .
+
+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 .
-
-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
+#
+# 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 .
+
+# 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 .
+
+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 .
-
-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 7765e037b9fc9913b58a39bfa39209ebfd3b3f71 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Wed, 9 Oct 2024 16:00:15 +0200
Subject: haproxy: T6745: Add haproxy migration to config test
---
smoketest/configs/basic-vyos | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/smoketest/configs/basic-vyos b/smoketest/configs/basic-vyos
index e95d7458f..242f3d1de 100644
--- a/smoketest/configs/basic-vyos
+++ b/smoketest/configs/basic-vyos
@@ -32,6 +32,27 @@ interfaces {
loopback lo {
}
}
+load-balancing {
+ reverse-proxy {
+ backend bk-01 {
+ balance "round-robin"
+ mode "tcp"
+ server srv01 {
+ address "192.0.2.11"
+ port "8881"
+ }
+ server srv02 {
+ address "192.0.2.12"
+ port "8882"
+ }
+ }
+ service my-tcp-api {
+ backend "bk-01"
+ mode "tcp"
+ port "8888"
+ }
+ }
+}
protocols {
static {
arp 192.168.0.20 {
--
cgit v1.2.3