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 @@ -1,77 +1,77 @@ { "system_conntrack": { "conntrack_sync": ["service_conntrack-sync"], "vrf": ["vrf"] }, "firewall": { "conntrack": ["system_conntrack"], "group_resync": ["system_conntrack", "nat", "policy_route"] }, "interfaces_bonding": { "ethernet": ["interfaces_ethernet"] }, "interfaces_bridge": { "vxlan": ["interfaces_vxlan"], "wlan": ["interfaces_wireless"] }, "load_balancing_wan": { "conntrack": ["system_conntrack"] }, "nat": { "conntrack": ["system_conntrack"] }, "nat66": { "conntrack": ["system_conntrack"] }, "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"], "stunnel": ["service_stunnel"] }, "vpn_ipsec": { "nhrp": ["protocols_nhrp"] }, "vpn_l2tp": { "ipsec": ["vpn_ipsec"] }, "qos": { "bonding": ["interfaces_bonding"], "bridge": ["interfaces_bridge"], "dummy": ["interfaces_dummy"], "ethernet": ["interfaces_ethernet"], "geneve": ["interfaces_geneve"], "input": ["interfaces_input"], "l2tpv3": ["interfaces_l2tpv3"], "loopback": ["interfaces_loopback"], "macsec": ["interfaces_macsec"], "openvpn": ["interfaces_openvpn"], "pppoe": ["interfaces_pppoe"], "pseudo-ethernet": ["interfaces_pseudo-ethernet"], "tunnel": ["interfaces_tunnel"], "vti": ["interfaces_vti"], "vxlan": ["interfaces_vxlan"], "wireguard": ["interfaces_wireguard"], "wireless": ["interfaces_wireless"], "wwan": ["interfaces_wwan"] }, "system_wireless": { "wireless": ["interfaces_wireless"] }, "system_ip": { "sysctl": ["system_sysctl"] }, "system_ipv6": { "sysctl": ["system_sysctl"] }, "system_option": { "ip_ipv6": ["system_ip", "system_ipv6"], "sysctl": ["system_sysctl"] } } 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 @@ -1,35 +1,35 @@ [ "accelppp.py", "bgp.py", "bonding.py", "bridge.py", "cgnat.py", "config_mgmt.py", "conntrack.py", "container.py", "cpu.py", "dhcp.py", "dns.py", "evpn.py", "interfaces.py", "ipsec.py", "lldp.py", "log.py", "memory.py", "multicast.py", "nat.py", "neighbor.py", "nhrp.py", "openconnect.py", "openvpn.py", "otp.py", "qos.py", "reset_vpn.py", -"reverseproxy.py", +"load-balancing_haproxy.py", "route.py", "storage.py", "system.py", "uptime.py", "version.py", "vrf.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,231 +1,231 @@ -### Autogenerated by load-balancing_reverse-proxy.py ### +### Autogenerated by load-balancing_haproxy.py ### global chroot /var/lib/haproxy stats socket /run/haproxy/admin.sock mode 660 level admin stats timeout 30s user haproxy group haproxy daemon {% if global_parameters is vyos_defined %} {% if global_parameters.logging is vyos_defined %} {% for facility, facility_config in global_parameters.logging.facility.items() %} log /dev/log {{ facility }} {{ facility_config.level }} {% endfor %} {% endif %} {% if global_parameters.max_connections is vyos_defined %} maxconn {{ global_parameters.max_connections }} {% endif %} # Default SSL material locations ca-base /etc/ssl/certs crt-base /etc/ssl/private {% if global_parameters.ssl_bind_ciphers is vyos_defined %} # https://ssl-config.mozilla.org/#server=haproxy&version=2.6.12-1&config=intermediate&openssl=3.0.8-1&guideline=5.6 ssl-default-bind-ciphers {{ global_parameters.ssl_bind_ciphers | join(':') | upper }} {% endif %} ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 {% if global_parameters.tls_version_min is vyos_defined('1.3') %} ssl-default-bind-options force-tlsv13 {% else %} ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets {% endif %} {% endif %} defaults log global mode http option dontlognull timeout connect 10s timeout client 50s timeout server 50s errorfile 400 /etc/haproxy/errors/400.http errorfile 403 /etc/haproxy/errors/403.http errorfile 408 /etc/haproxy/errors/408.http errorfile 500 /etc/haproxy/errors/500.http errorfile 502 /etc/haproxy/errors/502.http errorfile 503 /etc/haproxy/errors/503.http errorfile 504 /etc/haproxy/errors/504.http # Frontend {% if service is vyos_defined %} {% for front, front_config in service.items() %} frontend {{ front }} {% set ssl_front = [] %} {% if front_config.ssl.certificate is vyos_defined and front_config.ssl.certificate is iterable %} {% for cert in front_config.ssl.certificate %} {% set _ = ssl_front.append('crt /run/haproxy/' ~ cert ~ '.pem') %} {% endfor %} {% endif %} {% set ssl_directive = 'ssl' if ssl_front else '' %} {% if front_config.listen_address is vyos_defined %} {% for address in front_config.listen_address %} bind {{ address | bracketize_ipv6 }}:{{ front_config.port }} {{ ssl_directive }} {{ ssl_front | join(' ') }} {% endfor %} {% else %} bind [::]:{{ front_config.port }} v4v6 {{ ssl_directive }} {{ ssl_front | join(' ') }} {% endif %} {% if front_config.redirect_http_to_https is vyos_defined %} http-request redirect scheme https unless { ssl_fc } {% endif %} {% if front_config.logging is vyos_defined %} {% for facility, facility_config in front_config.logging.facility.items() %} log /dev/log {{ facility }} {{ facility_config.level }} {% endfor %} {% endif %} mode {{ front_config.mode }} {% if front_config.tcp_request.inspect_delay is vyos_defined %} tcp-request inspect-delay {{ front_config.tcp_request.inspect_delay }} {% endif %} {# add tcp-request related directive if ssl is configured #} {% if front_config.mode == 'tcp' and front_config.rule is vyos_defined %} {% for rule, rule_config in front_config.rule.items() %} {% if rule_config.ssl is vyos_defined %} tcp-request content accept if { req_ssl_hello_type 1 } {% break %} {% endif %} {% endfor %} {% endif %} {% if front_config.http_response_headers is vyos_defined %} {% for header, header_config in front_config.http_response_headers.items() %} http-response set-header {{ header }} '{{ header_config['value'] }}' {% endfor %} {% endif %} {% if front_config.rule is vyos_defined %} {% for rule, rule_config in front_config.rule.items() %} # rule {{ rule }} {% if rule_config.domain_name is vyos_defined %} {% set rule_options = 'hdr(host)' %} {% if rule_config.ssl is vyos_defined %} {% set ssl_rule_translate = {'req-ssl-sni': 'req_ssl_sni', 'ssl-fc-sni': 'ssl_fc_sni', 'ssl-fc-sni-end': 'ssl_fc_sni_end'} %} {% set rule_options = ssl_rule_translate[rule_config.ssl] %} {% endif %} {% for domain in rule_config.domain_name %} acl {{ rule }} {{ rule_options }} -i {{ domain }} {% endfor %} {% endif %} {# path url #} {% if rule_config.url_path is vyos_defined %} {% set path_mod_translate = {'begin': '-i -m beg', 'end': '-i -m end', 'exact': ''} %} {% for path, path_config in rule_config.url_path.items() %} {% for url in path_config %} acl {{ rule }} path {{ path_mod_translate[path] }} {{ url }} {% endfor %} {% endfor %} {% endif %} {% if rule_config.set.backend is vyos_defined %} use_backend {{ rule_config.set.backend }} if {{ rule }} {% endif %} {% if rule_config.set.redirect_location is vyos_defined %} http-request redirect location {{ rule_config.set.redirect_location }} code 301 if {{ rule }} {% endif %} {# endpath #} {% endfor %} {% endif %} {% if front_config.backend is vyos_defined %} {% for backend in front_config.backend %} default_backend {{ backend }} {% endfor %} {% endif %} {% endfor %} {% endif %} # Backend {% if backend is vyos_defined %} {% for back, back_config in backend.items() %} backend {{ back }} {% if back_config.health_check is vyos_defined %} {% if back_config.health_check == 'smtp' %} option smtpchk {% else %} option {{ back_config.health_check }}-check {% endif %} {% endif %} {% if back_config.http_check is vyos_defined %} option httpchk {% endif %} {% set send = '' %} {% if back_config.http_check.method is vyos_defined %} {% set send = send + ' meth ' + back_config.http_check.method | upper %} {% endif %} {% if back_config.http_check.uri is vyos_defined %} {% set send = send + ' uri ' + back_config.http_check.uri %} {% endif %} {% if send != '' %} http-check send{{ send }} {% endif %} {% if back_config.http_check.expect is vyos_defined %} {% if back_config.http_check.expect.status is vyos_defined %} http-check expect status {{ back_config.http_check.expect.status }} {% elif back_config.http_check.expect.string is vyos_defined %} http-check expect string {{ back_config.http_check.expect.string }} {% endif %} {% endif %} {% if back_config.balance is vyos_defined %} {% set balance_translate = {'least-connection': 'leastconn', 'round-robin': 'roundrobin', 'source-address': 'source'} %} balance {{ balance_translate[back_config.balance] }} {% endif %} {# If mode is HTTP add X-Forwarded headers #} {% if back_config.mode == 'http' %} option forwardfor http-request set-header X-Forwarded-Port %[dst_port] http-request add-header X-Forwarded-Proto https if { ssl_fc } {% endif %} {% if back_config.logging is vyos_defined %} {% for facility, facility_config in back_config.logging.facility.items() %} log /dev/log {{ facility }} {{ facility_config.level }} {% endfor %} {% endif %} mode {{ back_config.mode }} {% if back_config.http_response_headers is vyos_defined %} {% for header, header_config in back_config.http_response_headers.items() %} http-response set-header {{ header }} '{{ header_config['value'] }}' {% endfor %} {% endif %} {% if back_config.rule is vyos_defined %} {% for rule, rule_config in back_config.rule.items() %} {% if rule_config.domain_name is vyos_defined and rule_config.set.server is vyos_defined %} {% set rule_options = 'hdr(host)' %} {% if rule_config.ssl is vyos_defined %} {% set ssl_rule_translate = {'req-ssl-sni': 'req_ssl_sni', 'ssl-fc-sni': 'ssl_fc_sni', 'ssl-fc-sni-end': 'ssl_fc_sni_end'} %} {% set rule_options = ssl_rule_translate[rule_config.ssl] %} {% endif %} {% for domain in rule_config.domain_name %} acl {{ rule }} {{ rule_options }} -i {{ domain }} {% endfor %} use-server {{ rule_config.set.server }} if {{ rule }} {% endif %} {# path url #} {% if rule_config.url_path is vyos_defined and rule_config.set.redirect_location is vyos_defined %} {% set path_mod_translate = {'begin': '-i -m beg', 'end': '-i -m end', 'exact': ''} %} {% for path, path_config in rule_config.url_path.items() %} {% for url in path_config %} acl {{ rule }} path {{ path_mod_translate[path] }} {{ url }} {% endfor %} {% endfor %} http-request redirect location {{ rule_config.set.redirect_location }} code 301 if {{ rule }} {% endif %} {# endpath #} {% endfor %} {% endif %} {% if back_config.server is vyos_defined %} {% set ssl_back = 'ssl ca-file /run/haproxy/' ~ back_config.ssl.ca_certificate ~ '.pem' if back_config.ssl.ca_certificate is vyos_defined else ('ssl verify none' if back_config.ssl.no_verify is vyos_defined else '') %} {% for server, server_config in back_config.server.items() %} server {{ server }} {{ server_config.address }}:{{ server_config.port }}{{ ' check' if server_config.check is vyos_defined }}{{ ' backup' if server_config.backup is vyos_defined }}{{ ' send-proxy' if server_config.send_proxy is vyos_defined }}{{ ' send-proxy-v2' if server_config.send_proxy_v2 is vyos_defined }} {{ ssl_back }} {% endfor %} {% endif %} {% if back_config.timeout.check is vyos_defined %} timeout check {{ back_config.timeout.check }}s {% endif %} {% if back_config.timeout.connect is vyos_defined %} timeout connect {{ back_config.timeout.connect }}s {% endif %} {% if back_config.timeout.server is vyos_defined %} timeout server {{ back_config.timeout.server }}s {% endif %} {% endfor %} {% endif %} diff --git a/debian/control b/debian/control index 15fb5d72e..20cfcdc43 100644 --- a/debian/control +++ b/debian/control @@ -1,387 +1,387 @@ Source: vyos-1x Section: contrib/net Priority: extra Maintainer: VyOS Package Maintainers <maintainers@vyos.net> Build-Depends: debhelper (>= 9), dh-python, fakeroot, gcc, iproute2, libvyosconfig0 (>= 0.0.7), libzmq3-dev, python3 (>= 3.10), # For QA pylint, # For generating command definitions python3-lxml, python3-xmltodict, # For running tests python3-coverage, python3-hurry.filesize, python3-netaddr, python3-netifaces, python3-nose, python3-jinja2, python3-paramiko, python3-passlib, python3-psutil, python3-requests, python3-setuptools, python3-tabulate, python3-zmq, quilt, whois Standards-Version: 3.9.6 Package: vyos-1x Architecture: amd64 arm64 Pre-Depends: libpam-runtime [amd64], libnss-tacplus [amd64], libpam-tacplus [amd64], libpam-radius-auth [amd64] Depends: ## Fundamentals ${python3:Depends} (>= 3.10), dialog, libvyosconfig0, libpam-cap, bash-completion, ipvsadm, udev, less, at, rsync, vyatta-bash, vyatta-biosdevname, vyatta-cfg, vyos-http-api-tools, vyos-utils, ## End of Fundamentals ## Python libraries used in multiple modules and scripts python3, python3-cryptography, python3-hurry.filesize, python3-inotify, python3-jinja2, python3-jmespath, python3-netaddr, python3-netifaces, python3-paramiko, python3-passlib, python3-pyroute2, python3-psutil, python3-pyhumps, python3-pystache, python3-pyudev, python3-six, python3-tabulate, python3-voluptuous, python3-xmltodict, python3-zmq, ## End of Python libraries ## Basic System services and utilities coreutils, sudo, systemd, bsdmainutils, openssl, curl, dbus, file, iproute2 (>= 6.0.0), linux-cpupower, # ipaddrcheck is widely used in IP value validators ipaddrcheck, ethtool (>= 6.10), lm-sensors, procps, netplug, sed, ssl-cert, tuned, beep, wide-dhcpv6-client, # Generic colorizer grc, ## End of System services and utilities ## For the installer fdisk, gdisk, mdadm, efibootmgr, libefivar1, dosfstools, grub-efi-amd64-signed [amd64], grub-efi-arm64-bin [arm64], mokutil [amd64], shim-signed [amd64], sbsigntool [amd64], # Image signature verification tool minisign, # Live filesystem tools squashfs-tools, fuse-overlayfs, ## End installer auditd, iputils-arping, iputils-ping, isc-dhcp-client, # For "vpn pptp", "vpn l2tp", "vpn sstp", "service ipoe-server" accel-ppp, # End "vpn pptp", "vpn l2tp", "vpn sstp", "service ipoe-server" avahi-daemon, conntrack, conntrackd, ## Conf mode features # For "interfaces wireless" hostapd, hsflowd, iw, wireless-regdb, wpasupplicant (>= 0.6.7), # End "interfaces wireless" # For "interfaces wwan" modemmanager, usb-modeswitch, libqmi-utils, # End "interfaces wwan" # For "interfaces openvpn" openvpn, openvpn-auth-ldap, openvpn-auth-radius, openvpn-otp, openvpn-dco, libpam-google-authenticator, # End "interfaces openvpn" # For "interfaces wireguard" wireguard-tools, qrencode, # End "interfaces wireguard" # For "interfaces pppoe" pppoe, # End "interfaces pppoe" # For "interfaces sstpc" sstp-client, # End "interfaces sstpc" # For "protocols *" frr (>= 9.1), frr-pythontools, frr-rpki-rtrlib, frr-snmp, # End "protocols *" # For "protocols nhrp" (part of DMVPN) opennhrp, # End "protocols nhrp" # For "protocols igmp-proxy" igmpproxy, # End "protocols igmp-proxy" # For "pki" certbot, # End "pki" # For "service console-server" conserver-client, conserver-server, console-data, dropbear, # End "service console-server" # For "service aws glb" aws-gwlbtun, # For "service dns dynamic" ddclient (>= 3.11.1), # End "service dns dynamic" # # For "service ids" fastnetmon [amd64], suricata, suricata-update, # End "service ids" # # For "service ndp-proxy" ndppd, # End "service ndp-proxy" # 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" # For "service dhcp-relay" isc-dhcp-relay, # For "service dhcp-server" kea, # End "service dhcp-server" # For "service lldp" lldpd, # End "service lldp" # For "service https" nginx-light, # End "service https" # For "service ssh" openssh-server, sshguard, # End "service ssh" # For "service salt-minion" salt-minion, # End "service salt-minion" # For "service snmp" snmp, snmpd, # End "service snmp" # For "service webproxy" squid, squidclient, squidguard, # End "service webproxy" # For "service monitoring node-exporter" node-exporter, # End "service monitoring node-exporter" # For "service monitoring telegraf" telegraf (>= 1.20), # End "service monitoring telegraf" # For "service monitoring zabbix-agent" zabbix-agent2, # End "service monitoring zabbix-agent" # For "service tftp-server" tftpd-hpa, # End "service tftp-server" # For "service dns forwarding" pdns-recursor, # End "service dns forwarding" # For "service sla owamp" owamp-client, owamp-server, # End "service sla owamp" # For "service sla twamp" twamp-client, twamp-server, # End "service sla twamp" # For "service broadcast-relay" udp-broadcast-relay, # End "service broadcast-relay" # For "high-availability vrrp" keepalived (>=2.0.5), # End "high-availability-vrrp" # For "system console" util-linux, # End "system console" # For "system task-scheduler" cron, # End "system task-scheduler" # For "system lcd" lcdproc, lcdproc-extra-drivers, # End "system lcd" # For "system config-management commit-archive" git, # End "system config-management commit-archive" # For firewall libndp-tools, libnetfilter-conntrack3, libnfnetlink0, nfct, nftables (>= 0.9.3), # For "vpn ipsec" strongswan (>= 5.9), strongswan-swanctl (>= 5.9), charon-systemd, libcharon-extra-plugins (>=5.9), libcharon-extauth-plugins (>=5.9), libstrongswan-extra-plugins (>=5.9), libstrongswan-standard-plugins (>=5.9), python3-vici (>= 5.7.2), # End "vpn ipsec" # For "nat64" jool, # End "nat64" # For "system conntrack modules rtsp" nat-rtsp, # End "system conntrack modules rtsp" # For "service ntp" chrony, # End "system ntp" # For "vpn openconnect" ocserv, # End "vpn openconnect" # For "system flow-accounting" pmacct (>= 1.6.0), # End "system flow-accounting" # For "system syslog" rsyslog, # End "system syslog" # For "system option keyboard-layout" kbd, # End "system option keyboard-layout" # For "container" podman (>=4.9.5), netavark, aardvark-dns, # iptables is only used for containers now, not the the firewall CLI iptables, # End container ## End Configuration mode ## Operational mode # Used for hypervisor model in "run show version" hvinfo, # For "run traceroute" traceroute, # For "run monitor traffic" tcpdump, # End "run monitor traffic" # For "show hardware dmi" dmidecode, # For "run show hardware storage smart" smartmontools, # For "run show hardware scsi" lsscsi, # For "run show hardware pci" pciutils, # For "show hardware usb" usbutils, # For "run show hardware storage nvme" nvme-cli, # For "run monitor bandwidth-test" iperf, iperf3, # End "run monitor bandwidth-test" # For "run wake-on-lan" etherwake, # For "run force ipv6-nd" ndisc6, # For "run monitor bandwidth" bmon, # For "run format disk" parted, # End Operational mode ## TPM tools cryptsetup, tpm2-tools, ## End TPM tools ## Optional utilities easy-rsa, tcptraceroute, mtr-tiny, telnet, stunnel4, uidmap ## End optional utilities Description: VyOS configuration scripts and data VyOS configuration scripts, interface definitions, and everything Package: vyos-1x-vmware Architecture: amd64 Depends: vyos-1x, open-vm-tools Description: VyOS configuration scripts and data for VMware Adds configuration files required for VyOS running on VMware hosts. Package: vyos-1x-smoketest Architecture: all Depends: skopeo, snmp, vyos-1x Description: VyOS build sanity checking toolkit 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_reverse-proxy.xml.in b/interface-definitions/load-balancing_haproxy.xml.in similarity index 98% rename from interface-definitions/load-balancing_reverse-proxy.xml.in rename to interface-definitions/load-balancing_haproxy.xml.in index 18274622c..742272436 100644 --- a/interface-definitions/load-balancing_reverse-proxy.xml.in +++ b/interface-definitions/load-balancing_haproxy.xml.in @@ -1,344 +1,344 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="load-balancing"> <children> - <node name="reverse-proxy" owner="${vyos_conf_scripts_dir}/load-balancing_reverse-proxy.py"> + <node name="haproxy" owner="${vyos_conf_scripts_dir}/load-balancing_haproxy.py"> <properties> - <help>Configure reverse-proxy</help> + <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 reverse-proxy backend system</description> + <description>Name of haproxy backend system</description> </valueHelp> <completionHelp> - <path>load-balancing reverse-proxy backend</path> + <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/op-mode-definitions/reverse-proxy.xml.in b/op-mode-definitions/load-balacing_haproxy.in similarity index 55% rename from op-mode-definitions/reverse-proxy.xml.in rename to op-mode-definitions/load-balacing_haproxy.in index b45ce107f..c3d6c799b 100644 --- a/op-mode-definitions/reverse-proxy.xml.in +++ b/op-mode-definitions/load-balacing_haproxy.in @@ -1,23 +1,23 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="restart"> <children> - <node name="reverse-proxy"> + <node name="haproxy"> <properties> - <help>Restart reverse-proxy service</help> + <help>Restart haproxy service</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/restart.py restart_service --name reverse_proxy</command> + <command>sudo ${vyos_op_scripts_dir}/restart.py restart_service --name haproxy</command> </node> </children> </node> <node name="show"> <children> - <node name="reverse-proxy"> + <node name="haproxy"> <properties> - <help>Show load-balancing reverse-proxy</help> + <help>Show load-balancing haproxy</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/reverseproxy.py show</command> + <command>sudo ${vyos_op_scripts_dir}/load-balacing_haproxy.py show</command> </node> </children> </node> </interfaceDefinition> diff --git a/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py b/smoketest/scripts/cli/test_load-balancing_haproxy.py similarity index 99% rename from smoketest/scripts/cli/test_load-balancing_reverse-proxy.py rename to smoketest/scripts/cli/test_load-balancing_haproxy.py index 34f77b95d..967eb3869 100755 --- a/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py +++ b/smoketest/scripts/cli/test_load-balancing_haproxy.py @@ -1,502 +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', 'reverse-proxy'] +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/src/conf_mode/load-balancing_reverse-proxy.py b/src/conf_mode/load-balancing_haproxy.py old mode 100755 new mode 100644 similarity index 99% rename from src/conf_mode/load-balancing_reverse-proxy.py rename to src/conf_mode/load-balancing_haproxy.py index 17226efe9..45042dd52 --- a/src/conf_mode/load-balancing_reverse-proxy.py +++ b/src/conf_mode/load-balancing_haproxy.py @@ -1,206 +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', 'reverse-proxy'] + 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/pki.py b/src/conf_mode/pki.py index 233d73ba8..45e0129a3 100755 --- a/src/conf_mode/pki.py +++ b/src/conf_mode/pki.py @@ -1,517 +1,517 @@ #!/usr/bin/env python3 # # Copyright (C) 2021-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 argv from sys import exit from vyos.config import Config from vyos.config import config_dict_merge from vyos.configdep import set_dependents from vyos.configdep import call_dependents from vyos.configdict import node_changed from vyos.configdiff import Diff from vyos.configdiff import get_config_diff from vyos.defaults import directories from vyos.pki import encode_certificate from vyos.pki import is_ca_certificate from vyos.pki import load_certificate from vyos.pki import load_public_key from vyos.pki import load_openssh_public_key from vyos.pki import load_openssh_private_key from vyos.pki import load_private_key from vyos.pki import load_crl from vyos.pki import load_dh_parameters from vyos.utils.boot import boot_configuration_complete from vyos.utils.configfs import add_cli_node from vyos.utils.dict import dict_search from vyos.utils.dict import dict_search_args from vyos.utils.dict import dict_search_recursive from vyos.utils.file import read_file from vyos.utils.process import call from vyos.utils.process import cmd from vyos.utils.process import is_systemd_service_active from vyos import ConfigError from vyos import airbag airbag.enable() vyos_certbot_dir = directories['certbot'] # keys to recursively search for under specified path sync_search = [ { 'keys': ['certificate'], 'path': ['service', 'https'], }, { 'keys': ['certificate', 'ca_certificate'], 'path': ['interfaces', 'ethernet'], }, { 'keys': ['certificate', 'ca_certificate', 'dh_params', 'shared_secret_key', 'auth_key', 'crypt_key'], 'path': ['interfaces', 'openvpn'], }, { 'keys': ['ca_certificate'], 'path': ['interfaces', 'sstpc'], }, { 'keys': ['certificate', 'ca_certificate'], - 'path': ['load_balancing', 'reverse_proxy'], + 'path': ['load_balancing', 'haproxy'], }, { 'keys': ['key'], 'path': ['protocols', 'rpki', 'cache'], }, { 'keys': ['certificate', 'ca_certificate', 'local_key', 'remote_key'], 'path': ['vpn', 'ipsec'], }, { 'keys': ['certificate', 'ca_certificate'], 'path': ['vpn', 'openconnect'], }, { 'keys': ['certificate', 'ca_certificate'], 'path': ['vpn', 'sstp'], }, { 'keys': ['certificate', 'ca_certificate'], 'path': ['service', 'stunnel'], } ] # key from other config nodes -> key in pki['changed'] and pki sync_translate = { 'certificate': 'certificate', 'ca_certificate': 'ca', 'dh_params': 'dh', 'local_key': 'key_pair', 'remote_key': 'key_pair', 'shared_secret_key': 'openvpn', 'auth_key': 'openvpn', 'crypt_key': 'openvpn', 'key': 'openssh', } def certbot_delete(certificate): if not boot_configuration_complete(): return if os.path.exists(f'{vyos_certbot_dir}/renewal/{certificate}.conf'): cmd(f'certbot delete --non-interactive --config-dir {vyos_certbot_dir} --cert-name {certificate}') def certbot_request(name: str, config: dict, dry_run: bool=True): # We do not call certbot when booting the system - there is no need to do so and # request new certificates during boot/image upgrade as the certbot configuration # is stored persistent under /config - thus we do not open the door to transient # errors if not boot_configuration_complete(): return domains = '--domains ' + ' --domains '.join(config['domain_name']) tmp = f'certbot certonly --non-interactive --config-dir {vyos_certbot_dir} --cert-name {name} '\ f'--standalone --agree-tos --no-eff-email --expand --server {config["url"]} '\ f'--email {config["email"]} --key-type rsa --rsa-key-size {config["rsa_key_size"]} '\ f'{domains}' if 'listen_address' in config: tmp += f' --http-01-address {config["listen_address"]}' # verify() does not need to actually request a cert but only test for plausability if dry_run: tmp += ' --dry-run' cmd(tmp, raising=ConfigError, message=f'ACME certbot request failed for "{name}"!') def get_config(config=None): if config: conf = config else: conf = Config() base = ['pki'] pki = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) if len(argv) > 1 and argv[1] == 'certbot_renew': pki['certbot_renew'] = {} tmp = node_changed(conf, base + ['ca'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) if tmp: if 'changed' not in pki: pki.update({'changed':{}}) pki['changed'].update({'ca' : tmp}) tmp = node_changed(conf, base + ['certificate'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) if tmp: if 'changed' not in pki: pki.update({'changed':{}}) pki['changed'].update({'certificate' : tmp}) tmp = node_changed(conf, base + ['dh'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) if tmp: if 'changed' not in pki: pki.update({'changed':{}}) pki['changed'].update({'dh' : tmp}) tmp = node_changed(conf, base + ['key-pair'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) if tmp: if 'changed' not in pki: pki.update({'changed':{}}) pki['changed'].update({'key_pair' : tmp}) tmp = node_changed(conf, base + ['openssh'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) if tmp: if 'changed' not in pki: pki.update({'changed':{}}) pki['changed'].update({'openssh' : tmp}) tmp = node_changed(conf, base + ['openvpn', 'shared-secret'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) if tmp: if 'changed' not in pki: pki.update({'changed':{}}) pki['changed'].update({'openvpn' : tmp}) # We only merge on the defaults of there is a configuration at all if conf.exists(base): # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. default_values = conf.get_config_defaults(**pki.kwargs, recursive=True) # remove ACME default configuration if unused by CLI if 'certificate' in pki: for name, cert_config in pki['certificate'].items(): if 'acme' not in cert_config: # Remove ACME default values del default_values['certificate'][name]['acme'] # merge CLI and default dictionary pki = config_dict_merge(default_values, pki) # Certbot triggered an external renew of the certificates. # Mark all ACME based certificates as "changed" to trigger # update of dependent services if 'certificate' in pki and 'certbot_renew' in pki: renew = [] for name, cert_config in pki['certificate'].items(): if 'acme' in cert_config: renew.append(name) # If triggered externally by certbot, certificate key is not present in changed if 'changed' not in pki: pki.update({'changed':{}}) pki['changed'].update({'certificate' : renew}) # We need to get the entire system configuration to verify that we are not # deleting a certificate that is still referenced somewhere! pki['system'] = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) D = get_config_diff(conf) for search in sync_search: for key in search['keys']: changed_key = sync_translate[key] if 'changed' not in pki or changed_key not in pki['changed']: continue for item_name in pki['changed'][changed_key]: node_present = False if changed_key == 'openvpn': node_present = dict_search_args(pki, 'openvpn', 'shared_secret', item_name) else: node_present = dict_search_args(pki, changed_key, item_name) if node_present: search_dict = dict_search_args(pki['system'], *search['path']) if not search_dict: continue for found_name, found_path in dict_search_recursive(search_dict, key): if isinstance(found_name, list) and item_name not in found_name: continue if isinstance(found_name, str) and found_name != item_name: continue path = search['path'] path_str = ' '.join(path + found_path) #print(f'PKI: Updating config: {path_str} {item_name}') if path[0] == 'interfaces': ifname = found_path[0] if not D.node_changed_presence(path + [ifname]): set_dependents(path[1], conf, ifname) else: if not D.node_changed_presence(path): set_dependents(path[1], conf) return pki def is_valid_certificate(raw_data): # If it loads correctly we're good, or return False return load_certificate(raw_data, wrap_tags=True) def is_valid_ca_certificate(raw_data): # Check if this is a valid certificate with CA attributes cert = load_certificate(raw_data, wrap_tags=True) if not cert: return False return is_ca_certificate(cert) def is_valid_public_key(raw_data): # If it loads correctly we're good, or return False return load_public_key(raw_data, wrap_tags=True) def is_valid_private_key(raw_data, protected=False): # If it loads correctly we're good, or return False # With encrypted private keys, we always return true as we cannot ask for password to verify if protected: return True return load_private_key(raw_data, passphrase=None, wrap_tags=True) def is_valid_openssh_public_key(raw_data, type): # If it loads correctly we're good, or return False return load_openssh_public_key(raw_data, type) def is_valid_openssh_private_key(raw_data, protected=False): # If it loads correctly we're good, or return False # With encrypted private keys, we always return true as we cannot ask for password to verify if protected: return True return load_openssh_private_key(raw_data, passphrase=None, wrap_tags=True) def is_valid_crl(raw_data): # If it loads correctly we're good, or return False return load_crl(raw_data, wrap_tags=True) def is_valid_dh_parameters(raw_data): # If it loads correctly we're good, or return False return load_dh_parameters(raw_data, wrap_tags=True) def verify(pki): if not pki: return None if 'ca' in pki: for name, ca_conf in pki['ca'].items(): if 'certificate' in ca_conf: if not is_valid_ca_certificate(ca_conf['certificate']): raise ConfigError(f'Invalid certificate on CA certificate "{name}"') if 'private' in ca_conf and 'key' in ca_conf['private']: private = ca_conf['private'] protected = 'password_protected' in private if not is_valid_private_key(private['key'], protected): raise ConfigError(f'Invalid private key on CA certificate "{name}"') if 'crl' in ca_conf: ca_crls = ca_conf['crl'] if isinstance(ca_crls, str): ca_crls = [ca_crls] for crl in ca_crls: if not is_valid_crl(crl): raise ConfigError(f'Invalid CRL on CA certificate "{name}"') if 'certificate' in pki: for name, cert_conf in pki['certificate'].items(): if 'certificate' in cert_conf: if not is_valid_certificate(cert_conf['certificate']): raise ConfigError(f'Invalid certificate on certificate "{name}"') if 'private' in cert_conf and 'key' in cert_conf['private']: private = cert_conf['private'] protected = 'password_protected' in private if not is_valid_private_key(private['key'], protected): raise ConfigError(f'Invalid private key on certificate "{name}"') if 'acme' in cert_conf: if 'domain_name' not in cert_conf['acme']: raise ConfigError(f'At least one domain-name is required to request '\ f'certificate for "{name}" via ACME!') if 'email' not in cert_conf['acme']: raise ConfigError(f'An email address is required to request '\ f'certificate for "{name}" via ACME!') if 'certbot_renew' not in pki: # Only run the ACME command if something on this entity changed, # as this is time intensive tmp = dict_search('changed.certificate', pki) if tmp != None and name in tmp: certbot_request(name, cert_conf['acme']) if 'dh' in pki: for name, dh_conf in pki['dh'].items(): if 'parameters' in dh_conf: if not is_valid_dh_parameters(dh_conf['parameters']): raise ConfigError(f'Invalid DH parameters on "{name}"') if 'key_pair' in pki: for name, key_conf in pki['key_pair'].items(): if 'public' in key_conf and 'key' in key_conf['public']: if not is_valid_public_key(key_conf['public']['key']): raise ConfigError(f'Invalid public key on key-pair "{name}"') if 'private' in key_conf and 'key' in key_conf['private']: private = key_conf['private'] protected = 'password_protected' in private if not is_valid_private_key(private['key'], protected): raise ConfigError(f'Invalid private key on key-pair "{name}"') if 'openssh' in pki: for name, key_conf in pki['openssh'].items(): if 'public' in key_conf and 'key' in key_conf['public']: if 'type' not in key_conf['public']: raise ConfigError(f'Must define OpenSSH public key type for "{name}"') if not is_valid_openssh_public_key(key_conf['public']['key'], key_conf['public']['type']): raise ConfigError(f'Invalid OpenSSH public key "{name}"') if 'private' in key_conf and 'key' in key_conf['private']: private = key_conf['private'] protected = 'password_protected' in private if not is_valid_openssh_private_key(private['key'], protected): raise ConfigError(f'Invalid OpenSSH private key "{name}"') if 'x509' in pki: if 'default' in pki['x509']: default_values = pki['x509']['default'] if 'country' in default_values: country = default_values['country'] if len(country) != 2 or not country.isalpha(): raise ConfigError(f'Invalid default country value. Value must be 2 alpha characters.') if 'changed' in pki: # if the list is getting longer, we can move to a dict() and also embed the # search key as value from line 173 or 176 for search in sync_search: for key in search['keys']: changed_key = sync_translate[key] if changed_key not in pki['changed']: continue for item_name in pki['changed'][changed_key]: node_present = False if changed_key == 'openvpn': node_present = dict_search_args(pki, 'openvpn', 'shared_secret', item_name) else: node_present = dict_search_args(pki, changed_key, item_name) if not node_present: search_dict = dict_search_args(pki['system'], *search['path']) if not search_dict: continue for found_name, found_path in dict_search_recursive(search_dict, key): if found_name == item_name: path_str = " ".join(search['path'] + found_path) raise ConfigError(f'PKI object "{item_name}" still in use by "{path_str}"') return None def generate(pki): if not pki: return None # Certbot renewal only needs to re-trigger the services to load up the # new PEM file if 'certbot_renew' in pki: return None certbot_list = [] certbot_list_on_disk = [] if os.path.exists(f'{vyos_certbot_dir}/live'): certbot_list_on_disk = [f.path.split('/')[-1] for f in os.scandir(f'{vyos_certbot_dir}/live') if f.is_dir()] if 'certificate' in pki: changed_certificates = dict_search('changed.certificate', pki) for name, cert_conf in pki['certificate'].items(): if 'acme' in cert_conf: certbot_list.append(name) # generate certificate if not found on disk if name not in certbot_list_on_disk: certbot_request(name, cert_conf['acme'], dry_run=False) elif changed_certificates != None and name in changed_certificates: # when something for the certificate changed, we should delete it if name in certbot_list_on_disk: certbot_delete(name) certbot_request(name, cert_conf['acme'], dry_run=False) # Cleanup certbot configuration and certificates if no longer in use by CLI # Get foldernames under vyos_certbot_dir which each represent a certbot cert if os.path.exists(f'{vyos_certbot_dir}/live'): for cert in certbot_list_on_disk: # ACME certificate is no longer in use by CLI remove it if cert not in certbot_list: certbot_delete(cert) continue # ACME not enabled for individual certificate - bail out early if 'acme' not in pki['certificate'][cert]: continue # Read in ACME certificate chain information tmp = read_file(f'{vyos_certbot_dir}/live/{cert}/chain.pem') tmp = load_certificate(tmp, wrap_tags=False) cert_chain_base64 = "".join(encode_certificate(tmp).strip().split("\n")[1:-1]) # Check if CA chain certificate is already present on CLI to avoid adding # a duplicate. This only checks for manual added CA certificates and not # auto added ones with the AUTOCHAIN_ prefix autochain_prefix = 'AUTOCHAIN_' ca_cert_present = False if 'ca' in pki: for ca_base64, cli_path in dict_search_recursive(pki['ca'], 'certificate'): # Ignore automatic added CA certificates if any(item.startswith(autochain_prefix) for item in cli_path): continue if cert_chain_base64 == ca_base64: ca_cert_present = True if not ca_cert_present: tmp = dict_search_args(pki, 'ca', f'{autochain_prefix}{cert}', 'certificate') if not bool(tmp) or tmp != cert_chain_base64: print(f'Adding/replacing automatically imported CA certificate for "{cert}" ...') add_cli_node(['pki', 'ca', f'{autochain_prefix}{cert}', 'certificate'], value=cert_chain_base64) return None def apply(pki): systemd_certbot_name = 'certbot.timer' if not pki: call(f'systemctl stop {systemd_certbot_name}') return None has_certbot = False if 'certificate' in pki: for name, cert_conf in pki['certificate'].items(): if 'acme' in cert_conf: has_certbot = True break if not has_certbot: call(f'systemctl stop {systemd_certbot_name}') elif has_certbot and not is_systemd_service_active(systemd_certbot_name): call(f'systemctl restart {systemd_certbot_name}') if 'changed' in pki: call_dependents() 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/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/reverseproxy.py b/src/op_mode/load-balancing_haproxy.py similarity index 97% rename from src/op_mode/reverseproxy.py rename to src/op_mode/load-balancing_haproxy.py index 19704182a..ae6734e16 100755 --- a/src/op_mode/reverseproxy.py +++ b/src/op_mode/load-balancing_haproxy.py @@ -1,237 +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 reverse-proxy'): - raise vyos.opmode.UnconfiguredSubsystem('Reverse-proxy is not configured') + 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 @@ -1,149 +1,149 @@ #!/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 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 sys import typing import vyos.opmode from vyos.configquery import ConfigTreeQuery from vyos.utils.process import call from vyos.utils.commit import commit_in_progress config = ConfigTreeQuery() service_map = { 'dhcp': { 'systemd_service': 'kea-dhcp4-server', 'path': ['service', 'dhcp-server'], }, 'dhcpv6': { 'systemd_service': 'kea-dhcp6-server', 'path': ['service', 'dhcpv6-server'], }, 'dns_dynamic': { 'systemd_service': 'ddclient', 'path': ['service', 'dns', 'dynamic'], }, 'dns_forwarding': { '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'], }, 'ipsec': { 'systemd_service': 'strongswan', 'path': ['vpn', 'ipsec'], }, 'mdns_repeater': { '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'], }, 'snmp': { 'systemd_service': 'snmpd', }, 'ssh': { 'systemd_service': 'ssh', }, 'suricata': { 'systemd_service': 'suricata', }, 'vrrp': { 'systemd_service': 'keepalived', 'path': ['high-availability', 'vrrp'], }, 'webproxy': { 'systemd_service': 'squid', }, } services = typing.Literal[ 'dhcp', 'dhcpv6', 'dns_dynamic', 'dns_forwarding', + 'haproxy', 'igmp_proxy', 'ipsec', 'mdns_repeater', - 'reverse_proxy', 'router_advert', 'snmp', 'ssh', 'suricata', 'vrrp', 'webproxy', ] def _verify(func): """Decorator checks if DHCP(v6) config exists""" from functools import wraps @wraps(func) def _wrapper(*args, **kwargs): config = ConfigTreeQuery() name = kwargs.get('name') human_name = name.replace('_', '-') if commit_in_progress(): print(f'Cannot restart {human_name} service while a commit is in progress') sys.exit(1) # Get optional CLI path from service_mapping dict # otherwise use "service name" CLI path path = ['service', name] if 'path' in service_map[name]: path = service_map[name]['path'] # Check if config does not exist if not config.exists(path): raise vyos.opmode.UnconfiguredSubsystem( f'Service {human_name} is not configured!' ) if config.exists(path + ['disable']): raise vyos.opmode.UnconfiguredSubsystem( f'Service {human_name} is disabled!' ) return func(*args, **kwargs) return _wrapper @_verify def restart_service(raw: bool, name: services, vrf: typing.Optional[str]): systemd_service = service_map[name]['systemd_service'] if vrf: call(f'systemctl restart "{systemd_service}@{vrf}.service"') else: call(f'systemctl restart "{systemd_service}.service"') 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)