summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/tests/helper.py24
-rw-r--r--src/tests/test_config_diff.py69
-rw-r--r--src/tests/test_config_parser.py55
-rw-r--r--src/tests/test_configverify.py33
-rw-r--r--src/tests/test_dependency_graph.py31
-rw-r--r--src/tests/test_dict_search.py84
-rw-r--r--src/tests/test_find_device_file.py35
-rw-r--r--src/tests/test_initial_setup.py101
-rw-r--r--src/tests/test_op_mode.py65
-rw-r--r--src/tests/test_task_scheduler.py129
-rw-r--r--src/tests/test_template.py194
-rw-r--r--src/tests/test_utils.py28
-rw-r--r--src/tests/test_utils_network.py50
-rw-r--r--src/utils/initial-setup40
-rw-r--r--src/utils/vyos-config-file-query100
-rw-r--r--src/utils/vyos-config-to-commands29
-rw-r--r--src/utils/vyos-config-to-json40
-rw-r--r--src/utils/vyos-hostsd-client166
-rw-r--r--src/validators/as-number-list29
-rw-r--r--src/validators/base6427
-rw-r--r--src/validators/bgp-extended-community54
-rw-r--r--src/validators/bgp-large-community53
-rw-r--r--src/validators/bgp-large-community-list34
-rw-r--r--src/validators/bgp-rd-rt59
-rw-r--r--src/validators/bgp-regular-community50
-rw-r--r--src/validators/ddclient-protocol24
-rw-r--r--src/validators/fqdn2
-rw-r--r--src/validators/interface-address3
-rw-r--r--src/validators/ip-address10
-rw-r--r--src/validators/ip-cidr10
-rw-r--r--src/validators/ip-host10
-rw-r--r--src/validators/ip-prefix10
-rw-r--r--src/validators/ip-protocol42
-rw-r--r--src/validators/ipv410
-rw-r--r--src/validators/ipv4-address10
-rw-r--r--src/validators/ipv4-address-exclude7
-rw-r--r--src/validators/ipv4-host10
-rw-r--r--src/validators/ipv4-multicast10
-rw-r--r--src/validators/ipv4-prefix10
-rw-r--r--src/validators/ipv4-prefix-exclude7
-rw-r--r--src/validators/ipv4-range40
-rw-r--r--src/validators/ipv4-range-exclude7
-rw-r--r--src/validators/ipv4-range-mask27
-rw-r--r--src/validators/ipv610
-rw-r--r--src/validators/ipv6-address10
-rw-r--r--src/validators/ipv6-address-exclude7
-rw-r--r--src/validators/ipv6-eui64-prefix16
-rw-r--r--src/validators/ipv6-exclude7
-rw-r--r--src/validators/ipv6-host10
-rw-r--r--src/validators/ipv6-link-local12
-rw-r--r--src/validators/ipv6-multicast10
-rw-r--r--src/validators/ipv6-prefix10
-rw-r--r--src/validators/ipv6-prefix-exclude7
-rw-r--r--src/validators/ipv6-range20
-rw-r--r--src/validators/ipv6-range-exclude7
-rw-r--r--src/validators/ipv6-srv6-segments13
-rw-r--r--src/validators/mac-address2
-rw-r--r--src/validators/mac-address-exclude2
-rw-r--r--src/validators/numeric-exclude8
-rw-r--r--src/validators/port-multi52
-rw-r--r--src/validators/port-range40
-rw-r--r--src/validators/script42
-rw-r--r--src/validators/sysctl24
-rw-r--r--src/validators/timezone33
-rw-r--r--src/validators/vrf-name41
-rw-r--r--src/validators/wireless-phy25
66 files changed, 2236 insertions, 0 deletions
diff --git a/src/tests/helper.py b/src/tests/helper.py
new file mode 100644
index 0000000..f703314
--- /dev/null
+++ b/src/tests/helper.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 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 sys
+import importlib.util
+
+def prepare_module(file_path='', module_name=''):
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ sys.modules[module_name] = module
diff --git a/src/tests/test_config_diff.py b/src/tests/test_config_diff.py
new file mode 100644
index 0000000..61a2f34
--- /dev/null
+++ b/src/tests/test_config_diff.py
@@ -0,0 +1,69 @@
+#!/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 vyos.configtree
+
+from unittest import TestCase
+
+class TestConfigDiff(TestCase):
+ def setUp(self):
+ with open('tests/data/config.left', 'r') as f:
+ config_string = f.read()
+ self.config_left = vyos.configtree.ConfigTree(config_string)
+
+ with open('tests/data/config.right', 'r') as f:
+ config_string = f.read()
+ self.config_right = vyos.configtree.ConfigTree(config_string)
+
+ self.config_null = vyos.configtree.ConfigTree('')
+
+ def test_unit(self):
+ diff = vyos.configtree.DiffTree(self.config_left, self.config_null)
+ sub = diff.sub
+ self.assertEqual(sub.to_string(), self.config_left.to_string())
+
+ diff = vyos.configtree.DiffTree(self.config_null, self.config_left)
+ add = diff.add
+ self.assertEqual(add.to_string(), self.config_left.to_string())
+
+ def test_symmetry(self):
+ lr_diff = vyos.configtree.DiffTree(self.config_left,
+ self.config_right)
+ rl_diff = vyos.configtree.DiffTree(self.config_right,
+ self.config_left)
+
+ sub = lr_diff.sub
+ add = rl_diff.add
+ self.assertEqual(sub.to_string(), add.to_string())
+ add = lr_diff.add
+ sub = rl_diff.sub
+ self.assertEqual(add.to_string(), sub.to_string())
+
+ def test_identity(self):
+ lr_diff = vyos.configtree.DiffTree(self.config_left,
+ self.config_right)
+
+ sub = lr_diff.sub
+ inter = lr_diff.inter
+ add = lr_diff.add
+
+ r_union = vyos.configtree.union(add, inter)
+ l_union = vyos.configtree.union(sub, inter)
+
+ self.assertEqual(r_union.to_string(),
+ self.config_right.to_string(ordered_values=True))
+ self.assertEqual(l_union.to_string(),
+ self.config_left.to_string(ordered_values=True))
diff --git a/src/tests/test_config_parser.py b/src/tests/test_config_parser.py
new file mode 100644
index 0000000..c69732d
--- /dev/null
+++ b/src/tests/test_config_parser.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-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 vyos.configtree
+
+from unittest import TestCase
+
+class TestConfigParser(TestCase):
+ def setUp(self):
+ with open('tests/data/config.valid', 'r') as f:
+ config_string = f.read()
+ self.config = vyos.configtree.ConfigTree(config_string)
+
+ def test_top_level_valueless(self):
+ self.assertTrue(self.config.exists(["top-level-valueless-node"]))
+
+ def test_top_level_leaf(self):
+ self.assertTrue(self.config.exists(["top-level-leaf-node"]))
+ self.assertEqual(self.config.return_value(["top-level-leaf-node"]), "foo")
+
+ def test_top_level_tag(self):
+ self.assertTrue(self.config.exists(["top-level-tag-node"]))
+ # Sorting is now intentional, during parsing of config
+ self.assertEqual(self.config.list_nodes(["top-level-tag-node"]), ["bar", "foo"])
+
+ def test_copy(self):
+ self.config.copy(["top-level-tag-node", "bar"], ["top-level-tag-node", "baz"])
+ print(self.config.to_string())
+ self.assertTrue(self.config.exists(["top-level-tag-node", "baz"]))
+
+ def test_copy_duplicate(self):
+ with self.assertRaises(vyos.configtree.ConfigTreeError):
+ self.config.copy(["top-level-tag-node", "foo"], ["top-level-tag-node", "bar"])
+
+ def test_rename(self):
+ self.config.rename(["top-level-tag-node", "bar"], "quux")
+ print(self.config.to_string())
+ self.assertTrue(self.config.exists(["top-level-tag-node", "quux"]))
+
+ def test_rename_duplicate(self):
+ with self.assertRaises(vyos.configtree.ConfigTreeError):
+ self.config.rename(["top-level-tag-node", "foo"], "bar")
diff --git a/src/tests/test_configverify.py b/src/tests/test_configverify.py
new file mode 100644
index 0000000..15ccdf1
--- /dev/null
+++ b/src/tests/test_configverify.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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/>.
+
+from unittest import TestCase
+from vyos.configverify import verify_diffie_hellman_length
+from vyos.utils.process import cmd
+
+dh_file = '/tmp/dh.pem'
+
+class TestDictSearch(TestCase):
+ def setUp(self):
+ pass
+
+ def test_dh_key_none(self):
+ self.assertFalse(verify_diffie_hellman_length('/tmp/non_existing_file', '1024'))
+
+ def test_dh_key_512(self):
+ key_len = '512'
+ cmd(f'openssl dhparam -out {dh_file} {key_len}')
+ self.assertTrue(verify_diffie_hellman_length(dh_file, key_len))
diff --git a/src/tests/test_dependency_graph.py b/src/tests/test_dependency_graph.py
new file mode 100644
index 0000000..f682e87
--- /dev/null
+++ b/src/tests/test_dependency_graph.py
@@ -0,0 +1,31 @@
+#!/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 os
+from vyos.configdep import check_dependency_graph
+
+_here = os.path.dirname(__file__)
+ddir = os.path.join(_here, '../../data/config-mode-dependencies')
+
+from unittest import TestCase
+
+class TestDependencyGraph(TestCase):
+ def setUp(self):
+ pass
+
+ def test_acyclic(self):
+ res = check_dependency_graph(dependency_dir=ddir)
+ self.assertTrue(res)
diff --git a/src/tests/test_dict_search.py b/src/tests/test_dict_search.py
new file mode 100644
index 0000000..2435d89
--- /dev/null
+++ b/src/tests/test_dict_search.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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/>.
+
+from unittest import TestCase
+from vyos.utils.dict import dict_search
+from vyos.utils.dict import dict_search_recursive
+
+data = {
+ 'string': 'fooo',
+ 'nested': {'string': 'bar', 'empty': '', 'list': ['foo', 'bar']},
+ 'non': {},
+ 'list': ['bar', 'baz'],
+ 'dict': {'key_1': {}, 'key_2': 'vyos'},
+ 'interfaces': {'dummy': {'dum0': {'address': ['192.0.2.17/29']}},
+ 'ethernet': {'eth0': {'address': ['2001:db8::1/64', '192.0.2.1/29'],
+ 'description': 'Test123',
+ 'duplex': 'auto',
+ 'hw_id': '00:00:00:00:00:01',
+ 'speed': 'auto'},
+ 'eth1': {'address': ['192.0.2.9/29'],
+ 'description': 'Test456',
+ 'duplex': 'auto',
+ 'hw_id': '00:00:00:00:00:02',
+ 'speed': 'auto'}}}
+}
+
+class TestDictSearch(TestCase):
+ def setUp(self):
+ pass
+
+ def test_non_existing_keys(self):
+ # TestDictSearch: Return False when querying for non-existent key
+ self.assertEqual(dict_search('non_existing', data), None)
+ self.assertEqual(dict_search('non.existing.fancy.key', data), None)
+
+ def test_string(self):
+ # TestDictSearch: Return value when querying string
+ self.assertEqual(dict_search('string', data), data['string'])
+
+ def test_list(self):
+ # TestDictSearch: Return list items when querying list
+ self.assertEqual(dict_search('list', data), data['list'])
+
+ def test_dict_key_value(self):
+ # TestDictSearch: Return dictionary keys value when value is present
+ self.assertEqual(dict_search('dict.key_2', data), data['dict']['key_2'])
+
+ def test_nested_dict_key_value(self):
+ # TestDictSearch: Return string value of last key when querying for a nested string
+ self.assertEqual(dict_search('nested.string', data), data['nested']['string'])
+
+ def test_nested_dict_key_empty(self):
+ # TestDictSearch: Return False when querying for a nested string whose last key is empty
+ self.assertEqual(dict_search('nested.empty', data), '')
+ self.assertFalse(dict_search('nested.empty', data))
+
+ def test_nested_list(self):
+ # TestDictSearch: Return list items when querying nested list
+ self.assertEqual(dict_search('nested.list', data), data['nested']['list'])
+
+ def test_invalid_input(self):
+ # TestDictSearch: Return list items when querying nested list
+ self.assertEqual(dict_search('nested.list', None), None)
+ self.assertEqual(dict_search(None, data), None)
+
+ def test_dict_search_recursive(self):
+ # Test nested search in dictionary
+ tmp = list(dict_search_recursive(data, 'hw_id'))
+ self.assertEqual(len(tmp), 2)
+ tmp = list(dict_search_recursive(data, 'address'))
+ self.assertEqual(len(tmp), 3)
diff --git a/src/tests/test_find_device_file.py b/src/tests/test_find_device_file.py
new file mode 100644
index 0000000..f18043d
--- /dev/null
+++ b/src/tests/test_find_device_file.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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/>.
+
+from unittest import TestCase
+from vyos.utils.system import find_device_file
+
+class TestDeviceFile(TestCase):
+ """ used to find USB devices on target """
+ def setUp(self):
+ pass
+
+ def test_null(self):
+ self.assertEqual(find_device_file('null'), '/dev/null')
+
+ def test_zero(self):
+ self.assertEqual(find_device_file('zero'), '/dev/zero')
+
+ def test_input_event(self):
+ self.assertEqual(find_device_file('event0'), '/dev/input/event0')
+
+ def test_non_existing(self):
+ self.assertFalse(find_device_file('vyos'))
diff --git a/src/tests/test_initial_setup.py b/src/tests/test_initial_setup.py
new file mode 100644
index 0000000..f85bf12
--- /dev/null
+++ b/src/tests/test_initial_setup.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+import vyos.configtree
+import vyos.initialsetup as vis
+
+from unittest import TestCase
+from vyos.xml_ref import definition
+from vyos.xml_ref.pkg_cache.vyos_1x_cache import reference
+
+class TestInitialSetup(TestCase):
+ def setUp(self):
+ with open('tests/data/config.boot.default', 'r') as f:
+ config_string = f.read()
+ self.config = vyos.configtree.ConfigTree(config_string)
+ self.xml = definition.Xml()
+ self.xml.define(reference)
+
+ def test_set_user_password(self):
+ vis.set_user_password(self.config, 'vyos', 'vyosvyos')
+
+ # Old password hash from the default config
+ old_pw = '$6$QxPS.uk6mfo$9QBSo8u1FkH16gMyAVhus6fU3LOzvLR9Z9.82m3tiHFAxTtIkhaZSWssSgzt4v4dGAL8rhVQxTg0oAG9/q11h/'
+ new_pw = self.config.return_value(["system", "login", "user", "vyos", "authentication", "encrypted-password"])
+
+ # Just check it changed the hash, don't try to check if hash is good
+ self.assertNotEqual(old_pw, new_pw)
+
+ def test_disable_user_password(self):
+ vis.disable_user_password(self.config, 'vyos')
+ new_pw = self.config.return_value(["system", "login", "user", "vyos", "authentication", "encrypted-password"])
+
+ self.assertEqual(new_pw, '!')
+
+ def test_set_ssh_key_with_name(self):
+ test_ssh_key = " ssh-rsa fakedata vyos@vyos "
+ vis.set_user_ssh_key(self.config, 'vyos', test_ssh_key)
+
+ key_type = self.config.return_value(["system", "login", "user", "vyos", "authentication", "public-keys", "vyos@vyos", "type"])
+ key_data = self.config.return_value(["system", "login", "user", "vyos", "authentication", "public-keys", "vyos@vyos", "key"])
+
+ self.assertEqual(key_type, 'ssh-rsa')
+ self.assertEqual(key_data, 'fakedata')
+ self.assertTrue(self.xml.is_tag(["system", "login", "user", "vyos", "authentication", "public-keys"]))
+
+ def test_set_ssh_key_without_name(self):
+ # If key file doesn't include a name, the function will use user name for the key name
+
+ test_ssh_key = " ssh-rsa fakedata "
+ vis.set_user_ssh_key(self.config, 'vyos', test_ssh_key)
+
+ key_type = self.config.return_value(["system", "login", "user", "vyos", "authentication", "public-keys", "vyos", "type"])
+ key_data = self.config.return_value(["system", "login", "user", "vyos", "authentication", "public-keys", "vyos", "key"])
+
+ self.assertEqual(key_type, 'ssh-rsa')
+ self.assertEqual(key_data, 'fakedata')
+ self.assertTrue(self.xml.is_tag(["system", "login", "user", "vyos", "authentication", "public-keys"]))
+
+ def test_create_user(self):
+ vis.create_user(self.config, 'jrandomhacker', password='qwerty', key=" ssh-rsa fakedata jrandomhacker@foovax ")
+
+ self.assertTrue(self.config.exists(["system", "login", "user", "jrandomhacker"]))
+ self.assertTrue(self.config.exists(["system", "login", "user", "jrandomhacker", "authentication", "public-keys", "jrandomhacker@foovax"]))
+ self.assertTrue(self.config.exists(["system", "login", "user", "jrandomhacker", "authentication", "encrypted-password"]))
+ self.assertEqual(self.config.return_value(["system", "login", "user", "jrandomhacker", "level"]), "admin")
+
+ def test_set_hostname(self):
+ vis.set_host_name(self.config, "vyos-test")
+
+ self.assertEqual(self.config.return_value(["system", "host-name"]), "vyos-test")
+
+ def test_set_name_servers(self):
+ vis.set_name_servers(self.config, ["192.0.2.10", "203.0.113.20"])
+ servers = self.config.return_values(["system", "name-server"])
+
+ self.assertIn("192.0.2.10", servers)
+ self.assertIn("203.0.113.20", servers)
+
+ def test_set_gateway(self):
+ vis.set_default_gateway(self.config, '192.0.2.1')
+
+ self.assertTrue(self.config.exists(['protocols', 'static', 'route', '0.0.0.0/0', 'next-hop', '192.0.2.1']))
+ self.assertTrue(self.xml.is_tag(['protocols', 'static', 'multicast', 'route', '0.0.0.0/0', 'next-hop']))
+ self.assertTrue(self.xml.is_tag(['protocols', 'static', 'multicast', 'route']))
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/src/tests/test_op_mode.py b/src/tests/test_op_mode.py
new file mode 100644
index 0000000..90963b3
--- /dev/null
+++ b/src/tests/test_op_mode.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 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/>.
+
+from unittest import TestCase
+
+import vyos.opmode
+
+class TestVyOSOpMode(TestCase):
+ def test_field_name_normalization(self):
+ from vyos.opmode import _normalize_field_name
+
+ self.assertEqual(_normalize_field_name(" foo bar "), "foo_bar")
+ self.assertEqual(_normalize_field_name("foo-bar"), "foo_bar")
+ self.assertEqual(_normalize_field_name("foo (bar) baz"), "foo_bar_baz")
+ self.assertEqual(_normalize_field_name("load%"), "load_percentage")
+
+ def test_dict_fields_normalization_non_unique(self):
+ from vyos.opmode import _normalize_field_names
+
+ # Space and dot are both replaced by an underscore,
+ # so dicts like this cannor be normalized uniquely
+ data = {"foo bar": True, "foo.bar": False}
+
+ with self.assertRaises(vyos.opmode.InternalError):
+ _normalize_field_names(data)
+
+ def test_dict_fields_normalization_simple_dict(self):
+ from vyos.opmode import _normalize_field_names
+
+ data = {"foo bar": True, "Bar-Baz": False}
+ self.assertEqual(_normalize_field_names(data), {"foo_bar": True, "bar_baz": False})
+
+ def test_dict_fields_normalization_nested_dict(self):
+ from vyos.opmode import _normalize_field_names
+
+ data = {"foo bar": True, "bar-baz": {"baz-quux": {"quux-xyzzy": False}}}
+ self.assertEqual(_normalize_field_names(data),
+ {"foo_bar": True, "bar_baz": {"baz_quux": {"quux_xyzzy": False}}})
+
+ def test_dict_fields_normalization_mixed(self):
+ from vyos.opmode import _normalize_field_names
+
+ data = [{"foo bar": True, "bar-baz": [{"baz-quux": {"quux-xyzzy": [False]}}]}]
+ self.assertEqual(_normalize_field_names(data),
+ [{"foo_bar": True, "bar_baz": [{"baz_quux": {"quux_xyzzy": [False]}}]}])
+
+ def test_dict_fields_normalization_primitive(self):
+ from vyos.opmode import _normalize_field_names
+
+ data = [1, False, "foo"]
+ self.assertEqual(_normalize_field_names(data), [1, False, "foo"])
+
diff --git a/src/tests/test_task_scheduler.py b/src/tests/test_task_scheduler.py
new file mode 100644
index 0000000..130f825
--- /dev/null
+++ b/src/tests/test_task_scheduler.py
@@ -0,0 +1,129 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-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 os
+import tempfile
+import unittest
+import importlib
+
+from vyos import ConfigError
+
+try:
+ task_scheduler = importlib.import_module("src.conf_mode.system_task-scheduler")
+except ModuleNotFoundError: # for unittest.main()
+ import sys
+ sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+ task_scheduler = importlib.import_module("src.conf_mode.system_task-scheduler")
+
+class TestUpdateCrontab(unittest.TestCase):
+
+ def test_verify(self):
+ tests = [
+ {'name': 'one_task',
+ 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': None
+ },
+ {'name': 'has_interval_and_spec',
+ 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '0 * * * *', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': ConfigError
+ },
+ {'name': 'has_no_interval_and_spec',
+ 'tasks': [{'name': 'aaa', 'interval': '', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': ConfigError
+ },
+ {'name': 'invalid_interval',
+ 'tasks': [{'name': 'aaa', 'interval': '1y', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': ConfigError
+ },
+ {'name': 'invalid_interval_min',
+ 'tasks': [{'name': 'aaa', 'interval': '61m', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': ConfigError
+ },
+ {'name': 'invalid_interval_hour',
+ 'tasks': [{'name': 'aaa', 'interval': '25h', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': ConfigError
+ },
+ {'name': 'invalid_interval_day',
+ 'tasks': [{'name': 'aaa', 'interval': '32d', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': ConfigError
+ },
+ {'name': 'no_executable',
+ 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '', 'executable': '', 'args': ''}],
+ 'expected': ConfigError
+ },
+ {'name': 'invalid_executable',
+ 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '', 'executable': '/bin/aaa', 'args': ''}],
+ 'expected': ConfigError
+ }
+ ]
+ for t in tests:
+ with self.subTest(msg=t['name'], tasks=t['tasks'], expected=t['expected']):
+ if t['expected'] is not None:
+ with self.assertRaises(t['expected']):
+ task_scheduler.verify(t['tasks'])
+ else:
+ task_scheduler.verify(t['tasks'])
+
+ def test_generate(self):
+ tests = [
+ {'name': 'zero_task',
+ 'tasks': [],
+ 'expected': []
+ },
+ {'name': 'one_task',
+ 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': [
+ '### Generated by vyos-update-crontab.py ###',
+ '*/60 * * * * root sg vyattacfg \"/bin/ls -l\"']
+ },
+ {'name': 'one_task_with_hour',
+ 'tasks': [{'name': 'aaa', 'interval': '10h', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': [
+ '### Generated by vyos-update-crontab.py ###',
+ '0 */10 * * * root sg vyattacfg \"/bin/ls -l\"']
+ },
+ {'name': 'one_task_with_day',
+ 'tasks': [{'name': 'aaa', 'interval': '10d', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}],
+ 'expected': [
+ '### Generated by vyos-update-crontab.py ###',
+ '0 0 */10 * * root sg vyattacfg \"/bin/ls -l\"']
+ },
+ {'name': 'multiple_tasks',
+ 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '', 'executable': '/bin/ls', 'args': '-l'},
+ {'name': 'bbb', 'interval': '', 'spec': '0 0 * * *', 'executable': '/bin/ls', 'args': '-ltr'}
+ ],
+ 'expected': [
+ '### Generated by vyos-update-crontab.py ###',
+ '*/60 * * * * root sg vyattacfg \"/bin/ls -l\"',
+ '0 0 * * * root sg vyattacfg \"/bin/ls -ltr\"']
+ }
+ ]
+ for t in tests:
+ with self.subTest(msg=t['name'], tasks=t['tasks'], expected=t['expected']):
+ task_scheduler.crontab_file = tempfile.mkstemp()[1]
+ task_scheduler.generate(t['tasks'])
+ if len(t['expected']) > 0:
+ self.assertTrue(os.path.isfile(task_scheduler.crontab_file))
+ with open(task_scheduler.crontab_file) as f:
+ actual = f.read()
+ self.assertEqual(t['expected'], actual.splitlines())
+ os.remove(task_scheduler.crontab_file)
+ else:
+ self.assertFalse(os.path.isfile(task_scheduler.crontab_file))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/src/tests/test_template.py b/src/tests/test_template.py
new file mode 100644
index 0000000..dbb86b4
--- /dev/null
+++ b/src/tests/test_template.py
@@ -0,0 +1,194 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-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 vyos.template
+
+from vyos.utils.network import interface_exists
+from ipaddress import ip_network
+from unittest import TestCase
+
+class TestVyOSTemplate(TestCase):
+ def setUp(self):
+ pass
+
+ def test_is_interface(self):
+ for interface in ['lo', 'eth0']:
+ if interface_exists(interface):
+ self.assertTrue(vyos.template.is_interface(interface))
+ else:
+ self.assertFalse(vyos.template.is_interface(interface))
+ self.assertFalse(vyos.template.is_interface('non-existent'))
+
+ def test_is_ip(self):
+ self.assertTrue(vyos.template.is_ip('192.0.2.1'))
+ self.assertTrue(vyos.template.is_ip('2001:db8::1'))
+ self.assertFalse(vyos.template.is_ip('VyOS'))
+
+ def test_is_ipv4(self):
+ self.assertTrue(vyos.template.is_ipv4('192.0.2.1'))
+ self.assertTrue(vyos.template.is_ipv4('192.0.2.0/24'))
+ self.assertTrue(vyos.template.is_ipv4('192.0.2.1/32'))
+
+ self.assertFalse(vyos.template.is_ipv4('2001:db8::1'))
+ self.assertFalse(vyos.template.is_ipv4('2001:db8::/64'))
+ self.assertFalse(vyos.template.is_ipv4('VyOS'))
+
+ def test_is_ipv6(self):
+ self.assertTrue(vyos.template.is_ipv6('2001:db8::1'))
+ self.assertTrue(vyos.template.is_ipv6('2001:db8::/64'))
+ self.assertTrue(vyos.template.is_ipv6('2001:db8::1/64'))
+
+ self.assertFalse(vyos.template.is_ipv6('192.0.2.1'))
+ self.assertFalse(vyos.template.is_ipv6('192.0.2.0/24'))
+ self.assertFalse(vyos.template.is_ipv6('192.0.2.1/32'))
+ self.assertFalse(vyos.template.is_ipv6('VyOS'))
+
+ def test_address_from_cidr(self):
+ self.assertEqual(vyos.template.address_from_cidr('192.0.2.0/24'), '192.0.2.0')
+ self.assertEqual(vyos.template.address_from_cidr('2001:db8::/48'), '2001:db8::')
+
+ with self.assertRaises(ValueError):
+ # ValueError: 192.0.2.1/24 has host bits set
+ self.assertEqual(vyos.template.address_from_cidr('192.0.2.1/24'), '192.0.2.1')
+
+ with self.assertRaises(ValueError):
+ # ValueError: 2001:db8::1/48 has host bits set
+ self.assertEqual(vyos.template.address_from_cidr('2001:db8::1/48'), '2001:db8::1')
+
+ network_v4 = '192.0.2.0/26'
+ self.assertEqual(vyos.template.address_from_cidr(network_v4), str(ip_network(network_v4).network_address))
+
+ def test_netmask_from_cidr(self):
+ self.assertEqual(vyos.template.netmask_from_cidr('192.0.2.0/24'), '255.255.255.0')
+ self.assertEqual(vyos.template.netmask_from_cidr('192.0.2.128/25'), '255.255.255.128')
+ self.assertEqual(vyos.template.netmask_from_cidr('2001:db8::/48'), 'ffff:ffff:ffff::')
+
+ with self.assertRaises(ValueError):
+ # ValueError: 192.0.2.1/24 has host bits set
+ self.assertEqual(vyos.template.netmask_from_cidr('192.0.2.1/24'), '255.255.255.0')
+
+ with self.assertRaises(ValueError):
+ # ValueError: 2001:db8:1:/64 has host bits set
+ self.assertEqual(vyos.template.netmask_from_cidr('2001:db8:1:/64'), 'ffff:ffff:ffff:ffff::')
+
+ network_v4 = '192.0.2.0/26'
+ self.assertEqual(vyos.template.netmask_from_cidr(network_v4), str(ip_network(network_v4).netmask))
+
+ def test_first_host_address(self):
+ self.assertEqual(vyos.template.first_host_address('10.0.0.0/24'), '10.0.0.1')
+ self.assertEqual(vyos.template.first_host_address('10.0.0.10/24'), '10.0.0.1')
+ self.assertEqual(vyos.template.first_host_address('10.0.0.255/24'), '10.0.0.1')
+ self.assertEqual(vyos.template.first_host_address('10.0.0.128/25'), '10.0.0.129')
+ self.assertEqual(vyos.template.first_host_address('2001:db8::/64'), '2001:db8::1')
+ self.assertEqual(vyos.template.first_host_address('2001:db8::1000/64'), '2001:db8::1')
+ self.assertEqual(vyos.template.first_host_address('2001:db8::ffff:ffff:ffff:ffff/64'), '2001:db8::1')
+
+ def test_last_host_address(self):
+ self.assertEqual(vyos.template.last_host_address('10.0.0.0/24'), '10.0.0.254')
+ self.assertEqual(vyos.template.last_host_address('10.0.0.128/25'), '10.0.0.254')
+ self.assertEqual(vyos.template.last_host_address('2001:db8::/64'), '2001:db8::ffff:ffff:ffff:ffff')
+
+ def test_increment_ip(self):
+ self.assertEqual(vyos.template.inc_ip('10.0.0.0/24', '2'), '10.0.0.2')
+ self.assertEqual(vyos.template.inc_ip('10.0.0.0', '2'), '10.0.0.2')
+ self.assertEqual(vyos.template.inc_ip('10.0.0.0', '10'), '10.0.0.10')
+ self.assertEqual(vyos.template.inc_ip('2001:db8::/64', '2'), '2001:db8::2')
+ self.assertEqual(vyos.template.inc_ip('2001:db8::', '10'), '2001:db8::a')
+
+ def test_decrement_ip(self):
+ self.assertEqual(vyos.template.dec_ip('10.0.0.100/24', '1'), '10.0.0.99')
+ self.assertEqual(vyos.template.dec_ip('10.0.0.90', '10'), '10.0.0.80')
+ self.assertEqual(vyos.template.dec_ip('2001:db8::b/64', '10'), '2001:db8::1')
+ self.assertEqual(vyos.template.dec_ip('2001:db8::f', '5'), '2001:db8::a')
+
+ def test_is_network(self):
+ self.assertFalse(vyos.template.is_ip_network('192.0.2.0'))
+ self.assertFalse(vyos.template.is_ip_network('192.0.2.1/24'))
+ self.assertTrue(vyos.template.is_ip_network('192.0.2.0/24'))
+
+ self.assertFalse(vyos.template.is_ip_network('2001:db8::'))
+ self.assertFalse(vyos.template.is_ip_network('2001:db8::ffff'))
+ self.assertTrue(vyos.template.is_ip_network('2001:db8::/48'))
+ self.assertTrue(vyos.template.is_ip_network('2001:db8:1000::/64'))
+
+ def test_is_network(self):
+ self.assertTrue(vyos.template.compare_netmask('10.0.0.0/8', '20.0.0.0/8'))
+ self.assertTrue(vyos.template.compare_netmask('10.0.0.0/16', '20.0.0.0/16'))
+ self.assertFalse(vyos.template.compare_netmask('10.0.0.0/8', '20.0.0.0/16'))
+ self.assertFalse(vyos.template.compare_netmask('10.0.0.1', '20.0.0.0/16'))
+
+ self.assertTrue(vyos.template.compare_netmask('2001:db8:1000::/48', '2001:db8:2000::/48'))
+ self.assertTrue(vyos.template.compare_netmask('2001:db8:1000::/64', '2001:db8:2000::/64'))
+ self.assertFalse(vyos.template.compare_netmask('2001:db8:1000::/48', '2001:db8:2000::/64'))
+
+ def test_cipher_to_string(self):
+ ESP_DEFAULT = 'aes256gcm128-sha256-ecp256,aes128ccm64-sha256-ecp256'
+ IKEv2_DEFAULT = 'aes256gcm128-sha256-ecp256,aes128ccm128-md5_128-modp1024'
+
+ data = {
+ 'esp_group': {
+ 'ESP_DEFAULT': {
+ 'compression': 'disable',
+ 'lifetime': '3600',
+ 'mode': 'tunnel',
+ 'pfs': 'dh-group19',
+ 'proposal': {
+ '10': {
+ 'encryption': 'aes256gcm128',
+ 'hash': 'sha256',
+ },
+ '20': {
+ 'encryption': 'aes128ccm64',
+ 'hash': 'sha256',
+ }
+ }
+ }
+ },
+ 'ike_group': {
+ 'IKEv2_DEFAULT': {
+ 'close_action': 'none',
+ 'dead_peer_detection': {
+ 'action': 'hold',
+ 'interval': '30',
+ 'timeout': '120'
+ },
+ 'ikev2_reauth': 'no',
+ 'key_exchange': 'ikev2',
+ 'lifetime': '10800',
+ 'mobike': 'disable',
+ 'proposal': {
+ '10': {
+ 'dh_group': '19',
+ 'encryption': 'aes256gcm128',
+ 'hash': 'sha256'
+ },
+ '20': {
+ 'dh_group': '2',
+ 'encryption': 'aes128ccm128',
+ 'hash': 'md5_128'
+ },
+ }
+ }
+ },
+ }
+
+ for group_name, group_config in data['esp_group'].items():
+ ciphers = vyos.template.get_esp_ike_cipher(group_config)
+ self.assertIn(ESP_DEFAULT, ','.join(ciphers))
+
+ for group_name, group_config in data['ike_group'].items():
+ ciphers = vyos.template.get_esp_ike_cipher(group_config)
+ self.assertIn(IKEv2_DEFAULT, ','.join(ciphers))
diff --git a/src/tests/test_utils.py b/src/tests/test_utils.py
new file mode 100644
index 0000000..9ae329c
--- /dev/null
+++ b/src/tests/test_utils.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-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/>.
+
+from unittest import TestCase
+class TestVyOSUtils(TestCase):
+ def test_key_mangling(self):
+ from vyos.utils.dict import mangle_dict_keys
+ data = {"foo-bar": {"baz-quux": None}}
+ expected_data = {"foo_bar": {"baz_quux": None}}
+ new_data = mangle_dict_keys(data, '-', '_')
+ self.assertEqual(new_data, expected_data)
+
+ def test_sysctl_read(self):
+ from vyos.utils.system import sysctl_read
+ self.assertEqual(sysctl_read('net.ipv4.conf.lo.forwarding'), '1')
diff --git a/src/tests/test_utils_network.py b/src/tests/test_utils_network.py
new file mode 100644
index 0000000..5a6dc25
--- /dev/null
+++ b/src/tests/test_utils_network.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-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 vyos.utils.network
+from unittest import TestCase
+
+class TestVyOSUtilsNetwork(TestCase):
+ def setUp(self):
+ pass
+
+ def test_is_addr_assigned(self):
+ self.assertTrue(vyos.utils.network.is_addr_assigned('127.0.0.1'))
+ self.assertTrue(vyos.utils.network.is_addr_assigned('::1'))
+ self.assertFalse(vyos.utils.network.is_addr_assigned('127.251.255.123'))
+
+ def test_is_ipv6_link_local(self):
+ self.assertFalse(vyos.utils.network.is_ipv6_link_local('169.254.0.1'))
+ self.assertTrue(vyos.utils.network.is_ipv6_link_local('fe80::'))
+ self.assertTrue(vyos.utils.network.is_ipv6_link_local('fe80::affe:1'))
+ self.assertTrue(vyos.utils.network.is_ipv6_link_local('fe80::affe:1%eth0'))
+ self.assertFalse(vyos.utils.network.is_ipv6_link_local('2001:db8::'))
+ self.assertFalse(vyos.utils.network.is_ipv6_link_local('2001:db8::%eth0'))
+ self.assertFalse(vyos.utils.network.is_ipv6_link_local('VyOS'))
+ self.assertFalse(vyos.utils.network.is_ipv6_link_local('::1'))
+ self.assertFalse(vyos.utils.network.is_ipv6_link_local('::1%lo'))
+
+ def test_is_ipv6_link_local(self):
+ self.assertTrue(vyos.utils.network.is_loopback_addr('127.0.0.1'))
+ self.assertTrue(vyos.utils.network.is_loopback_addr('127.0.1.1'))
+ self.assertTrue(vyos.utils.network.is_loopback_addr('127.1.1.1'))
+ self.assertTrue(vyos.utils.network.is_loopback_addr('::1'))
+
+ self.assertFalse(vyos.utils.network.is_loopback_addr('::2'))
+ self.assertFalse(vyos.utils.network.is_loopback_addr('192.0.2.1'))
+
+
+
diff --git a/src/utils/initial-setup b/src/utils/initial-setup
new file mode 100644
index 0000000..37fc457
--- /dev/null
+++ b/src/utils/initial-setup
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+
+import argparse
+
+import vyos.configtree
+
+
+parser = argparse.ArgumentParser()
+
+parser.add_argument("--ssh", help="Enable SSH", action="store_true")
+parser.add_argument("--ssh-port", help="SSH port", type=int, action="store", default=22)
+
+parser.add_argument("--intf-address", help="Set interface address", type=str, action="append")
+
+parser.add_argument("config_file", help="Configuration file to modify", type=str)
+
+args = parser.parse_args()
+
+# Load the config file
+with open(args.config_file, 'r') as f:
+ config_file = f.read()
+
+config = vyos.configtree.ConfigTree(config_file)
+
+
+# Interface names and addresses are comma-separated,
+# we need to split them
+intf_addrs = list(map(lambda s: s.split(","), args.intf_address))
+
+# Enable SSH, if requested
+if args.ssh:
+ config.set(["service", "ssh", "port"], value=str(args.ssh_port))
+
+# Assign addresses to interfaces
+if intf_addrs:
+ for a in intf_addrs:
+ config.set(["interfaces", "ethernet", a[0], "address"], value=a[1])
+ config.set_tag(["interfaces", "ethernet"])
+
+print( config.to_string() )
diff --git a/src/utils/vyos-config-file-query b/src/utils/vyos-config-file-query
new file mode 100644
index 0000000..a10c7e9
--- /dev/null
+++ b/src/utils/vyos-config-file-query
@@ -0,0 +1,100 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 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 re
+import os
+import sys
+import json
+import argparse
+
+import vyos.configtree
+
+
+arg_parser = argparse.ArgumentParser()
+arg_parser.add_argument('-p', '--path', type=str,
+ help="VyOS config node, e.g. \"system config-management commit-revisions\"", required=True)
+arg_parser.add_argument('-f', '--file', type=str, help="VyOS config file, e.g. /config/config.boot", required=True)
+
+arg_parser.add_argument('-s', '--separator', type=str, default=' ', help="Value separator for the plain format")
+arg_parser.add_argument('-j', '--json', action='store_true')
+
+op_group = arg_parser.add_mutually_exclusive_group(required=True)
+op_group.add_argument('--return-value', action='store_true', help="Return a single node value")
+op_group.add_argument('--return-values', action='store_true', help="Return all values of a multi-value node")
+op_group.add_argument('--list-nodes', action='store_true', help="List children of a node")
+op_group.add_argument('--exists', action='store_true', help="Check if a node exists")
+
+args = arg_parser.parse_args()
+
+
+try:
+ with open(args.file, 'r') as f:
+ config_file = f.read()
+except OSError as e:
+ print("Could not read the config file: {0}".format(e))
+ sys.exit(1)
+
+try:
+ config = vyos.configtree.ConfigTree(config_file)
+except Exception as e:
+ print(e)
+ sys.exit(1)
+
+
+path = re.split(r'\s+', args.path)
+values = None
+
+if args.exists:
+ if config.exists(path):
+ sys.exit(0)
+ else:
+ sys.exit(1)
+elif args.return_value:
+ try:
+ values = [config.return_value(path)]
+ except vyos.configtree.ConfigTreeError as e:
+ print(e)
+ sys.exit(1)
+elif args.return_values:
+ try:
+ values = config.return_values(path)
+ except vyos.configtree.ConfigTreeError as e:
+ print(e)
+ sys.exit(1)
+elif args.list_nodes:
+ values = config.list_nodes(path)
+ if not values:
+ values = []
+else:
+ # Can't happen
+ print("Operation required")
+ sys.exit(1)
+
+
+if values:
+ if args.json:
+ print(json.dumps(values))
+ else:
+ if len(values) == 1:
+ print(values[0])
+ else:
+ # XXX: assuming values never contain quotes
+ values = list(map(lambda s: "\'{0}\'".format(s), values))
+ values_str = args.separator.join(values)
+ print(values_str)
+
+sys.exit(0)
diff --git a/src/utils/vyos-config-to-commands b/src/utils/vyos-config-to-commands
new file mode 100644
index 0000000..8b50f7c
--- /dev/null
+++ b/src/utils/vyos-config-to-commands
@@ -0,0 +1,29 @@
+#!/usr/bin/python3
+
+import sys
+
+from signal import signal, SIGPIPE, SIG_DFL
+from vyos.configtree import ConfigTree
+
+signal(SIGPIPE,SIG_DFL)
+
+config_string = None
+if (len(sys.argv) == 1):
+ # If no argument given, act as a pipe
+ config_string = sys.stdin.read()
+else:
+ file_name = sys.argv[1]
+ try:
+ with open(file_name, 'r') as f:
+ config_string = f.read()
+ except OSError as e:
+ print("Could not read config file {0}: {1}".format(file_name, e), file=sys.stderr)
+
+try:
+ config = ConfigTree(config_string)
+ commands = config.to_commands()
+except ValueError as e:
+ print("Could not parse the config file: {0}".format(e), file=sys.stderr)
+ sys.exit(1)
+
+print(commands)
diff --git a/src/utils/vyos-config-to-json b/src/utils/vyos-config-to-json
new file mode 100644
index 0000000..e03fd6a
--- /dev/null
+++ b/src/utils/vyos-config-to-json
@@ -0,0 +1,40 @@
+#!/usr/bin/python3
+
+import sys
+import json
+
+from signal import signal, SIGPIPE, SIG_DFL
+from vyos.configtree import ConfigTree
+
+signal(SIGPIPE,SIG_DFL)
+
+config_string = None
+if (len(sys.argv) == 1):
+ # If no argument given, act as a pipe
+ config_string = sys.stdin.read()
+else:
+ file_name = sys.argv[1]
+ try:
+ with open(file_name, 'r') as f:
+ config_string = f.read()
+ except OSError as e:
+ print("Could not read config file {0}: {1}".format(file_name, e), file=sys.stderr)
+
+# This script is usually called with the output of "cli-shell-api showCfg", which does not
+# escape backslashes. "ConfigTree()" expects escaped backslashes when parsing a config
+# string (and also prints them itself). Therefore this script would fail.
+# Manually escape backslashes here to handle backslashes in any configuration strings
+# properly. The alternative would be to modify the output of "cli-shell-api showCfg",
+# but that may be break other things who rely on that specific output.
+config_string = config_string.replace("\\", "\\\\")
+
+try:
+ config = ConfigTree(config_string)
+ json_str = config.to_json()
+ # Pretty print
+ json_str = json.dumps(json.loads(json_str), indent=4, sort_keys=True)
+except ValueError as e:
+ print("Could not parse the config file: {0}".format(e), file=sys.stderr)
+ sys.exit(1)
+
+print(json_str)
diff --git a/src/utils/vyos-hostsd-client b/src/utils/vyos-hostsd-client
new file mode 100644
index 0000000..a051595
--- /dev/null
+++ b/src/utils/vyos-hostsd-client
@@ -0,0 +1,166 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 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 sys
+import argparse
+
+import vyos.hostsd_client
+
+parser = argparse.ArgumentParser(allow_abbrev=False)
+group = parser.add_mutually_exclusive_group()
+
+group.add_argument('--add-name-servers', type=str, nargs='*')
+group.add_argument('--delete-name-servers', action='store_true')
+group.add_argument('--get-name-servers', type=str, const='.*', nargs='?')
+
+group.add_argument('--add-name-server-tags-recursor', type=str, nargs='*')
+group.add_argument('--delete-name-server-tags-recursor', type=str, nargs='*')
+group.add_argument('--get-name-server-tags-recursor', action='store_true')
+
+group.add_argument('--add-name-server-tags-system', type=str, nargs='*')
+group.add_argument('--delete-name-server-tags-system', type=str, nargs='*')
+group.add_argument('--get-name-server-tags-system', action='store_true')
+
+group.add_argument('--add-forward-zone', type=str, nargs='?')
+group.add_argument('--delete-forward-zones', type=str, nargs='*')
+group.add_argument('--get-forward-zones', action='store_true')
+
+group.add_argument('--add-search-domains', type=str, nargs='*')
+group.add_argument('--delete-search-domains', action='store_true')
+group.add_argument('--get-search-domains', type=str, const='.*', nargs='?')
+
+group.add_argument('--add-hosts', type=str, nargs='*')
+group.add_argument('--delete-hosts', action='store_true')
+group.add_argument('--get-hosts', type=str, const='.*', nargs='?')
+
+group.add_argument('--set-host-name', type=str)
+
+# for --set-host-name
+parser.add_argument('--domain-name', type=str)
+
+# for forward zones
+parser.add_argument('--nameservers', type=str, nargs='*')
+parser.add_argument('--addnta', action='store_true')
+parser.add_argument('--recursion-desired', action='store_true')
+
+parser.add_argument('--tag', type=str)
+
+# users must call --apply either in the same command or after they're done
+parser.add_argument('--apply', action="store_true")
+
+args = parser.parse_args()
+
+try:
+ client = vyos.hostsd_client.Client()
+ ops = 1
+
+ if args.add_name_servers:
+ if not args.tag:
+ raise ValueError("--tag is required for this operation")
+ client.add_name_servers({args.tag: args.add_name_servers})
+ elif args.delete_name_servers:
+ if not args.tag:
+ raise ValueError("--tag is required for this operation")
+ client.delete_name_servers([args.tag])
+ elif args.get_name_servers:
+ print(client.get_name_servers(args.get_name_servers))
+
+ elif args.add_name_server_tags_recursor:
+ client.add_name_server_tags_recursor(args.add_name_server_tags_recursor)
+ elif args.delete_name_server_tags_recursor:
+ client.delete_name_server_tags_recursor(args.delete_name_server_tags_recursor)
+ elif args.get_name_server_tags_recursor:
+ print(client.get_name_server_tags_recursor())
+
+ elif args.add_name_server_tags_system:
+ client.add_name_server_tags_system(args.add_name_server_tags_system)
+ elif args.delete_name_server_tags_system:
+ client.delete_name_server_tags_system(args.delete_name_server_tags_system)
+ elif args.get_name_server_tags_system:
+ print(client.get_name_server_tags_system())
+
+ elif args.add_forward_zone:
+ if not args.nameservers:
+ raise ValueError("--nameservers is required for this operation")
+ client.add_forward_zones(
+ { args.add_forward_zone: {
+ 'server': args.nameservers,
+ 'addnta': args.addnta,
+ 'recursion_desired': args.recursion_desired
+ }
+ })
+ elif args.delete_forward_zones:
+ client.delete_forward_zones(args.delete_forward_zones)
+ elif args.get_forward_zones:
+ print(client.get_forward_zones())
+
+ elif args.add_search_domains:
+ if not args.tag:
+ raise ValueError("--tag is required for this operation")
+ client.add_search_domains({args.tag: args.add_search_domains})
+ elif args.delete_search_domains:
+ if not args.tag:
+ raise ValueError("--tag is required for this operation")
+ client.delete_search_domains([args.tag])
+ elif args.get_search_domains:
+ print(client.get_search_domains(args.get_search_domains))
+
+ elif args.add_hosts:
+ if not args.tag:
+ raise ValueError("--tag is required for this operation")
+ data = {}
+ for h in args.add_hosts:
+ entry = {}
+ params = h.split(",")
+ if len(params) < 2:
+ raise ValueError("Malformed host entry")
+ # Address needs to be a list because of changes made in T2683
+ entry['address'] = [params[1]]
+ entry['aliases'] = params[2:]
+ data[params[0]] = entry
+ client.add_hosts({args.tag: data})
+ elif args.delete_hosts:
+ if not args.tag:
+ raise ValueError("--tag is required for this operation")
+ client.delete_hosts([args.tag])
+ elif args.get_hosts:
+ print(client.get_hosts(args.get_hosts))
+
+ elif args.set_host_name:
+ if not args.domain_name:
+ raise ValueError('--domain-name is required for this operation')
+ client.set_host_name({'host_name': args.set_host_name, 'domain_name': args.domain_name})
+
+ elif args.apply:
+ pass
+ else:
+ ops = 0
+
+ if args.apply:
+ client.apply()
+
+ if ops == 0:
+ raise ValueError("Operation required")
+
+except ValueError as e:
+ print("Incorrect options: {0}".format(e))
+ sys.exit(1)
+except vyos.hostsd_client.VyOSHostsdError as e:
+ print("Server returned an error: {0}".format(e))
+ sys.exit(1)
+
diff --git a/src/validators/as-number-list b/src/validators/as-number-list
new file mode 100644
index 0000000..432d441
--- /dev/null
+++ b/src/validators/as-number-list
@@ -0,0 +1,29 @@
+#!/bin/sh
+#
+# Copyright (C) 2022 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/>.
+
+if [ $# -lt 1 ]; then
+ echo "Illegal number of parameters"
+ exit 1
+fi
+
+for var in "$@"; do
+ ${vyos_validators_dir}/numeric --range 1-4294967294 $var
+ if [ $? -ne 0 ]; then
+ exit 1
+ fi
+done
+
+exit 0
diff --git a/src/validators/base64 b/src/validators/base64
new file mode 100644
index 0000000..e2b1e73
--- /dev/null
+++ b/src/validators/base64
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 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 base64
+from sys import argv
+
+if __name__ == '__main__':
+ if len(argv) != 2:
+ exit(1)
+ try:
+ base64.b64decode(argv[1])
+ except:
+ exit(1)
+ exit(0)
diff --git a/src/validators/bgp-extended-community b/src/validators/bgp-extended-community
new file mode 100644
index 0000000..d666655
--- /dev/null
+++ b/src/validators/bgp-extended-community
@@ -0,0 +1,54 @@
+#!/usr/bin/env python3
+
+# Copyright 2019-2023 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/>.
+
+from argparse import ArgumentParser
+from sys import exit
+
+from vyos.template import is_ipv4
+
+COMM_MAX_2_OCTET: int = 65535
+COMM_MAX_4_OCTET: int = 4294967295
+
+if __name__ == '__main__':
+ # add an argument with community
+ parser: ArgumentParser = ArgumentParser()
+ parser.add_argument('community', type=str)
+ args = parser.parse_args()
+
+ for community in args.community.split():
+ if community.count(':') != 1:
+ print("Invalid community format")
+ exit(1)
+ try:
+ # try to extract community parts from an argument
+ comm_left: str = community.split(':')[0]
+ comm_right: int = int(community.split(':')[1])
+
+ # check if left part is an IPv4 address
+ if is_ipv4(comm_left) and 0 <= comm_right <= COMM_MAX_2_OCTET:
+ continue
+ # check if a left part is a number
+ if 0 <= int(comm_left) <= COMM_MAX_2_OCTET \
+ and 0 <= comm_right <= COMM_MAX_4_OCTET:
+ continue
+
+ raise Exception()
+
+ except Exception:
+ # fail if something was wrong
+ print("Invalid community format")
+ exit(1) \ No newline at end of file
diff --git a/src/validators/bgp-large-community b/src/validators/bgp-large-community
new file mode 100644
index 0000000..3863983
--- /dev/null
+++ b/src/validators/bgp-large-community
@@ -0,0 +1,53 @@
+#!/usr/bin/env python3
+
+# Copyright 2019-2022 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/>.
+
+from argparse import ArgumentParser
+from sys import exit
+
+from vyos.template import is_ipv4
+
+COMM_MAX_4_OCTET: int = 4294967295
+
+if __name__ == '__main__':
+ # add an argument with community
+ parser: ArgumentParser = ArgumentParser()
+ parser.add_argument('community', type=str)
+ args = parser.parse_args()
+ community: str = args.community
+ if community.count(':') != 2:
+ print("Invalid community format")
+ exit(1)
+ try:
+ # try to extract community parts from an argument
+ comm_part1: int = int(community.split(':')[0])
+ comm_part2: int = int(community.split(':')[1])
+ comm_part3: int = int(community.split(':')[2])
+
+ # check compatibilities of left and right parts
+ if 0 <= comm_part1 <= COMM_MAX_4_OCTET \
+ and 0 <= comm_part2 <= COMM_MAX_4_OCTET \
+ and 0 <= comm_part3 <= COMM_MAX_4_OCTET:
+ exit(0)
+
+ except Exception:
+ # fail if something was wrong
+ print("Invalid community format")
+ exit(1)
+
+ # fail if none of validators catched the value
+ print("Invalid community format")
+ exit(1) \ No newline at end of file
diff --git a/src/validators/bgp-large-community-list b/src/validators/bgp-large-community-list
new file mode 100644
index 0000000..9ba5b27
--- /dev/null
+++ b/src/validators/bgp-large-community-list
@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-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 re
+import sys
+
+pattern = '(.*):(.*):(.*)'
+allowedChars = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '+', '*', '?', '^', '$', '(', ')', '[', ']', '{', '}', '|', '\\', ':', '-' }
+
+if __name__ == '__main__':
+ if len(sys.argv) != 2:
+ sys.exit(1)
+
+ value = sys.argv[1].split(':')
+ if not len(value) == 3:
+ sys.exit(1)
+
+ if not (re.match(pattern, sys.argv[1]) and set(sys.argv[1]).issubset(allowedChars)):
+ sys.exit(1)
+
+ sys.exit(0)
diff --git a/src/validators/bgp-rd-rt b/src/validators/bgp-rd-rt
new file mode 100644
index 0000000..b2b69c9
--- /dev/null
+++ b/src/validators/bgp-rd-rt
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 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/>.
+
+from argparse import ArgumentParser
+from vyos.template import is_ipv4
+
+parser = ArgumentParser()
+group = parser.add_mutually_exclusive_group()
+group.add_argument('--route-distinguisher', action='store', help='Validate BGP route distinguisher')
+group.add_argument('--route-target', action='store', help='Validate one BGP route-target')
+group.add_argument('--route-target-multi', action='store', help='Validate multiple, whitespace separated BGP route-targets')
+args = parser.parse_args()
+
+def is_valid(rt):
+ """ Verify BGP RD/RT - both can be verified using the same logic """
+ # every RD/RT (route distinguisher/route target) needs to have a colon and
+ # must consists of two parts
+ value = rt.split(':')
+ if len(value) != 2:
+ return False
+
+ # An RD/RT must either be only numbers, or the first part must be an IPv4
+ # address
+ if (is_ipv4(value[0]) or value[0].isdigit()) and value[1].isdigit():
+ return True
+ return False
+
+if __name__ == '__main__':
+ if args.route_distinguisher:
+ if not is_valid(args.route_distinguisher):
+ exit(1)
+
+ elif args.route_target:
+ if not is_valid(args.route_target):
+ exit(1)
+
+ elif args.route_target_multi:
+ for rt in args.route_target_multi.split(' '):
+ if not is_valid(rt):
+ exit(1)
+
+ else:
+ parser.print_help()
+ exit(1)
+
+ exit(0)
diff --git a/src/validators/bgp-regular-community b/src/validators/bgp-regular-community
new file mode 100644
index 0000000..d43a71e
--- /dev/null
+++ b/src/validators/bgp-regular-community
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+
+# Copyright 2019-2022 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/>.
+
+from argparse import ArgumentParser
+from sys import exit
+
+from vyos.template import is_ipv4
+
+COMM_MAX_2_OCTET: int = 65535
+
+if __name__ == '__main__':
+ # add an argument with community
+ parser: ArgumentParser = ArgumentParser()
+ parser.add_argument('community', type=str)
+ args = parser.parse_args()
+ community: str = args.community
+ if community.count(':') != 1:
+ print("Invalid community format")
+ exit(1)
+ try:
+ # try to extract community parts from an argument
+ comm_left: int = int(community.split(':')[0])
+ comm_right: int = int(community.split(':')[1])
+
+ # check compatibilities of left and right parts
+ if 0 <= comm_left <= COMM_MAX_2_OCTET \
+ and 0 <= comm_right <= COMM_MAX_2_OCTET:
+ exit(0)
+ except Exception:
+ # fail if something was wrong
+ print("Invalid community format")
+ exit(1)
+
+ # fail if none of validators catched the value
+ print("Invalid community format")
+ exit(1) \ No newline at end of file
diff --git a/src/validators/ddclient-protocol b/src/validators/ddclient-protocol
new file mode 100644
index 0000000..ce5efbd
--- /dev/null
+++ b/src/validators/ddclient-protocol
@@ -0,0 +1,24 @@
+#!/bin/sh
+#
+# 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/>.
+
+ddclient -list-protocols | grep -vE 'cloudns|porkbun' | grep -qw $1
+
+if [ $? -gt 0 ]; then
+ echo "Error: $1 is not a valid protocol, please choose from the supported list of protocols"
+ exit 1
+fi
+
+exit 0
diff --git a/src/validators/fqdn b/src/validators/fqdn
new file mode 100644
index 0000000..a65d2d5
--- /dev/null
+++ b/src/validators/fqdn
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+${vyos_libexec_dir}/validate-value --regex "[A-Za-z0-9][-.A-Za-z0-9]*" --value "$1"
diff --git a/src/validators/interface-address b/src/validators/interface-address
new file mode 100644
index 0000000..4c20395
--- /dev/null
+++ b/src/validators/interface-address
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+ipaddrcheck --is-ipv4-host $1 || ipaddrcheck --is-ipv6-host $1
diff --git a/src/validators/ip-address b/src/validators/ip-address
new file mode 100644
index 0000000..11d6df0
--- /dev/null
+++ b/src/validators/ip-address
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+ipaddrcheck --is-any-single $1
+
+if [ $? -gt 0 ]; then
+ echo "Error: $1 is not a valid IP address"
+ exit 1
+fi
+
+exit 0 \ No newline at end of file
diff --git a/src/validators/ip-cidr b/src/validators/ip-cidr
new file mode 100644
index 0000000..60d2ac2
--- /dev/null
+++ b/src/validators/ip-cidr
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+ipaddrcheck --is-any-cidr $1
+
+if [ $? -gt 0 ]; then
+ echo "Error: $1 is not a valid IP CIDR"
+ exit 1
+fi
+
+exit 0 \ No newline at end of file
diff --git a/src/validators/ip-host b/src/validators/ip-host
new file mode 100644
index 0000000..77c578f
--- /dev/null
+++ b/src/validators/ip-host
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+ipaddrcheck --is-any-host $1
+
+if [ $? -gt 0 ]; then
+ echo "Error: $1 is not a valid IP host"
+ exit 1
+fi
+
+exit 0 \ No newline at end of file
diff --git a/src/validators/ip-prefix b/src/validators/ip-prefix
new file mode 100644
index 0000000..e5a64fe
--- /dev/null
+++ b/src/validators/ip-prefix
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+ipaddrcheck --is-any-net $1
+
+if [ $? -gt 0 ]; then
+ echo "Error: $1 is not a valid IP prefix"
+ exit 1
+fi
+
+exit 0 \ No newline at end of file
diff --git a/src/validators/ip-protocol b/src/validators/ip-protocol
new file mode 100644
index 0000000..c4c8825
--- /dev/null
+++ b/src/validators/ip-protocol
@@ -0,0 +1,42 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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 re
+from sys import argv,exit
+
+if __name__ == '__main__':
+ if len(argv) != 2:
+ exit(1)
+
+ input = argv[1]
+ try:
+ # IP protocol can be in the range 0 - 255, thus the range must end with 256
+ if int(input) in range(0, 256):
+ exit(0)
+ except ValueError:
+ pass
+
+ pattern = "!?\\b(all|ip|hopopt|icmp|igmp|ggp|ipencap|st|tcp|egp|igp|pup|udp|" \
+ "tcp_udp|hmp|xns-idp|rdp|iso-tp4|dccp|xtp|ddp|idpr-cmtp|ipv6|" \
+ "ipv6-route|ipv6-frag|idrp|rsvp|gre|esp|ah|skip|ipv6-icmp|icmpv6|" \
+ "ipv6-nonxt|ipv6-opts|rspf|vmtp|eigrp|ospf|ax.25|ipip|etherip|" \
+ "encap|99|pim|ipcomp|vrrp|l2tp|isis|sctp|fc|mobility-header|" \
+ "udplite|mpls-in-ip|manet|hip|shim6|wesp|rohc)\\b"
+ if re.match(pattern, input):
+ exit(0)
+
+ print(f'Error: {input} is not a valid IP protocol')
+ exit(1)
diff --git a/src/validators/ipv4 b/src/validators/ipv4
new file mode 100644
index 0000000..8676d58
--- /dev/null
+++ b/src/validators/ipv4
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+ipaddrcheck --is-ipv4 $1
+
+if [ $? -gt 0 ]; then
+ echo "Error: $1 is not IPv4"
+ exit 1
+fi
+
+exit 0 \ No newline at end of file
diff --git a/src/validators/ipv4-address b/src/validators/ipv4-address
new file mode 100644
index 0000000..058db08
--- /dev/null
+++ b/src/validators/ipv4-address
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+ipaddrcheck --is-ipv4-single $1
+
+if [ $? -gt 0 ]; then
+ echo "Error: $1 is not a valid IPv4 address"
+ exit 1
+fi
+
+exit 0 \ No newline at end of file
diff --git a/src/validators/ipv4-address-exclude b/src/validators/ipv4-address-exclude
new file mode 100644
index 0000000..80ad17d
--- /dev/null
+++ b/src/validators/ipv4-address-exclude
@@ -0,0 +1,7 @@
+#!/bin/sh
+arg="$1"
+if [ "${arg:0:1}" != "!" ]; then
+ exit 1
+fi
+path=$(dirname "$0")
+${path}/ipv4-address "${arg:1}"
diff --git a/src/validators/ipv4-host b/src/validators/ipv4-host
new file mode 100644
index 0000000..74b8c36
--- /dev/null
+++ b/src/validators/ipv4-host
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+ipaddrcheck --is-ipv4-host $1
+
+if [ $? -gt 0 ]; then
+ echo "Error: $1 is not a valid IPv4 host"
+ exit 1
+fi
+
+exit 0 \ No newline at end of file
diff --git a/src/validators/ipv4-multicast b/src/validators/ipv4-multicast
new file mode 100644
index 0000000..3f28c51
--- /dev/null
+++ b/src/validators/ipv4-multicast
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+ipaddrcheck --is-ipv4-multicast $1 && ipaddrcheck --is-ipv4-single $1
+
+if [ $? -gt 0 ]; then
+ echo "Error: $1 is not a valid IPv4 multicast address"
+ exit 1
+fi
+
+exit 0 \ No newline at end of file
diff --git a/src/validators/ipv4-prefix b/src/validators/ipv4-prefix
new file mode 100644
index 0000000..7e1e0e8
--- /dev/null
+++ b/src/validators/ipv4-prefix
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+ipaddrcheck --is-ipv4-net $1
+
+if [ $? -gt 0 ]; then
+ echo "Error: $1 is not a valid IPv4 prefix"
+ exit 1
+fi
+
+exit 0 \ No newline at end of file
diff --git a/src/validators/ipv4-prefix-exclude b/src/validators/ipv4-prefix-exclude
new file mode 100644
index 0000000..4f7de40
--- /dev/null
+++ b/src/validators/ipv4-prefix-exclude
@@ -0,0 +1,7 @@
+#!/bin/sh
+arg="$1"
+if [ "${arg:0:1}" != "!" ]; then
+ exit 1
+fi
+path=$(dirname "$0")
+${path}/ipv4-prefix "${arg:1}"
diff --git a/src/validators/ipv4-range b/src/validators/ipv4-range
new file mode 100644
index 0000000..6492bfc
--- /dev/null
+++ b/src/validators/ipv4-range
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+# snippet from https://stackoverflow.com/questions/10768160/ip-address-converter
+ip2dec () {
+ local a b c d ip=$@
+ IFS=. read -r a b c d <<< "$ip"
+ printf '%d\n' "$((a * 256 ** 3 + b * 256 ** 2 + c * 256 + d))"
+}
+
+error_exit() {
+ echo "Error: $1 is not a valid IPv4 address range"
+ exit 1
+}
+
+# Only run this if there is a hypen present in $1
+if [[ "$1" =~ "-" ]]; then
+ # This only works with real bash (<<<) - split IP addresses into array with
+ # hyphen as delimiter
+ readarray -d - -t strarr <<< $1
+
+ ipaddrcheck --is-ipv4-single ${strarr[0]}
+ if [ $? -gt 0 ]; then
+ error_exit $1
+ fi
+
+ ipaddrcheck --is-ipv4-single ${strarr[1]}
+ if [ $? -gt 0 ]; then
+ error_exit $1
+ fi
+
+ start=$(ip2dec ${strarr[0]})
+ stop=$(ip2dec ${strarr[1]})
+ if [ $start -ge $stop ]; then
+ error_exit $1
+ fi
+
+ exit 0
+fi
+
+error_exit $1
diff --git a/src/validators/ipv4-range-exclude b/src/validators/ipv4-range-exclude
new file mode 100644
index 0000000..3787b4d
--- /dev/null
+++ b/src/validators/ipv4-range-exclude
@@ -0,0 +1,7 @@
+#!/bin/sh
+arg="$1"
+if [ "${arg:0:1}" != "!" ]; then
+ exit 1
+fi
+path=$(dirname "$0")
+${path}/ipv4-range "${arg:1}"
diff --git a/src/validators/ipv4-range-mask b/src/validators/ipv4-range-mask
new file mode 100644
index 0000000..9373328
--- /dev/null
+++ b/src/validators/ipv4-range-mask
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+error_exit() {
+ echo "Error: $1 is not a valid IPv4 address range or these IPs are not under /$2"
+ exit 1
+}
+
+# Check if address range is under the same netmask
+# -m - mask
+# -r - IP range in format x.x.x.x-y.y.y.y
+while getopts m:r: flag
+do
+ case "${flag}" in
+ m) mask=${OPTARG};;
+ r) range=${OPTARG}
+ esac
+done
+
+if [[ "${range}" =~ "-" ]]&&[[ ! -z ${mask} ]]; then
+ ipaddrcheck --range-prefix-length ${mask} --is-ipv4-range ${range}
+ if [ $? -gt 0 ]; then
+ error_exit ${range} ${mask}
+ fi
+ exit 0
+fi
+
+error_exit ${range} ${mask}
diff --git a/src/validators/ipv6 b/src/validators/ipv6
new file mode 100644
index 0000000..4ae130e
--- /dev/null
+++ b/src/validators/ipv6
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+ipaddrcheck --is-ipv6 $1
+
+if [ $? -gt 0 ]; then
+ echo "Error: $1 is not IPv6"
+ exit 1
+fi
+
+exit 0 \ No newline at end of file
diff --git a/src/validators/ipv6-address b/src/validators/ipv6-address
new file mode 100644
index 0000000..1fca776
--- /dev/null
+++ b/src/validators/ipv6-address
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+ipaddrcheck --is-ipv6-single $1
+
+if [ $? -gt 0 ]; then
+ echo "Error: $1 is not a valid IPv6 address"
+ exit 1
+fi
+
+exit 0 \ No newline at end of file
diff --git a/src/validators/ipv6-address-exclude b/src/validators/ipv6-address-exclude
new file mode 100644
index 0000000..be1d3db
--- /dev/null
+++ b/src/validators/ipv6-address-exclude
@@ -0,0 +1,7 @@
+#!/bin/sh
+arg="$1"
+if [ "${arg:0:1}" != "!" ]; then
+ exit 1
+fi
+path=$(dirname "$0")
+${path}/ipv6-address "${arg:1}"
diff --git a/src/validators/ipv6-eui64-prefix b/src/validators/ipv6-eui64-prefix
new file mode 100644
index 0000000..d7f2626
--- /dev/null
+++ b/src/validators/ipv6-eui64-prefix
@@ -0,0 +1,16 @@
+#!/usr/bin/env python3
+
+# Validator used to check if given IPv6 prefix is of size /64 required by EUI64
+
+from sys import argv
+from sys import exit
+
+if __name__ == '__main__':
+ if len(argv) != 2:
+ exit(1)
+
+ prefix = argv[1]
+ if prefix.split('/')[1] == '64':
+ exit(0)
+
+ exit(1)
diff --git a/src/validators/ipv6-exclude b/src/validators/ipv6-exclude
new file mode 100644
index 0000000..893eeab
--- /dev/null
+++ b/src/validators/ipv6-exclude
@@ -0,0 +1,7 @@
+#!/bin/sh
+arg="$1"
+if [ "${arg:0:1}" != "!" ]; then
+ exit 1
+fi
+path=$(dirname "$0")
+${path}/ipv6 "${arg:1}"
diff --git a/src/validators/ipv6-host b/src/validators/ipv6-host
new file mode 100644
index 0000000..7085809
--- /dev/null
+++ b/src/validators/ipv6-host
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+ipaddrcheck --is-ipv6-host $1
+
+if [ $? -gt 0 ]; then
+ echo "Error: $1 is not a valid IPv6 host"
+ exit 1
+fi
+
+exit 0 \ No newline at end of file
diff --git a/src/validators/ipv6-link-local b/src/validators/ipv6-link-local
new file mode 100644
index 0000000..6ac3ea7
--- /dev/null
+++ b/src/validators/ipv6-link-local
@@ -0,0 +1,12 @@
+#!/usr/bin/python3
+
+import sys
+from vyos.utils.network import is_ipv6_link_local
+
+if __name__ == '__main__':
+ if len(sys.argv)>1:
+ addr = sys.argv[1]
+ if not is_ipv6_link_local(addr):
+ sys.exit(1)
+
+ sys.exit(0)
diff --git a/src/validators/ipv6-multicast b/src/validators/ipv6-multicast
new file mode 100644
index 0000000..5aa7d73
--- /dev/null
+++ b/src/validators/ipv6-multicast
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+ipaddrcheck --is-ipv6-multicast $1 && ipaddrcheck --is-ipv6-single $1
+
+if [ $? -gt 0 ]; then
+ echo "Error: $1 is not a valid IPv6 multicast address"
+ exit 1
+fi
+
+exit 0 \ No newline at end of file
diff --git a/src/validators/ipv6-prefix b/src/validators/ipv6-prefix
new file mode 100644
index 0000000..890dda7
--- /dev/null
+++ b/src/validators/ipv6-prefix
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+ipaddrcheck --is-ipv6-net $1
+
+if [ $? -gt 0 ]; then
+ echo "Error: $1 is not a valid IPv6 prefix"
+ exit 1
+fi
+
+exit 0 \ No newline at end of file
diff --git a/src/validators/ipv6-prefix-exclude b/src/validators/ipv6-prefix-exclude
new file mode 100644
index 0000000..6fa4f1d
--- /dev/null
+++ b/src/validators/ipv6-prefix-exclude
@@ -0,0 +1,7 @@
+#!/bin/sh
+arg="$1"
+if [ "${arg:0:1}" != "!" ]; then
+ exit 1
+fi
+path=$(dirname "$0")
+${path}/ipv6-prefix "${arg:1}"
diff --git a/src/validators/ipv6-range b/src/validators/ipv6-range
new file mode 100644
index 0000000..7080860
--- /dev/null
+++ b/src/validators/ipv6-range
@@ -0,0 +1,20 @@
+#!/usr/bin/env python3
+
+from ipaddress import IPv6Address
+from sys import argv, exit
+
+if __name__ == '__main__':
+ if len(argv) > 1:
+ # try to pass validation and raise an error if failed
+ try:
+ ipv6_range = argv[1]
+ range_left = ipv6_range.split('-')[0]
+ range_right = ipv6_range.split('-')[1]
+ if not IPv6Address(range_left) < IPv6Address(range_right):
+ raise ValueError(f'left element {range_left} must be less than right element {range_right}')
+ except Exception as err:
+ print(f'Error: {ipv6_range} is not a valid IPv6 range: {err}')
+ exit(1)
+ else:
+ print('Error: an IPv6 range argument must be provided')
+ exit(1)
diff --git a/src/validators/ipv6-range-exclude b/src/validators/ipv6-range-exclude
new file mode 100644
index 0000000..912b55a
--- /dev/null
+++ b/src/validators/ipv6-range-exclude
@@ -0,0 +1,7 @@
+#!/bin/sh
+arg="$1"
+if [ "${arg:0:1}" != "!" ]; then
+ exit 1
+fi
+path=$(dirname "$0")
+${path}/ipv6-range "${arg:1}"
diff --git a/src/validators/ipv6-srv6-segments b/src/validators/ipv6-srv6-segments
new file mode 100644
index 0000000..e72a4f9
--- /dev/null
+++ b/src/validators/ipv6-srv6-segments
@@ -0,0 +1,13 @@
+#!/bin/sh
+segments="$1"
+export IFS="/"
+
+for ipv6addr in $segments; do
+ ipaddrcheck --is-ipv6-single $ipv6addr
+ if [ $? -gt 0 ]; then
+ echo "Error: $1 is not a valid IPv6 address"
+ exit 1
+ fi
+done
+exit 0
+
diff --git a/src/validators/mac-address b/src/validators/mac-address
new file mode 100644
index 0000000..bb859a6
--- /dev/null
+++ b/src/validators/mac-address
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+${vyos_libexec_dir}/validate-value --regex "([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})" --value "$1"
diff --git a/src/validators/mac-address-exclude b/src/validators/mac-address-exclude
new file mode 100644
index 0000000..c449130
--- /dev/null
+++ b/src/validators/mac-address-exclude
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+${vyos_libexec_dir}/validate-value --regex "!([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})" --value "$1"
diff --git a/src/validators/numeric-exclude b/src/validators/numeric-exclude
new file mode 100644
index 0000000..676a240
--- /dev/null
+++ b/src/validators/numeric-exclude
@@ -0,0 +1,8 @@
+#!/bin/sh
+path=$(dirname "$0")
+num="${@: -1}"
+if [ "${num:0:1}" != "!" ]; then
+ ${path}/numeric $@
+else
+ ${path}/numeric ${@:1:$#-1} ${num:1}
+fi
diff --git a/src/validators/port-multi b/src/validators/port-multi
new file mode 100644
index 0000000..ed6ff68
--- /dev/null
+++ b/src/validators/port-multi
@@ -0,0 +1,52 @@
+#!/usr/bin/python3
+
+from sys import argv
+from sys import exit
+import re
+
+from vyos.utils.file import read_file
+
+services_file = '/etc/services'
+
+def get_services():
+ names = []
+ service_data = read_file(services_file, "")
+ for line in service_data.split("\n"):
+ if not line or line[0] == '#':
+ continue
+ tmp = line.split()
+ names.append(tmp[0])
+ if len(tmp) > 2:
+ # Add port aliases to service list, too
+ names.extend(tmp[2:])
+ # remove duplicate entries (e.g. echo) from list
+ names = list(dict.fromkeys(names))
+ return names
+
+if __name__ == '__main__':
+ if len(argv)>1:
+ ports = argv[1].split(",")
+ services = get_services()
+
+ for port in ports:
+ if port and port[0] == '!':
+ port = port[1:]
+ if re.match('^[0-9]{1,5}-[0-9]{1,5}$', port):
+ port_1, port_2 = port.split('-')
+ if int(port_1) not in range(1, 65536) or int(port_2) not in range(1, 65536):
+ print(f'Error: {port} is not a valid port range')
+ exit(1)
+ if int(port_1) > int(port_2):
+ print(f'Error: {port} is not a valid port range')
+ exit(1)
+ elif port.isnumeric():
+ if int(port) not in range(1, 65536):
+ print(f'Error: {port} is not a valid port')
+ exit(1)
+ elif port not in services:
+ print(f'Error: {port} is not a valid service name')
+ exit(1)
+ else:
+ exit(2)
+
+ exit(0)
diff --git a/src/validators/port-range b/src/validators/port-range
new file mode 100644
index 0000000..526c639
--- /dev/null
+++ b/src/validators/port-range
@@ -0,0 +1,40 @@
+#!/usr/bin/python3
+
+import sys
+import re
+
+from vyos.utils.file import read_file
+
+services_file = '/etc/services'
+
+def get_services():
+ names = []
+ service_data = read_file(services_file, "")
+ for line in service_data.split("\n"):
+ if not line or line[0] == '#':
+ continue
+ names.append(line.split(None, 1)[0])
+ return names
+
+def error(port_range):
+ print(f'Error: {port_range} is not a valid port or port range')
+ sys.exit(1)
+
+if __name__ == '__main__':
+ if len(sys.argv)>1:
+ port_range = sys.argv[1]
+ if re.match('^[0-9]{1,5}-[0-9]{1,5}$', port_range):
+ port_1, port_2 = port_range.split('-')
+ if int(port_1) not in range(1, 65536) or int(port_2) not in range(1, 65536):
+ error(port_range)
+ if int(port_1) > int(port_2):
+ error(port_range)
+ elif port_range.isnumeric() and int(port_range) not in range(1, 65536):
+ error(port_range)
+ elif not port_range.isnumeric() and port_range not in get_services():
+ print(f'Error: {port_range} is not a valid service name')
+ sys.exit(1)
+ else:
+ sys.exit(2)
+
+ sys.exit(0)
diff --git a/src/validators/script b/src/validators/script
new file mode 100644
index 0000000..eb176d2
--- /dev/null
+++ b/src/validators/script
@@ -0,0 +1,42 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-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
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# 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
+import sys
+import shlex
+
+from vyos.utils.file import file_is_persistent
+
+if __name__ == '__main__':
+ if len(sys.argv) < 2:
+ sys.exit('Please specify script file to check')
+
+ # if the "script" is a script+ stowaway arguments, this removes the aguements
+ script = shlex.split(sys.argv[1])[0]
+
+ if not os.path.exists(script):
+ sys.exit(f'File {script} does not exist')
+
+ if not (os.path.isfile(script) and os.access(script, os.X_OK)):
+ sys.exit(f'File {script} is not an executable file')
+
+ # File outside the config dir is just a warning
+ if not file_is_persistent(script):
+ sys.exit(0)(
+ f'Warning: file {script} is outside the "/config" directory\n'
+ 'It will not be automatically migrated to a new image on system update'
+ )
diff --git a/src/validators/sysctl b/src/validators/sysctl
new file mode 100644
index 0000000..9b5bba3
--- /dev/null
+++ b/src/validators/sysctl
@@ -0,0 +1,24 @@
+#!/bin/sh
+#
+# Copyright (C) 2021 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/>.
+
+declare -a array
+eval "array=($(/sbin/sysctl -N -a))"
+
+if [[ ! " ${array[@]} " =~ " $1 " ]]; then
+ # passed sysctl option is invalid
+ exit 1
+fi
+exit 0
diff --git a/src/validators/timezone b/src/validators/timezone
new file mode 100644
index 0000000..e55af8d
--- /dev/null
+++ b/src/validators/timezone
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-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 argparse
+import sys
+
+from vyos.utils.process import cmd
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--validate", action="store", required=True, help="Check if timezone is valid")
+ args = parser.parse_args()
+
+ tz_data = cmd('timedatectl list-timezones')
+ tz_data = tz_data.split('\n')
+
+ if args.validate not in tz_data:
+ sys.exit("the timezone can't be found in the timezone list")
+ sys.exit()
diff --git a/src/validators/vrf-name b/src/validators/vrf-name
new file mode 100644
index 0000000..29167c6
--- /dev/null
+++ b/src/validators/vrf-name
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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 re
+from sys import argv, exit
+
+if __name__ == '__main__':
+ if len(argv) != 2:
+ exit(1)
+
+ vrf = argv[1]
+ length = len(vrf)
+
+ if length not in range(1, 16):
+ exit(1)
+
+ # Treat loopback interface "lo" explicitly. Adding "lo" explicitly to the
+ # following regex pattern would deny any VRF name starting with lo - thuse
+ # local-vrf would be illegal - and that we do not want.
+ if vrf == "lo":
+ exit(1)
+
+ pattern = r'^(?!(bond|br|dum|eth|lan|eno|ens|enp|enx|gnv|ipoe|l2tp|l2tpeth|\
+ vtun|ppp|pppoe|peth|tun|vti|vxlan|wg|wlan|wwan|\d)\d*(\.\d+)?(v.+)?).*$'
+ if not re.match(pattern, vrf):
+ exit(1)
+
+ exit(0)
diff --git a/src/validators/wireless-phy b/src/validators/wireless-phy
new file mode 100644
index 0000000..513a902
--- /dev/null
+++ b/src/validators/wireless-phy
@@ -0,0 +1,25 @@
+#!/bin/sh
+#
+# Copyright (C) 2018-2020 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/>.
+
+if [ ! -d /sys/class/ieee80211 ]; then
+ echo No IEEE 802.11 physical interfaces detected
+ exit 1
+fi
+
+if [ ! -e /sys/class/ieee80211/$1 ]; then
+ echo Device interface "$1" does not exist
+ exit 1
+fi