diff options
| -rw-r--r-- | interface-definitions/service_config-sync.xml.in | 6 | ||||
| -rw-r--r-- | python/vyos/configdict.py | 4 | ||||
| -rw-r--r-- | python/vyos/configsession.py | 19 | ||||
| -rw-r--r-- | python/vyos/configtree.py | 24 | ||||
| -rw-r--r-- | python/vyos/template.py | 2 | ||||
| -rw-r--r-- | python/vyos/utils/network.py | 4 | ||||
| -rw-r--r-- | python/vyos/utils/system.py | 7 | ||||
| -rw-r--r-- | smoketest/scripts/cli/base_accel_ppp_test.py | 4 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_load-balancing_reverse-proxy.py | 171 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_service_dhcp-server.py | 25 | ||||
| -rwxr-xr-x | src/conf_mode/load-balancing_reverse-proxy.py | 32 | ||||
| -rwxr-xr-x | src/conf_mode/service_dhcp-server.py | 2 | ||||
| -rwxr-xr-x | src/helpers/vyos_config_sync.py | 66 | ||||
| -rwxr-xr-x | src/services/vyos-http-api-server | 38 | 
14 files changed, 348 insertions, 56 deletions
| diff --git a/interface-definitions/service_config-sync.xml.in b/interface-definitions/service_config-sync.xml.in index cb51a33b1..e9ea9aa4b 100644 --- a/interface-definitions/service_config-sync.xml.in +++ b/interface-definitions/service_config-sync.xml.in @@ -495,6 +495,12 @@                        <valueless/>                      </properties>                    </leafNode> +                  <leafNode name="time-zone"> +                    <properties> +                      <help>Local time zone</help> +                      <valueless/> +                    </properties> +                  </leafNode>                  </children>                </node>                <leafNode name="vpn"> diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 4111d7271..cb9f0cbb8 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -633,7 +633,7 @@ def get_accel_dict(config, base, chap_secrets, with_pki=False):      Return a dictionary with the necessary interface config keys.      """ -    from vyos.utils.system import get_half_cpus +    from vyos.cpu import get_core_count      from vyos.template import is_ipv4      dict = config.get_config_dict(base, key_mangling=('-', '_'), @@ -643,7 +643,7 @@ def get_accel_dict(config, base, chap_secrets, with_pki=False):                                    with_pki=with_pki)      # set CPUs cores to process requests -    dict.update({'thread_count' : get_half_cpus()}) +    dict.update({'thread_count' : get_core_count()})      # we need to store the path to the secrets file      dict.update({'chap_secrets_file' : chap_secrets}) diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index 90842b749..ab7a631bb 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -176,6 +176,25 @@ class ConfigSession(object):          except (ValueError, ConfigSessionError) as e:              raise ConfigSessionError(e) +    def set_section_tree(self, d: dict): +        try: +            if d: +                for p in dict_to_paths(d): +                    self.set(p) +        except (ValueError, ConfigSessionError) as e: +            raise ConfigSessionError(e) + +    def load_section_tree(self, mask: dict, d: dict): +        try: +            if mask: +                for p in dict_to_paths(mask): +                    self.delete(p) +            if d: +                for p in dict_to_paths(d): +                    self.set(p) +        except (ValueError, ConfigSessionError) as e: +            raise ConfigSessionError(e) +      def comment(self, path, value=None):          if not value:              value = [""] diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index 423fe01ed..e4b282d72 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -401,6 +401,30 @@ def union(left, right, libpath=LIBPATH):      return tree +def mask_inclusive(left, right, libpath=LIBPATH): +    if not (isinstance(left, ConfigTree) and isinstance(right, ConfigTree)): +        raise TypeError("Arguments must be instances of ConfigTree") + +    try: +        __lib = cdll.LoadLibrary(libpath) +        __mask_tree = __lib.mask_tree +        __mask_tree.argtypes = [c_void_p, c_void_p] +        __mask_tree.restype = c_void_p +        __get_error = __lib.get_error +        __get_error.argtypes = [] +        __get_error.restype = c_char_p + +        res = __mask_tree(left._get_config(), right._get_config()) +    except Exception as e: +        raise ConfigTreeError(e) +    if not res: +        msg = __get_error().decode() +        raise ConfigTreeError(msg) + +    tree = ConfigTree(address=res) + +    return tree +  def reference_tree_to_json(from_dir, to_file, libpath=LIBPATH):      try:          __lib = cdll.LoadLibrary(libpath) diff --git a/python/vyos/template.py b/python/vyos/template.py index 392322d46..1aa9ace8b 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -807,7 +807,7 @@ def kea_address_json(addresses):      out = []      for address in addresses: -        ifname = is_addr_assigned(address, return_ifname=True) +        ifname = is_addr_assigned(address, return_ifname=True, include_vrf=True)          if not ifname:              continue diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py index cac59475d..829124b57 100644 --- a/python/vyos/utils/network.py +++ b/python/vyos/utils/network.py @@ -310,7 +310,7 @@ def is_ipv6_link_local(addr):      return False -def is_addr_assigned(ip_address, vrf=None, return_ifname=False) -> bool | str: +def is_addr_assigned(ip_address, vrf=None, return_ifname=False, include_vrf=False) -> bool | str:      """ Verify if the given IPv4/IPv6 address is assigned to any interface """      from netifaces import interfaces      from vyos.utils.network import get_interface_config @@ -321,7 +321,7 @@ def is_addr_assigned(ip_address, vrf=None, return_ifname=False) -> bool | str:          # case there is no need to proceed with this data set - continue loop          # with next element          tmp = get_interface_config(interface) -        if dict_search('master', tmp) != vrf: +        if dict_search('master', tmp) != vrf and not include_vrf:              continue          if is_intf_addr_assigned(interface, ip_address): diff --git a/python/vyos/utils/system.py b/python/vyos/utils/system.py index 5d41c0c05..55813a5f7 100644 --- a/python/vyos/utils/system.py +++ b/python/vyos/utils/system.py @@ -79,13 +79,6 @@ def sysctl_apply(sysctl_dict: dict[str, str], revert: bool = True) -> bool:      # everything applied      return True -def get_half_cpus(): -    """ return 1/2 of the numbers of available CPUs """ -    cpu = os.cpu_count() -    if cpu > 1: -        cpu /= 2 -    return int(cpu) -  def find_device_file(device):      """ Recurively search /dev for the given device file and return its full path.          If no device file was found 'None' is returned """ diff --git a/smoketest/scripts/cli/base_accel_ppp_test.py b/smoketest/scripts/cli/base_accel_ppp_test.py index ac4bbcfe5..cc27cfbe9 100644 --- a/smoketest/scripts/cli/base_accel_ppp_test.py +++ b/smoketest/scripts/cli/base_accel_ppp_test.py @@ -21,7 +21,7 @@ from configparser import ConfigParser  from vyos.configsession import ConfigSession  from vyos.configsession import ConfigSessionError  from vyos.template import is_ipv4 -from vyos.utils.system import get_half_cpus +from vyos.cpu import get_core_count  from vyos.utils.process import process_named_running  from vyos.utils.process import cmd @@ -132,7 +132,7 @@ class BasicAccelPPPTest:              return out          def verify(self, conf): -            self.assertEqual(conf["core"]["thread-count"], str(get_half_cpus())) +            self.assertEqual(conf["core"]["thread-count"], str(get_core_count()))          def test_accel_name_servers(self):              # Verify proper Name-Server configuration for IPv4 and IPv6 diff --git a/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py b/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py index 274b97f22..97304da8b 100755 --- a/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py +++ b/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py @@ -27,6 +27,110 @@ 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): @@ -35,11 +139,34 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):          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' @@ -109,6 +236,50 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):          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() +  if __name__ == '__main__':      unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py index 24bd14af2..abf40cd3b 100755 --- a/smoketest/scripts/cli/test_service_dhcp-server.py +++ b/smoketest/scripts/cli/test_service_dhcp-server.py @@ -738,5 +738,30 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):          self.assertTrue(process_named_running(PROCESS_NAME))          self.assertTrue(process_named_running(CTRL_PROCESS_NAME)) +    def test_dhcp_on_interface_with_vrf(self): +        self.cli_set(['interfaces', 'ethernet', 'eth1', 'address', '10.1.1.1/30']) +        self.cli_set(['interfaces', 'ethernet', 'eth1', 'vrf', 'SMOKE-DHCP']) +        self.cli_set(['protocols', 'static', 'route', '10.1.10.0/24', 'interface', 'eth1', 'vrf', 'SMOKE-DHCP']) +        self.cli_set(['vrf', 'name', 'SMOKE-DHCP', 'protocols', 'static', 'route', '10.1.10.0/24', 'next-hop', '10.1.1.2']) +        self.cli_set(['vrf', 'name', 'SMOKE-DHCP', 'table', '1000']) +        self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'subnet-id', '1']) +        self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'option', 'default-router', '10.1.10.1']) +        self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'option', 'name-server', '1.1.1.1']) +        self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'range', '1', 'start', '10.1.10.10']) +        self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'range', '1', 'stop', '10.1.10.20']) +        self.cli_set(base_path + ['listen-address', '10.1.1.1']) +        self.cli_commit() + +        config = read_file(KEA4_CONF) +        obj = loads(config) + +        self.verify_config_value(obj, ['Dhcp4', 'interfaces-config'], 'interfaces', ['eth1/10.1.1.1']) + +        self.cli_delete(['interfaces', 'ethernet', 'eth1', 'vrf', 'SMOKE-DHCP']) +        self.cli_delete(['protocols', 'static', 'route', '10.1.10.0/24', 'interface', 'eth1', 'vrf']) +        self.cli_delete(['vrf', 'name', 'SMOKE-DHCP']) +        self.cli_commit() + +  if __name__ == '__main__':      unittest.main(verbosity=2) diff --git a/src/conf_mode/load-balancing_reverse-proxy.py b/src/conf_mode/load-balancing_reverse-proxy.py index 7338fe573..2a0acd84a 100755 --- a/src/conf_mode/load-balancing_reverse-proxy.py +++ b/src/conf_mode/load-balancing_reverse-proxy.py @@ -55,6 +55,29 @@ def get_config(config=None):      return lb +def _verify_cert(lb: dict, config: dict) -> None: +    if 'ca_certificate' in config['ssl']: +        ca_name = config['ssl']['ca_certificate'] +        pki_ca = lb['pki'].get('ca') +        if pki_ca is None: +            raise ConfigError(f'CA certificates does not exist in PKI') +        else: +            ca = pki_ca.get(ca_name) +            if ca is None: +                raise ConfigError(f'CA certificate "{ca_name}" does not exist') + +    elif 'certificate' in config['ssl']: +        cert_names = config['ssl']['certificate'] +        pki_certs = lb['pki'].get('certificate') +        if pki_certs is None: +            raise ConfigError(f'Certificates does not exist in PKI') + +        for cert_name in cert_names: +            pki_cert = pki_certs.get(cert_name) +            if pki_cert is None: +                raise ConfigError(f'Certificate "{cert_name}" does not exist') + +  def verify(lb):      if not lb:          return None @@ -83,6 +106,15 @@ def verify(lb):              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}"') +    for front, front_config in lb['service'].items(): +        if 'ssl' in front_config: +            _verify_cert(lb, front_config) + +    for back, back_config in lb['backend'].items(): +        if 'ssl' in back_config: +            _verify_cert(lb, back_config) + +  def generate(lb):      if not lb:          # Delete /run/haproxy/haproxy.cfg diff --git a/src/conf_mode/service_dhcp-server.py b/src/conf_mode/service_dhcp-server.py index ba3d69b07..bf4454fda 100755 --- a/src/conf_mode/service_dhcp-server.py +++ b/src/conf_mode/service_dhcp-server.py @@ -316,7 +316,7 @@ def verify(dhcp):                  raise ConfigError(f'Invalid CA certificate specified for DHCP high-availability')      for address in (dict_search('listen_address', dhcp) or []): -        if is_addr_assigned(address): +        if is_addr_assigned(address, include_vrf=True):              listen_ok = True              # no need to probe further networks, we have one that is valid              continue diff --git a/src/helpers/vyos_config_sync.py b/src/helpers/vyos_config_sync.py index 77f7cd810..0604b2837 100755 --- a/src/helpers/vyos_config_sync.py +++ b/src/helpers/vyos_config_sync.py @@ -21,9 +21,11 @@ import json  import requests  import urllib3  import logging -from typing import Optional, List, Union, Dict, Any +from typing import Optional, List, Tuple, Dict, Any  from vyos.config import Config +from vyos.configtree import ConfigTree +from vyos.configtree import mask_inclusive  from vyos.template import bracketize_ipv6 @@ -61,39 +63,45 @@ def post_request(url: str, -def retrieve_config(section: Optional[List[str]] = None) -> Optional[Dict[str, Any]]: +def retrieve_config(sections: List[list[str]]) -> Tuple[Dict[str, Any], Dict[str, Any]]:      """Retrieves the configuration from the local server.      Args: -        section: List[str]: The section of the configuration to retrieve. -        Default is None. +        sections: List[list[str]]: The list of sections of the configuration +        to retrieve, given as list of paths.      Returns: -        Optional[Dict[str, Any]]: The retrieved configuration as a -        dictionary, or None if an error occurred. +        Tuple[Dict[str, Any],Dict[str,Any]]: The tuple (mask, config) where: +            - mask: The tree of paths of sections, as a dictionary. +            - config: The subtree of masked config data, as a dictionary.      """ -    if section is None: -        section = [] -    conf = Config() -    config = conf.get_config_dict(section, get_first_key=True) -    if config: -        return config -    return None +    mask = ConfigTree('') +    for section in sections: +        mask.set(section) +    mask_dict = json.loads(mask.to_json()) + +    config = Config() +    config_tree = config.get_config_tree() +    masked = mask_inclusive(config_tree, mask) +    config_dict = json.loads(masked.to_json()) +    return mask_dict, config_dict  def set_remote_config(          address: str,          key: str, -        commands: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]: +        op: str, +        mask: Dict[str, Any], +        config: Dict[str, Any]) -> Optional[Dict[str, Any]]:      """Loads the VyOS configuration in JSON format to a remote host.      Args:          address (str): The address of the remote host.          key (str): The key to use for loading the configuration. -        commands (list): List of set/load commands for request, given as: -                         [{'op': str, 'path': list[str], 'section': dict}, -                         ...] +        op (str): The operation to perform (set or load). +        mask (dict): The dict of paths in sections. +        config (dict): The dict of masked config data.      Returns:          Optional[Dict[str, Any]]: The response from the remote host as a @@ -107,7 +115,9 @@ def set_remote_config(      url = f'https://{address}/configure-section'      data = json.dumps({ -        'commands': commands, +        'op': op, +        'mask': mask, +        'config': config,          'key': key      }) @@ -140,23 +150,15 @@ def config_sync(secondary_address: str,      )      # Sync sections ("nat", "firewall", etc) -    commands = [] -    for section in sections: -        config_json = retrieve_config(section=section) -        # Check if config path deesn't exist, for example "set nat" -        # we set empty value for config_json data -        # As we cannot send to the remote host section "nat None" config -        if not config_json: -            config_json = {} -        logger.debug( -            f"Retrieved config for section '{section}': {config_json}") - -        d = {'op': mode, 'path': section, 'section': config_json} -        commands.append(d) +    mask_dict, config_dict = retrieve_config(sections) +    logger.debug( +        f"Retrieved config for sections '{sections}': {config_dict}")      set_config = set_remote_config(address=secondary_address,                                     key=secondary_key, -                                   commands=commands) +                                   op=mode, +                                   mask=mask_dict, +                                   config=config_dict)      logger.debug(f"Set config for sections '{sections}': {set_config}") diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 77870a84c..ecbf6fcf9 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -140,6 +140,14 @@ class ConfigSectionModel(ApiModel, BaseConfigSectionModel):  class ConfigSectionListModel(ApiModel):      commands: List[BaseConfigSectionModel] +class BaseConfigSectionTreeModel(BaseModel): +    op: StrictStr +    mask: Dict +    config: Dict + +class ConfigSectionTreeModel(ApiModel, BaseConfigSectionTreeModel): +    pass +  class RetrieveModel(ApiModel):      op: StrictStr      path: List[StrictStr] @@ -374,7 +382,7 @@ class MultipartRequest(Request):                          self.form_err = (400,                          f"Malformed command '{c}': missing 'op' field")                      if endpoint not in ('/config-file', '/container-image', -                                        '/image'): +                                        '/image', '/configure-section'):                          if 'path' not in c:                              self.form_err = (400,                              f"Malformed command '{c}': missing 'path' field") @@ -392,12 +400,9 @@ class MultipartRequest(Request):                              self.form_err = (400,                              f"Malformed command '{c}': 'value' field must be a string")                      if endpoint in ('/configure-section'): -                        if 'section' not in c: -                            self.form_err = (400, -                            f"Malformed command '{c}': missing 'section' field") -                        elif not isinstance(c['section'], dict): +                        if 'section' not in c and 'config' not in c:                              self.form_err = (400, -                            f"Malformed command '{c}': 'section' field must be JSON of dict") +                            f"Malformed command '{c}': missing 'section' or 'config' field")                  if 'key' not in forms and 'key' not in merge:                      self.form_err = (401, "Valid API key is required") @@ -455,7 +460,8 @@ def call_commit(s: ConfigSession):              logger.warning(f"ConfigSessionError: {e}")  def _configure_op(data: Union[ConfigureModel, ConfigureListModel, -                              ConfigSectionModel, ConfigSectionListModel], +                              ConfigSectionModel, ConfigSectionListModel, +                              ConfigSectionTreeModel],                    request: Request, background_tasks: BackgroundTasks):      session = app.state.vyos_session      env = session.get_session_env() @@ -481,7 +487,8 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel,      try:          for c in data:              op = c.op -            path = c.path +            if not isinstance(c, BaseConfigSectionTreeModel): +                path = c.path              if isinstance(c, BaseConfigureModel):                  if c.value: @@ -495,6 +502,10 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel,              elif isinstance(c, BaseConfigSectionModel):                  section = c.section +            elif isinstance(c, BaseConfigSectionTreeModel): +                mask = c.mask +                config = c.config +              if isinstance(c, BaseConfigureModel):                  if op == 'set':                      session.set(path, value=value) @@ -514,6 +525,14 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel,                      session.load_section(path, section)                  else:                      raise ConfigSessionError(f"'{op}' is not a valid operation") + +            elif isinstance(c, BaseConfigSectionTreeModel): +                if op == 'set': +                    session.set_section_tree(config) +                elif op == 'load': +                    session.load_section_tree(mask, config) +                else: +                    raise ConfigSessionError(f"'{op}' is not a valid operation")          # end for          config = Config(session_env=env)          d = get_config_diff(config) @@ -554,7 +573,8 @@ def configure_op(data: Union[ConfigureModel,  @app.post('/configure-section')  def configure_section_op(data: Union[ConfigSectionModel, -                                     ConfigSectionListModel], +                                     ConfigSectionListModel, +                                     ConfigSectionTreeModel],                                 request: Request, background_tasks: BackgroundTasks):      return _configure_op(data, request, background_tasks) | 
