diff options
-rw-r--r-- | Makefile | 1 | ||||
-rw-r--r-- | data/templates/frr/rip.frr.tmpl | 143 | ||||
-rw-r--r-- | debian/control | 2 | ||||
-rwxr-xr-x | debian/rules | 4 | ||||
-rw-r--r-- | interface-definitions/protocols-rip.xml.in | 2 | ||||
-rw-r--r-- | interface-definitions/service_console-server.xml.in | 3 | ||||
-rw-r--r-- | interface-definitions/ssh.xml.in | 21 | ||||
-rw-r--r-- | python/setup.py | 10 | ||||
-rw-r--r-- | python/vyos/ifconfig/wireguard.py | 58 | ||||
-rw-r--r-- | python/vyos/xml/.gitignore | 1 | ||||
-rw-r--r-- | python/vyos/xml/__init__.py | 39 | ||||
-rw-r--r-- | python/vyos/xml/cache/__init__.py | 0 | ||||
-rw-r--r-- | python/vyos/xml/definition.py | 301 | ||||
-rwxr-xr-x | python/vyos/xml/generate.py | 70 | ||||
-rw-r--r-- | python/vyos/xml/kw.py | 83 | ||||
-rw-r--r-- | python/vyos/xml/load.py | 290 | ||||
-rw-r--r-- | python/vyos/xml/test_xml.py | 279 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-wireguard.py | 9 | ||||
-rwxr-xr-x | src/conf_mode/protocols_rip.py | 317 | ||||
-rwxr-xr-x | src/conf_mode/service_console-server.py | 9 | ||||
-rwxr-xr-x | src/conf_mode/service_pppoe-server.py | 2 | ||||
-rwxr-xr-x | src/conf_mode/vpn_l2tp.py | 2 | ||||
-rwxr-xr-x | src/conf_mode/vpn_pptp.py | 2 | ||||
-rwxr-xr-x | src/conf_mode/vpn_sstp.py | 2 |
24 files changed, 1594 insertions, 56 deletions
@@ -73,7 +73,6 @@ interface_definitions: $(BUILD_DIR) $(obj) rm -f $(TMPL_DIR)/interfaces/wirelessmodem/node.tag/ipv6/node.def rm -f $(TMPL_DIR)/protocols/node.def rm -rf $(TMPL_DIR)/protocols/nbgp - rm -rf $(TMPL_DIR)/protocols/nrip rm -rf $(TMPL_DIR)/protocols/isis rm -f $(TMPL_DIR)/protocols/static/node.def rm -f $(TMPL_DIR)/system/node.def diff --git a/data/templates/frr/rip.frr.tmpl b/data/templates/frr/rip.frr.tmpl new file mode 100644 index 000000000..60bc686bd --- /dev/null +++ b/data/templates/frr/rip.frr.tmpl @@ -0,0 +1,143 @@ +! +{% if rip_conf -%} +router rip +{% if old_default_distance -%} +no distance {{old_default_distance}} +{% endif -%} +{% if default_distance -%} +distance {{default_distance}} +{% endif -%} +{% if old_default_originate -%} +no default-information originate +{% endif -%} +{% if default_originate -%} +default-information originate +{% endif -%} +{% if old_rip.default_metric -%} +no default-metric {{old_rip.default_metric}} +{% endif -%} +{% if rip.default_metric -%} +default-metric {{rip.default_metric}} +{% endif -%} +{% for protocol in old_rip.redist -%} +{% if old_rip.redist[protocol]['metric'] and old_rip.redist[protocol]['route_map'] -%} +no redistribute {{protocol}} metric {{rip.redist[protocol]['metric']}} route-map {{rip.redist[protocol]['route_map']}} +{% elif old_rip.redist[protocol]['metric'] -%} +no redistribute {{protocol}} metric {{old_rip.redist[protocol]['metric']}} +{% elif old_rip.redist[protocol]['route_map'] -%} +no redistribute {{protocol}} route-map {{old_rip.redist[protocol]['route_map']}} +{% else -%} +no redistribute {{protocol}} +{% endif -%} +{% endfor -%} +{% for protocol in rip.redist -%} +{% if rip.redist[protocol]['metric'] and rip.redist[protocol]['route_map'] -%} +redistribute {{protocol}} metric {{rip.redist[protocol]['metric']}} route-map {{rip.redist[protocol]['route_map']}} +{% elif rip.redist[protocol]['metric'] -%} +redistribute {{protocol}} metric {{rip.redist[protocol]['metric']}} +{% elif rip.redist[protocol]['route_map'] -%} +redistribute {{protocol}} route-map {{rip.redist[protocol]['route_map']}} +{% else -%} +redistribute {{protocol}} +{% endif -%} +{% endfor -%} +{% for iface in old_rip.distribute -%} +{% if old_rip.distribute[iface].iface_access_list_in -%} +no distribute-list {{old_rip.distribute[iface].iface_access_list_in}} in {{iface}} +{% endif -%} +{% if old_rip.distribute[iface].iface_access_list_out -%} +no distribute-list {{old_rip.distribute[iface].iface_access_list_out}} out {{iface}} +{% endif -%} +{% if old_rip.distribute[iface].iface_prefix_list_in -%} +no distribute-list prefix {{old_rip.distribute[iface].iface_prefix_list_in}} in {{iface}} +{% endif -%} +{% if old_rip.distribute[iface].iface_prefix_list_out -%} +no distribute-list prefix {{old_rip.distribute[iface].iface_prefix_list_out}} out {{iface}} +{% endif -%} +{% endfor -%} +{% for iface in rip.distribute -%} +{% if rip.distribute[iface].iface_access_list_in -%} +distribute-list {{rip.distribute[iface].iface_access_list_in}} in {{iface}} +{% endif -%} +{% if rip.distribute[iface].iface_access_list_out -%} +distribute-list {{rip.distribute[iface].iface_access_list_out}} out {{iface}} +{% endif -%} +{% if rip.distribute[iface].iface_prefix_list_in -%} +distribute-list prefix {{rip.distribute[iface].iface_prefix_list_in}} in {{iface}} +{% endif -%} +{% if rip.distribute[iface].iface_prefix_list_out -%} +distribute-list prefix {{rip.distribute[iface].iface_prefix_list_out}} out {{iface}} +{% endif -%} +{% endfor -%} +{% if old_rip.dist_acl_in -%} +no distribute-list {{old_rip.dist_acl_in}} in +{% endif -%} +{% if rip.dist_acl_in -%} +distribute-list {{rip.dist_acl_in}} in +{% endif -%} +{% if old_rip.dist_acl_out -%} +no distribute-list {{old_rip.dist_acl_out}} out +{% endif -%} +{% if rip.dist_acl_out -%} +distribute-list {{rip.dist_acl_out}} out +{% endif -%} +{% if old_rip.dist_prfx_in -%} +no distribute-list prefix {{old_rip.dist_prfx_in}} in +{% endif -%} +{% if rip.dist_prfx_in -%} +distribute-list prefix {{rip.dist_prfx_in}} in +{% endif -%} +{% if old_rip.dist_prfx_out -%} +no distribute-list prefix {{old_rip.dist_prfx_out}} out +{% endif -%} +{% if rip.dist_prfx_out -%} +distribute-list prefix {{rip.dist_prfx_out}} out +{% endif -%} +{% for network in old_rip.networks -%} +no network {{network}} +{% endfor -%} +{% for network in rip.networks -%} +network {{network}} +{% endfor -%} +{% for iface in old_rip.ifaces -%} +no network {{iface}} +{% endfor -%} +{% for iface in rip.ifaces -%} +network {{iface}} +{% endfor -%} +{% for neighbor in old_rip.neighbors -%} +no neighbor {{neighbor}} +{% endfor -%} +{% for neighbor in rip.neighbors -%} +neighbor {{neighbor}} +{% endfor -%} +{% for net in rip.net_distance -%} +{% if rip.net_distance[net].access_list and rip.net_distance[net].distance -%} +distance {{rip.net_distance[net].distance}} {{net}} {{rip.net_distance[net].access_list}} +{% else -%} +distance {{rip.net_distance[net].distance}} {{net}} +{% endif -%} +{% endfor -%} +{% for passive_iface in old_rip.passive_iface -%} +no passive-interface {{passive_iface}} +{% endfor -%} +{% for passive_iface in rip.passive_iface -%} +passive-interface {{passive_iface}} +{% endfor -%} +{% for route in old_rip.route -%} +no route {{route}} +{% endfor -%} +{% for route in rip.route -%} +route {{route}} +{% endfor -%} +{% if old_rip.timer_update or old_rip.timer_timeout or old_rip.timer_garbage -%} +no timers basic +{% endif -%} +{% if rip.timer_update or rip.timer_timeout or rip.timer_garbage -%} +timers basic {{rip.timer_update}} {{rip.timer_timeout}} {{rip.timer_garbage}} +{% endif -%} +! +{% else -%} +no router rip +! +{% endif -%} diff --git a/debian/control b/debian/control index aaaf33e2a..5e14340a8 100644 --- a/debian/control +++ b/debian/control @@ -6,7 +6,7 @@ Build-Depends: debhelper (>= 9), quilt, python3, python3-setuptools, - quilt, + python3-xmltodict, python3-lxml, python3-nose, python3-coverage, diff --git a/debian/rules b/debian/rules index 3e408b538..c080b8633 100755 --- a/debian/rules +++ b/debian/rules @@ -23,6 +23,10 @@ override_dh_auto_build: override_dh_auto_install: dh_auto_install + + # convert the XML to dictionaries + env PYTHONPATH=python python3 python/vyos/xml/generate.py + cd python; python3 setup.py install --install-layout=deb --root ../$(DIR); cd .. # Install scripts diff --git a/interface-definitions/protocols-rip.xml.in b/interface-definitions/protocols-rip.xml.in index a9c295f4c..107f0e0d5 100644 --- a/interface-definitions/protocols-rip.xml.in +++ b/interface-definitions/protocols-rip.xml.in @@ -2,7 +2,7 @@ <interfaceDefinition> <node name="protocols"> <children> - <node name="nrip" owner="${vyos_conf_scripts_dir}/protocols_rip.py"> + <node name="rip" owner="${vyos_conf_scripts_dir}/protocols_rip.py"> <properties> <help>Routing Information Protocol (RIP) parameters</help> </properties> diff --git a/interface-definitions/service_console-server.xml.in b/interface-definitions/service_console-server.xml.in index 348d591dd..59a9fe237 100644 --- a/interface-definitions/service_console-server.xml.in +++ b/interface-definitions/service_console-server.xml.in @@ -50,6 +50,7 @@ <regex>(7|8)</regex> </constraint> </properties> + <defaultValue>8</defaultValue> </leafNode> <leafNode name="stop-bits"> <properties> @@ -61,6 +62,7 @@ <regex>(1|2)</regex> </constraint> </properties> + <defaultValue>1</defaultValue> </leafNode> <leafNode name="parity"> <properties> @@ -72,6 +74,7 @@ <regex>(even|odd|none)</regex> </constraint> </properties> + <defaultValue>none</defaultValue> </leafNode> <node name="ssh"> <properties> diff --git a/interface-definitions/ssh.xml.in b/interface-definitions/ssh.xml.in index de926a897..4adfaecfb 100644 --- a/interface-definitions/ssh.xml.in +++ b/interface-definitions/ssh.xml.in @@ -5,7 +5,7 @@ <children> <node name="ssh" owner="${vyos_conf_scripts_dir}/ssh.py"> <properties> - <help>Secure SHell (SSH) protocol</help> + <help>Secure Shell (SSH)</help> <priority>500</priority> </properties> <children> @@ -76,8 +76,12 @@ <properties> <help>Allowed ciphers</help> <completionHelp> - <script>ssh -Q cipher | tr '\n' ' '</script> + <!-- generated by ssh -Q cipher | tr '\n' ' ' as this will not change dynamically --> + <list>3des-cbc aes128-cbc aes192-cbc aes256-cbc rijndael-cbc@lysator.liu.se aes128-ctr aes192-ctr aes256-ctr aes128-gcm@openssh.com aes256-gcm@openssh.com chacha20-poly1305@openssh.com</list> </completionHelp> + <constraint> + <regex>^(3des-cbc|aes128-cbc|aes192-cbc|aes256-cbc|rijndael-cbc@lysator.liu.se|aes128-ctr|aes192-ctr|aes256-ctr|aes128-gcm@openssh.com|aes256-gcm@openssh.com|chacha20-poly1305@openssh.com)$</regex> + </constraint> <multi/> </properties> </leafNode> @@ -97,9 +101,13 @@ <properties> <help>Allowed key exchange (KEX) algorithms</help> <completionHelp> - <script>ssh -Q kex | tr '\n' ' '</script> + <!-- generated by ssh -Q kex | tr '\n' ' ' as this will not change dynamically --> + <list>diffie-hellman-group1-sha1 diffie-hellman-group14-sha1 diffie-hellman-group14-sha256 diffie-hellman-group16-sha512 diffie-hellman-group18-sha512 diffie-hellman-group-exchange-sha1 diffie-hellman-group-exchange-sha256 ecdh-sha2-nistp256 ecdh-sha2-nistp384 ecdh-sha2-nistp521 curve25519-sha256 curve25519-sha256@libssh.org</list> </completionHelp> <multi/> + <constraint> + <regex>^(diffie-hellman-group1-sha1|diffie-hellman-group14-sha1|diffie-hellman-group14-sha256|diffie-hellman-group16-sha512|diffie-hellman-group18-sha512|diffie-hellman-group-exchange-sha1|diffie-hellman-group-exchange-sha256|ecdh-sha2-nistp256|ecdh-sha2-nistp384|ecdh-sha2-nistp521|curve25519-sha256|curve25519-sha256@libssh.org)$</regex> + </constraint> </properties> </leafNode> <leafNode name="listen-address"> @@ -144,13 +152,18 @@ <description>enable logging of failed login attempts</description> </valueHelp> </properties> + <defaultValue>INFO</defaultValue> </leafNode> <leafNode name="mac"> <properties> <help>Allowed message authentication code (MAC) algorithms</help> <completionHelp> - <script>ssh -Q mac | tr '\n' ' '</script> + <!-- generated by ssh -Q mac | tr '\n' ' ' as this will not change dynamically --> + <list>hmac-sha1 hmac-sha1-96 hmac-sha2-256 hmac-sha2-512 hmac-md5 hmac-md5-96 umac-64@openssh.com umac-128@openssh.com hmac-sha1-etm@openssh.com hmac-sha1-96-etm@openssh.com hmac-sha2-256-etm@openssh.com hmac-sha2-512-etm@openssh.com hmac-md5-etm@openssh.com hmac-md5-96-etm@openssh.com umac-64-etm@openssh.com umac-128-etm@openssh.com</list> </completionHelp> + <constraint> + <regex>^(hmac-sha1|hmac-sha1-96|hmac-sha2-256|hmac-sha2-512|hmac-md5|hmac-md5-96|umac-64@openssh.com|umac-128@openssh.com|hmac-sha1-etm@openssh.com|hmac-sha1-96-etm@openssh.com|hmac-sha2-256-etm@openssh.com|hmac-sha2-512-etm@openssh.com|hmac-md5-etm@openssh.com|hmac-md5-96-etm@openssh.com|umac-64-etm@openssh.com|umac-128-etm@openssh.com)$</regex> + </constraint> <multi/> </properties> </leafNode> diff --git a/python/setup.py b/python/setup.py index 9440e7fe7..e2d28bd6b 100644 --- a/python/setup.py +++ b/python/setup.py @@ -1,6 +1,13 @@ import os from setuptools import setup +def packages(directory): + return [ + _[0].replace('/','.') + for _ in os.walk(directory) + if os.path.isfile(os.path.join(_[0], '__init__.py')) + ] + setup( name = "vyos", version = "1.3.0", @@ -10,7 +17,7 @@ setup( license = "LGPLv2+", keywords = "vyos", url = "http://www.vyos.io", - packages=["vyos","vyos.ifconfig"], + packages = packages('vyos'), long_description="VyOS configuration libraries", classifiers=[ "Development Status :: 4 - Beta", @@ -18,4 +25,3 @@ setup( "License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)", ], ) - diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py index 027b5ea8c..a90a66ac3 100644 --- a/python/vyos/ifconfig/wireguard.py +++ b/python/vyos/ifconfig/wireguard.py @@ -149,10 +149,10 @@ class WireGuardIf(Interface): default = { 'type': 'wireguard', 'port': 0, - 'private-key': None, + 'private_key': None, 'pubkey': None, - 'psk': '/dev/null', - 'allowed-ips': [], + 'psk': '', + 'allowed_ips': [], 'fwmark': 0x00, 'endpoint': None, 'keepalive': 0 @@ -166,8 +166,8 @@ class WireGuardIf(Interface): } } options = Interface.options + \ - ['port', 'private-key', 'pubkey', 'psk', - 'allowed-ips', 'fwmark', 'endpoint', 'keepalive'] + ['port', 'private_key', 'pubkey', 'psk', + 'allowed_ips', 'fwmark', 'endpoint', 'keepalive'] """ Wireguard interface class, contains a comnfig dictionary since @@ -180,44 +180,44 @@ class WireGuardIf(Interface): >>> from vyos.ifconfig import WireGuardIf as wg_if >>> wg_intfc = wg_if("wg01") >>> print (wg_intfc.wg_config) - {'private-key': None, 'keepalive': 0, 'endpoint': None, 'port': 0, - 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} + {'private_key': None, 'keepalive': 0, 'endpoint': None, 'port': 0, + 'allowed_ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} >>> wg_intfc.wg_config['keepalive'] = 100 >>> print (wg_intfc.wg_config) - {'private-key': None, 'keepalive': 100, 'endpoint': None, 'port': 0, - 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} + {'private_key': None, 'keepalive': 100, 'endpoint': None, 'port': 0, + 'allowed_ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} """ def update(self): - if not self.config['private-key']: + if not self.config['private_key']: raise ValueError("private key required") else: # fmask permission check? pass - cmd = "wg set {} ".format(self.config['ifname']) - cmd += "listen-port {} ".format(self.config['port']) - cmd += "fwmark {} ".format(str(self.config['fwmark'])) - cmd += "private-key {} ".format(self.config['private-key']) - cmd += "peer {} ".format(self.config['pubkey']) - cmd += " preshared-key {} ".format(self.config['psk']) - cmd += " allowed-ips " - for aip in self.config['allowed-ips']: - if aip != self.config['allowed-ips'][-1]: - cmd += aip + "," - else: - cmd += aip + cmd = 'wg set {ifname}'.format(**self.config) + cmd += ' listen-port {port}'.format(**self.config) + cmd += ' fwmark "{fwmark}" '.format(**self.config) + cmd += ' private-key {private_key}'.format(**self.config) + cmd += ' peer {pubkey}'.format(**self.config) + cmd += ' persistent-keepalive {keepalive}'.format(**self.config) + cmd += ' allowed-ips {}'.format(', '.join(self.config['allowed-ips'])) + if self.config['endpoint']: - cmd += " endpoint '{}'".format(self.config['endpoint']) - cmd += " persistent-keepalive {}".format(self.config['keepalive']) + cmd += ' endpoint "{endpoint}"'.format(**self.config) + + psk_file = '' + if self.config['psk']: + psk_file = '/tmp/{ifname}.psk'.format(**self.config) + with open(psk_file, 'w') as f: + f.write(self.config['psk']) + cmd += f' preshared-key {psk_file}' self._cmd(cmd) - # remove psk since it isn't required anymore and is saved in the cli - # config only !! - if self.config['psk'] != '/dev/null': - if os.path.exists(self.config['psk']): - os.remove(self.config['psk']) + # PSK key file is not required to be stored persistently as its backed by CLI + if os.path.exists(psk_file): + os.remove(psk_file) def remove_peer(self, peerkey): """ diff --git a/python/vyos/xml/.gitignore b/python/vyos/xml/.gitignore new file mode 100644 index 000000000..e934adfd1 --- /dev/null +++ b/python/vyos/xml/.gitignore @@ -0,0 +1 @@ +cache/ diff --git a/python/vyos/xml/__init__.py b/python/vyos/xml/__init__.py new file mode 100644 index 000000000..52f5bfb38 --- /dev/null +++ b/python/vyos/xml/__init__.py @@ -0,0 +1,39 @@ +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or modify it under the terms of +# the GNU Lesser General Public License as published by the Free Software Foundation; +# either version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along with this library; +# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +from vyos.xml import definition +from vyos.xml import load +from vyos.xml import kw + + +def load_configuration(cache=[]): + if cache: + return cache[0] + + xml = definition.XML() + + try: + from vyos.xml.cache import configuration + xml.update(configuration.definition) + cache.append(xml) + except Exception: + xml = definition.XML() + print('no xml configuration cache') + xml.update(load.xml(load.configuration_definition)) + + return xml + + +def defaults(lpath): + return load_configuration().defaults(lpath) diff --git a/python/vyos/xml/cache/__init__.py b/python/vyos/xml/cache/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/python/vyos/xml/cache/__init__.py diff --git a/python/vyos/xml/definition.py b/python/vyos/xml/definition.py new file mode 100644 index 000000000..c5f6b0fc7 --- /dev/null +++ b/python/vyos/xml/definition.py @@ -0,0 +1,301 @@ +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or modify it under the terms of +# the GNU Lesser General Public License as published by the Free Software Foundation; +# either version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along with this library; +# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +from vyos.xml import kw + +# As we index by key, the name is first and then the data: +# {'dummy': { +# '[node]': '[tagNode]', +# 'address': { ... } +# } } + +# so when we encounter a tagNode, we are really encountering +# the tagNode data. + + +class XML(dict): + def __init__(self): + self[kw.tree] = {} + self[kw.priorities] = {} + self[kw.owners] = {} + self[kw.default] = {} + self[kw.tags] = [] + + dict.__init__(self) + + self.tree = self[kw.tree] + # the options which matched the last incomplete world we had + # or the last word in a list + self.options = [] + # store all the part of the command we processed + self.inside = [] + # should we check the data pass with the constraints + self.check = False + # are we still typing a word + self.filling = False + # do what have the tagNode value ? + self.filled = False + # last word seen + self.word = '' + # do we have all the data we want ? + self.final = False + # do we have too much data ? + self.extra = False + # what kind of node are we in plain vs data not + self.plain = True + + def reset(self): + self.tree = self[kw.tree] + self.options = [] + self.inside = [] + self.check = False + self.filling = False + self.filled = False + self.word = '' + self.final = False + self.extra = False + self.plain = True + + # from functools import lru_cache + # @lru_cache(maxsize=100) + # XXX: need to use cachetool instead - for later + + def traverse(self, cmd): + self.reset() + + # using split() intead of split(' ') eats the final ' ' + words = cmd.split(' ') + passed = [] + word = '' + data_node = False + space = False + + while words: + word = words.pop(0) + space = word == '' + perfect = False + if word in self.tree: + passed = [] + perfect = True + self.tree = self.tree[word] + data_node = self.tree[kw.node] + self.inside.append(word) + word = '' + continue + if word and data_node: + passed.append(word) + + is_valueless = self.tree.get(kw.valueless, False) + is_leafNode = data_node == kw.leafNode + is_dataNode = data_node in (kw.leafNode, kw.tagNode) + named_options = [_ for _ in self.tree if not kw.found(_)] + + if is_leafNode: + self.final = is_valueless or len(passed) > 0 + self.extra = is_valueless and len(passed) > 0 + self.check = len(passed) >= 1 + else: + self.final = False + self.extra = False + self.check = len(passed) == 1 and not space + + if self.final: + self.word = ' '.join(passed) + else: + self.word = word + + if self.final: + self.filling = True + else: + self.filling = not perfect and bool(cmd and word != '') + + self.filled = self.final or (is_dataNode and len(passed) > 0 and word == '') + + if is_dataNode and len(passed) == 0: + self.options = [] + elif word: + if data_node != kw.plainNode or len(passed) == 1: + self.options = [_ for _ in self.tree if _.startswith(word)] + else: + self.options = [] + else: + self.options = named_options + + self.plain = not is_dataNode + + # self.debug() + + return self.word + + def speculate(self): + if len(self.options) == 1: + self.tree = self.tree[self.options[0]] + self.word = '' + if self.tree.get(kw.node,'') not in (kw.tagNode, kw.leafNode): + self.options = [_ for _ in self.tree if not kw.found(_)] + + def checks(self, cmd): + # as we move thought the named node twice + # the first time we get the data with the node + # and the second with the pass parameters + xml = self[kw.tree] + + words = cmd.split(' ') + send = True + last = [] + while words: + word = words.pop(0) + if word in xml: + xml = xml[word] + send = True + last = [] + continue + if xml[kw.node] in (kw.tagNode, kw.leafNode): + if kw.constraint in xml: + if send: + yield (word, xml[kw.constraint]) + send = False + else: + last.append((word, None)) + if len(last) >= 2: + yield last[0] + + def summary(self): + yield ('enter', '[ summary ]', str(self.inside)) + + if kw.help not in self.tree: + yield ('skip', '[ summary ]', str(self.inside)) + return + + if self.filled: + return + + yield('', '', '\nHelp:') + + if kw.help in self.tree: + summary = self.tree[kw.help].get(kw.summary) + values = self.tree[kw.help].get(kw.valuehelp, []) + if summary: + yield(summary, '', '') + for value in values: + yield(value[kw.format], value[kw.description], '') + + def constraint(self): + yield ('enter', '[ constraint ]', str(self.inside)) + + if kw.help in self.tree: + yield ('skip', '[ constraint ]', str(self.inside)) + return + if kw.error not in self.tree: + yield ('skip', '[ constraint ]', str(self.inside)) + return + if not self.word or self.filling: + yield ('skip', '[ constraint ]', str(self.inside)) + return + + yield('', '', '\nData Constraint:') + + yield('', 'constraint', str(self.tree[kw.error])) + + def listing(self): + yield ('enter', '[ listing ]', str(self.inside)) + + # only show the details when we passed the tagNode data + if not self.plain and not self.filled: + yield ('skip', '[ listing ]', str(self.inside)) + return + + yield('', '', '\nPossible completions:') + + options = list(self.tree.keys()) + options.sort() + for option in options: + if kw.found(option): + continue + if not option.startswith(self.word): + continue + inner = self.tree[option] + prefix = '+> ' if inner.get(kw.node, '') != kw.leafNode else ' ' + if kw.help in inner: + h = inner[kw.help] + yield (prefix + option, h.get(kw.summary), '') + + def debug(self): + print('------') + print("word '%s'" % self.word) + print("filling " + str(self.filling)) + print("filled " + str(self.filled)) + print("final " + str(self.final)) + print("extra " + str(self.extra)) + print("plain " + str(self.plain)) + print("options " + str(self.options)) + + # from functools import lru_cache + # @lru_cache(maxsize=100) + # XXX: need to use cachetool instead - for later + + def defaults(self, lpath): + d = self[kw.default] + for k in lpath: + d = d[k] + r = {} + + def _flatten(inside, index, d, r): + local = inside[index:] + prefix = '_'.join(_.replace('-','_') for _ in local) + '_' if local else '' + for k in d: + under = prefix + k.replace('-','_') + level = inside + [k] + if isinstance(d[k],dict): + _flatten(level, index, d[k], r) + continue + if self.is_multi(level): + r[under] = [_.strip() for _ in d[k].split(',')] + continue + r[under] = d[k] + + _flatten(lpath, len(lpath), d, r) + return r + + # from functools import lru_cache + # @lru_cache(maxsize=100) + # XXX: need to use cachetool instead - for later + + def _tree(self, lpath): + """ + returns the part of the tree searched or None if it does not exists + """ + tree = self[kw.tree] + spath = lpath.copy() + while spath: + p = spath.pop(0) + if p not in tree: + return None + tree = tree[p] + return tree + + def _get(self, lpath, tag): + return self._tree(lpath + [tag]) + + def is_multi(self, lpath): + return self._get(lpath, kw.multi) is True + + def is_tag(self, lpath): + return self._get(lpath, kw.node) == kw.tagNode + + def is_leaf(self, lpath): + return self._get(lpath, kw.node) == kw.leafNode + + def exists(self, lpath): + return self._get(lpath, kw.node) is not None diff --git a/python/vyos/xml/generate.py b/python/vyos/xml/generate.py new file mode 100755 index 000000000..dfbbadd74 --- /dev/null +++ b/python/vyos/xml/generate.py @@ -0,0 +1,70 @@ + +#!/usr/bin/env python3 + +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or modify it under the terms of +# the GNU Lesser General Public License as published by the Free Software Foundation; +# either version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along with this library; +# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os +import sys +import pprint +import argparse + +from vyos.xml import kw +from vyos.xml import load + + +# import json +# def save_json(fname, loaded): +# with open(fname, 'w') as w: +# print(f'saving {fname}') +# w.write(json.dumps(loaded)) + + +def save_dict(fname, loaded): + with open(fname, 'w') as w: + print(f'saving {fname}') + w.write(f'# generated by {__file__}\n\n') + w.write('definition = ') + w.write(str(loaded)) + + +def main(): + parser = argparse.ArgumentParser(description='generate python file from xml defintions') + parser.add_argument('--conf-folder', type=str, default=load.configuration_definition, help='XML interface definition folder') + parser.add_argument('--conf-cache', type=str, default=load.configuration_cache, help='python file with the conf mode dict') + + # parser.add_argument('--op-folder', type=str, default=load.operational_definition, help='XML interface definition folder') + # parser.add_argument('--op-cache', type=str, default=load.operational_cache, help='python file with the conf mode dict') + + parser.add_argument('--dry', action='store_true', help='dry run, print to screen') + + args = parser.parse_args() + + if os.path.exists(load.configuration_cache): + os.remove(load.configuration_cache) + # if os.path.exists(load.operational_cache): + # os.remove(load.operational_cache) + + conf = load.xml(args.conf_folder) + # op = load.xml(args.op_folder) + + if args.dry: + pprint.pprint(conf) + return + + save_dict(args.conf_cache, conf) + # save_dict(args.op_cache, op) + + +if __name__ == '__main__': + main() diff --git a/python/vyos/xml/kw.py b/python/vyos/xml/kw.py new file mode 100644 index 000000000..c85d9e0fd --- /dev/null +++ b/python/vyos/xml/kw.py @@ -0,0 +1,83 @@ +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or modify it under the terms of +# the GNU Lesser General Public License as published by the Free Software Foundation; +# either version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along with this library; +# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +# all named used as key (keywords) in this module are defined here. +# using variable name will allow the linter to warn on typos +# it separates our dict syntax from the xmldict one, making it easy to change + +# we are redefining a python keyword "list" for ease + + +def found(word): + """ + is the word following the format for a keyword + """ + return word and word[0] == '[' and word[-1] == ']' + + +# root + +version = '(version)' +tree = '(tree)' +priorities = '(priorities)' +owners = '(owners)' +tags = '(tags)' +default = '(default)' + +# nodes + +node = '[node]' + +plainNode = '[plainNode]' +leafNode = '[leafNode]' +tagNode = '[tagNode]' + +owner = '[owner]' + +valueless = '[valueless]' +multi = '[multi]' +hidden = '[hidden]' + +# properties + +priority = '[priority]' + +completion = '[completion]' +list = '[list]' +script = '[script]' +path = '[path]' + +# help + +help = '[help]' + +summary = '[summary]' + +valuehelp = '[valuehelp]' +format = 'format' +description = 'description' + +# constraint + +constraint = '[constraint]' +name = '[name]' + +regex = '[regex]' +validator = '[validator]' +argument = '[argument]' + +error = '[error]' + +# created + +node = '[node]' diff --git a/python/vyos/xml/load.py b/python/vyos/xml/load.py new file mode 100644 index 000000000..1f463a5b7 --- /dev/null +++ b/python/vyos/xml/load.py @@ -0,0 +1,290 @@ +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or modify it under the terms of +# the GNU Lesser General Public License as published by the Free Software Foundation; +# either version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along with this library; +# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import glob + +from os.path import join +from os.path import abspath +from os.path import dirname + +import xmltodict + +from vyos import debug +from vyos.xml import kw +from vyos.xml import definition + + +# where the files are located + +_here = dirname(__file__) + +configuration_definition = abspath(join(_here, '..', '..' ,'..', 'interface-definitions')) +configuration_cache = abspath(join(_here, 'cache', 'configuration.py')) + +operational_definition = abspath(join(_here, '..', '..' ,'..', 'op-mode-definitions')) +operational_cache = abspath(join(_here, 'cache', 'operational.py')) + + +# This code is only ran during the creation of the debian package +# therefore we accept that failure can be fatal and not handled +# gracefully. + + +def _fatal(debug_info=''): + """ + raise a RuntimeError or if in developer mode stop the code + """ + if not debug.enabled('developer'): + raise RuntimeError(str(debug_info)) + + if debug_info: + print(debug_info) + breakpoint() + + +def _safe_update(dict1, dict2): + """ + return a dict made of two, raise if any root key would be overwritten + """ + if set(dict1).intersection(dict2): + raise RuntimeError('overlapping configuration') + return {**dict1, **dict2} + + +def _merge(dict1, dict2): + """ + merge dict2 in to dict1 and return it + """ + for k in list(dict2): + if k not in dict1: + dict1[k] = dict2[k] + continue + if isinstance(dict1[k], dict) and isinstance(dict2[k], dict): + dict1[k] = _merge(dict1[k], dict2[k]) + elif isinstance(dict1[k], dict) and isinstance(dict2[k], dict): + dict1[k].extend(dict2[k]) + elif dict1[k] == dict2[k]: + # A definition shared between multiple files + if k in (kw.valueless, kw.multi, kw.hidden, kw.node, kw.summary, kw.owner, kw.priority): + continue + _fatal() + raise RuntimeError('parsing issue - undefined leaf?') + else: + raise RuntimeError('parsing issue - we messed up?') + return dict1 + + +def _include(fname, folder=''): + """ + return the content of a file, including any file referenced with a #include + """ + if not folder: + folder = dirname(fname) + content = '' + with open(fname, 'r') as r: + for line in r.readlines(): + if '#include' in line: + content += _include(join(folder,line.strip()[10:-1]), folder) + continue + content += line + return content + + +def _format_nodes(inside, conf, xml): + r = {} + while conf: + nodetype = '' + nodename = '' + if 'node' in conf.keys(): + nodetype = 'node' + nodename = kw.plainNode + elif 'leafNode' in conf.keys(): + nodetype = 'leafNode' + nodename = kw.leafNode + elif 'tagNode' in conf.keys(): + nodetype = 'tagNode' + nodename = kw.tagNode + elif 'syntaxVersion' in conf.keys(): + r[kw.version] = conf.pop('syntaxVersion')['@version'] + continue + else: + _fatal(conf.keys()) + + nodes = conf.pop(nodetype) + if isinstance(nodes, list): + for node in nodes: + name = node.pop('@name') + into = inside + [name] + r[name] = _format_node(into, node, xml) + r[name][kw.node] = nodename + xml[kw.tags].append(' '.join(into)) + else: + node = nodes + name = node.pop('@name') + into = inside + [name] + r[name] = _format_node(inside + [name], node, xml) + r[name][kw.node] = nodename + xml[kw.tags].append(' '.join(into)) + return r + + +def _set_validator(r, validator): + v = {} + while validator: + if '@name' in validator: + v[kw.name] = validator.pop('@name') + elif '@argument' in validator: + v[kw.argument] = validator.pop('@argument') + else: + _fatal(validator) + r[kw.constraint][kw.validator].append(v) + + +def _format_node(inside, conf, xml): + r = { + kw.valueless: False, + kw.multi: False, + kw.hidden: False, + } + + if '@owner' in conf: + owner = conf.pop('@owner', '') + r[kw.owner] = owner + xml[kw.owners][' '.join(inside)] = owner + + while conf: + keys = conf.keys() + if 'children' in keys: + children = conf.pop('children') + + if isinstance(conf, list): + for child in children: + r = _safe_update(r, _format_nodes(inside, child, xml)) + else: + child = children + r = _safe_update(r, _format_nodes(inside, child, xml)) + + elif 'properties' in keys: + properties = conf.pop('properties') + + while properties: + if 'help' in properties: + helpname = properties.pop('help') + r[kw.help] = {} + r[kw.help][kw.summary] = helpname + + elif 'valueHelp' in properties: + valuehelps = properties.pop('valueHelp') + if kw.valuehelp in r[kw.help]: + _fatal(valuehelps) + r[kw.help][kw.valuehelp] = [] + if isinstance(valuehelps, list): + for valuehelp in valuehelps: + r[kw.help][kw.valuehelp].append(dict(valuehelp)) + else: + valuehelp = valuehelps + r[kw.help][kw.valuehelp].append(dict(valuehelp)) + + elif 'constraint' in properties: + constraint = properties.pop('constraint') + r[kw.constraint] = {} + while constraint: + if 'regex' in constraint: + regexes = constraint.pop('regex') + if kw.regex in kw.constraint: + _fatal(regexes) + r[kw.constraint][kw.regex] = [] + if isinstance(regexes, list): + r[kw.constraint][kw.regex] = [] + for regex in regexes: + r[kw.constraint][kw.regex].append(regex) + else: + regex = regexes + r[kw.constraint][kw.regex].append(regex) + elif 'validator' in constraint: + validators = constraint.pop('validator') + if kw.validator in r[kw.constraint]: + _fatal(validators) + r[kw.constraint][kw.validator] = [] + if isinstance(validators, list): + for validator in validators: + _set_validator(r, validator) + else: + validator = validators + _set_validator(r, validator) + else: + _fatal(constraint) + + elif 'constraintErrorMessage' in properties: + r[kw.error] = properties.pop('constraintErrorMessage') + + elif 'valueless' in properties: + properties.pop('valueless') + r[kw.valueless] = True + + elif 'multi' in properties: + properties.pop('multi') + r[kw.multi] = True + + elif 'hidden' in properties: + properties.pop('hidden') + r[kw.hidden] = True + + elif 'completionHelp' in properties: + completionHelp = properties.pop('completionHelp') + r[kw.completion] = {} + while completionHelp: + if 'list' in completionHelp: + r[kw.completion][kw.list] = completionHelp.pop('list') + elif 'script' in completionHelp: + r[kw.completion][kw.script] = completionHelp.pop('script') + elif 'path' in completionHelp: + r[kw.completion][kw.path] = completionHelp.pop('path') + else: + _fatal(completionHelp.keys()) + + elif 'priority' in properties: + priority = int(properties.pop('priority')) + r[kw.priority] = priority + xml[kw.priorities].setdefault(priority, []).append(' '.join(inside)) + + else: + _fatal(properties.keys()) + + elif 'defaultValue' in keys: + default = conf.pop('defaultValue') + x = xml[kw.default] + for k in inside[:-1]: + x = x.setdefault(k,{}) + x[inside[-1]] = '' if default is None else default + + else: + _fatal(conf) + + return r + + +def xml(folder): + """ + read all the xml in the folder + """ + xml = definition.XML() + for fname in glob.glob(f'{folder}/*.xml.in'): + parsed = xmltodict.parse(_include(fname)) + formated = _format_nodes([], parsed['interfaceDefinition'], xml) + _merge(xml[kw.tree], formated) + # fix the configuration root node for completion + # as we moved all the name "up" the chain to use them as index. + xml[kw.tree][kw.node] = kw.plainNode + # XXX: do the others + return xml diff --git a/python/vyos/xml/test_xml.py b/python/vyos/xml/test_xml.py new file mode 100644 index 000000000..ac0620d99 --- /dev/null +++ b/python/vyos/xml/test_xml.py @@ -0,0 +1,279 @@ +#!/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 os +import unittest +from unittest import TestCase, mock + +from vyos.xml import load_configuration + +import sys + + +class TestSearch(TestCase): + def setUp(self): + self.xml = load_configuration() + + def test_(self): + last = self.xml.traverse("") + self.assertEqual(last, '') + self.assertEqual(self.xml.inside, []) + self.assertEqual(self.xml.options, ['protocols', 'service', 'system', 'firewall', 'interfaces', 'vpn', 'nat', 'vrf', 'high-availability']) + self.assertEqual(self.xml.filling, False) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, False) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, True) + + def test_i(self): + last = self.xml.traverse("i") + self.assertEqual(last, 'i') + self.assertEqual(self.xml.inside, []) + self.assertEqual(self.xml.options, ['interfaces']) + self.assertEqual(self.xml.filling, True) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, False) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, True) + + def test_interfaces(self): + last = self.xml.traverse("interfaces") + self.assertEqual(last, '') + self.assertEqual(self.xml.inside, ['interfaces']) + self.assertEqual(self.xml.options, ['bonding', 'bridge', 'dummy', 'ethernet', 'geneve', 'l2tpv3', 'loopback', 'macsec', 'openvpn', 'pppoe', 'pseudo-ethernet', 'tunnel', 'vxlan', 'wireguard', 'wireless', 'wirelessmodem']) + self.assertEqual(self.xml.filling, False) + self.assertEqual(self.xml.word, '') + self.assertEqual(self.xml.check, False) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, True) + + def test_interfaces_space(self): + last = self.xml.traverse("interfaces ") + self.assertEqual(last, '') + self.assertEqual(self.xml.inside, ['interfaces']) + self.assertEqual(self.xml.options, ['bonding', 'bridge', 'dummy', 'ethernet', 'geneve', 'l2tpv3', 'loopback', 'macsec', 'openvpn', 'pppoe', 'pseudo-ethernet', 'tunnel', 'vxlan', 'wireguard', 'wireless', 'wirelessmodem']) + self.assertEqual(self.xml.filling, False) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, False) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, True) + + def test_interfaces_w(self): + last = self.xml.traverse("interfaces w") + self.assertEqual(last, 'w') + self.assertEqual(self.xml.inside, ['interfaces']) + self.assertEqual(self.xml.options, ['wireguard', 'wireless', 'wirelessmodem']) + self.assertEqual(self.xml.filling, True) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, True) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, True) + + def test_interfaces_ethernet(self): + last = self.xml.traverse("interfaces ethernet") + self.assertEqual(last, '') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, False) + self.assertEqual(self.xml.word, '') + self.assertEqual(self.xml.check, False) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_space(self): + last = self.xml.traverse("interfaces ethernet ") + self.assertEqual(last, '') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, False) + self.assertEqual(self.xml.word, '') + self.assertEqual(self.xml.check, False) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_e(self): + last = self.xml.traverse("interfaces ethernet e") + self.assertEqual(last, 'e') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, True) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, True) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_la(self): + last = self.xml.traverse("interfaces ethernet la") + self.assertEqual(last, 'la') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, True) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, True) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_lan0(self): + last = self.xml.traverse("interfaces ethernet lan0") + self.assertEqual(last, 'lan0') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, True) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, True) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_lan0_space(self): + last = self.xml.traverse("interfaces ethernet lan0 ") + self.assertEqual(last, '') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet']) + self.assertEqual(len(self.xml.options), 19) + self.assertEqual(self.xml.filling, False) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, False) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, True) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_lan0_ad(self): + last = self.xml.traverse("interfaces ethernet lan0 ad") + self.assertEqual(last, 'ad') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet']) + self.assertEqual(self.xml.options, ['address']) + self.assertEqual(self.xml.filling, True) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, False) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_lan0_address(self): + last = self.xml.traverse("interfaces ethernet lan0 address") + self.assertEqual(last, '') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, False) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, False) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_lan0_address_space(self): + last = self.xml.traverse("interfaces ethernet lan0 address ") + self.assertEqual(last, '') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, False) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, False) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_lan0_address_space_11(self): + last = self.xml.traverse("interfaces ethernet lan0 address 1.1") + self.assertEqual(last, '1.1') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, True) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, True) + self.assertEqual(self.xml.final, True) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, True) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_lan0_address_space_1111_32(self): + last = self.xml.traverse("interfaces ethernet lan0 address 1.1.1.1/32") + self.assertEqual(last, '1.1.1.1/32') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, True) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, True) + self.assertEqual(self.xml.final, True) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, True) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_lan0_address_space_1111_32_space(self): + last = self.xml.traverse("interfaces ethernet lan0 address 1.1.1.1/32 ") + self.assertEqual(last, '1.1.1.1/32') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, True) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, True) + self.assertEqual(self.xml.final, True) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, True) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_lan0_address_space_1111_32_space_text(self): + last = self.xml.traverse("interfaces ethernet lan0 address 1.1.1.1/32 text") + self.assertEqual(last, '1.1.1.1/32 text') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, True) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, True) + self.assertEqual(self.xml.final, True) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, True) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_lan0_address_space_1111_32_space_text_space(self): + last = self.xml.traverse("interfaces ethernet lan0 address 1.1.1.1/32 text ") + self.assertEqual(last, '1.1.1.1/32 text') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, True) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, True) + self.assertEqual(self.xml.final, True) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, True) + self.assertEqual(self.xml.plain, False) + + # Need to add a check for a valuless leafNode
\ No newline at end of file diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index ab3e073ae..c24c9a7ce 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -275,7 +275,7 @@ def apply(wg): # peer pubkey # setting up the wg interface - w.config['private-key'] = c['pk'] + w.config['private_key'] = c['pk'] for peer in wg['peer']: # peer pubkey @@ -300,13 +300,8 @@ def apply(wg): if peer['persistent_keepalive']: w.config['keepalive'] = peer['persistent_keepalive'] - # maybe move it into ifconfig.py - # preshared-key - needs to be read from a file if peer['psk']: - psk_file = '/config/auth/wireguard/psk' - with open(psk_file, 'w') as f: - f.write(peer['psk']) - w.config['psk'] = psk_file + w.config['psk'] = peer['psk'] w.update() diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py new file mode 100755 index 000000000..c5ac26806 --- /dev/null +++ b/src/conf_mode/protocols_rip.py @@ -0,0 +1,317 @@ +#!/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 os + +from sys import exit + +from vyos import ConfigError +from vyos.config import Config +from vyos.util import call +from vyos.template import render + +from vyos import airbag +airbag.enable() + +config_file = r'/tmp/ripd.frr' + +def get_config(): + conf = Config() + base = ['protocols', 'rip'] + rip_conf = { + 'rip_conf' : False, + 'default_distance' : [], + 'default_originate' : False, + 'old_rip' : { + 'default_metric' : [], + 'distribute' : {}, + 'neighbors' : {}, + 'networks' : {}, + 'net_distance' : {}, + 'passive_iface' : {}, + 'redist' : {}, + 'route' : {}, + 'ifaces' : {}, + 'timer_garbage' : 120, + 'timer_timeout' : 180, + 'timer_update' : 30 + }, + 'rip' : { + 'default_metric' : None, + 'distribute' : {}, + 'neighbors' : {}, + 'networks' : {}, + 'net_distance' : {}, + 'passive_iface' : {}, + 'redist' : {}, + 'route' : {}, + 'ifaces' : {}, + 'timer_garbage' : 120, + 'timer_timeout' : 180, + 'timer_update' : 30 + } + } + + if not (conf.exists(base) or conf.exists_effective(base)): + return None + + if conf.exists(base): + rip_conf['rip_conf'] = True + + conf.set_level(base) + + # Get default distance + if conf.exists_effective('default-distance'): + rip_conf['old_default_distance'] = conf.return_effective_value('default-distance') + + if conf.exists('default-distance'): + rip_conf['default_distance'] = conf.return_value('default-distance') + + # Get default information originate (originate default route) + if conf.exists_effective('default-information originate'): + rip_conf['old_default_originate'] = True + + if conf.exists('default-information originate'): + rip_conf['default_originate'] = True + + # Get default-metric + if conf.exists_effective('default-metric'): + rip_conf['old_rip']['default_metric'] = conf.return_effective_value('default-metric') + + if conf.exists('default-metric'): + rip_conf['rip']['default_metric'] = conf.return_value('default-metric') + + # Get distribute list interface old_rip + for dist_iface in conf.list_effective_nodes('distribute-list interface'): + # Set level 'distribute-list interface ethX' + conf.set_level((str(base)) + ' distribute-list interface ' + dist_iface) + rip_conf['rip']['distribute'].update({ + dist_iface : { + 'iface_access_list_in': conf.return_effective_value('access-list in'.format(dist_iface)), + 'iface_access_list_out': conf.return_effective_value('access-list out'.format(dist_iface)), + 'iface_prefix_list_in': conf.return_effective_value('prefix-list in'.format(dist_iface)), + 'iface_prefix_list_out': conf.return_effective_value('prefix-list out'.format(dist_iface)) + } + }) + + # Access-list in old_rip + if conf.exists_effective('access-list in'.format(dist_iface)): + rip_conf['old_rip']['iface_access_list_in'] = conf.return_effective_value('access-list in'.format(dist_iface)) + # Access-list out old_rip + if conf.exists_effective('access-list out'.format(dist_iface)): + rip_conf['old_rip']['iface_access_list_out'] = conf.return_effective_value('access-list out'.format(dist_iface)) + # Prefix-list in old_rip + if conf.exists_effective('prefix-list in'.format(dist_iface)): + rip_conf['old_rip']['iface_prefix_list_in'] = conf.return_effective_value('prefix-list in'.format(dist_iface)) + # Prefix-list out old_rip + if conf.exists_effective('prefix-list out'.format(dist_iface)): + rip_conf['old_rip']['iface_prefix_list_out'] = conf.return_effective_value('prefix-list out'.format(dist_iface)) + + conf.set_level(base) + + # Get distribute list interface + for dist_iface in conf.list_nodes('distribute-list interface'): + # Set level 'distribute-list interface ethX' + conf.set_level((str(base)) + ' distribute-list interface ' + dist_iface) + rip_conf['rip']['distribute'].update({ + dist_iface : { + 'iface_access_list_in': conf.return_value('access-list in'.format(dist_iface)), + 'iface_access_list_out': conf.return_value('access-list out'.format(dist_iface)), + 'iface_prefix_list_in': conf.return_value('prefix-list in'.format(dist_iface)), + 'iface_prefix_list_out': conf.return_value('prefix-list out'.format(dist_iface)) + } + }) + + # Access-list in + if conf.exists('access-list in'.format(dist_iface)): + rip_conf['rip']['iface_access_list_in'] = conf.return_value('access-list in'.format(dist_iface)) + # Access-list out + if conf.exists('access-list out'.format(dist_iface)): + rip_conf['rip']['iface_access_list_out'] = conf.return_value('access-list out'.format(dist_iface)) + # Prefix-list in + if conf.exists('prefix-list in'.format(dist_iface)): + rip_conf['rip']['iface_prefix_list_in'] = conf.return_value('prefix-list in'.format(dist_iface)) + # Prefix-list out + if conf.exists('prefix-list out'.format(dist_iface)): + rip_conf['rip']['iface_prefix_list_out'] = conf.return_value('prefix-list out'.format(dist_iface)) + + conf.set_level((str(base)) + ' distribute-list') + + # Get distribute list, access-list in + if conf.exists_effective('access-list in'): + rip_conf['old_rip']['dist_acl_in'] = conf.return_effective_value('access-list in') + + if conf.exists('access-list in'): + rip_conf['rip']['dist_acl_in'] = conf.return_value('access-list in') + + # Get distribute list, access-list out + if conf.exists_effective('access-list out'): + rip_conf['old_rip']['dist_acl_out'] = conf.return_effective_value('access-list out') + + if conf.exists('access-list out'): + rip_conf['rip']['dist_acl_out'] = conf.return_value('access-list out') + + # Get ditstribute list, prefix-list in + if conf.exists_effective('prefix-list in'): + rip_conf['old_rip']['dist_prfx_in'] = conf.return_effective_value('prefix-list in') + + if conf.exists('prefix-list in'): + rip_conf['rip']['dist_prfx_in'] = conf.return_value('prefix-list in') + + # Get distribute list, prefix-list out + if conf.exists_effective('prefix-list out'): + rip_conf['old_rip']['dist_prfx_out'] = conf.return_effective_value('prefix-list out') + + if conf.exists('prefix-list out'): + rip_conf['rip']['dist_prfx_out'] = conf.return_value('prefix-list out') + + conf.set_level(base) + + # Get network Interfaces + if conf.exists_effective('interface'): + rip_conf['old_rip']['ifaces'] = conf.return_effective_values('interface') + + if conf.exists('interface'): + rip_conf['rip']['ifaces'] = conf.return_values('interface') + + # Get neighbors + if conf.exists_effective('neighbor'): + rip_conf['old_rip']['neighbors'] = conf.return_effective_values('neighbor') + + if conf.exists('neighbor'): + rip_conf['rip']['neighbors'] = conf.return_values('neighbor') + + # Get networks + if conf.exists_effective('network'): + rip_conf['old_rip']['networks'] = conf.return_effective_values('network') + + if conf.exists('network'): + rip_conf['rip']['networks'] = conf.return_values('network') + + # Get network-distance old_rip + for net_dist in conf.list_effective_nodes('network-distance'): + rip_conf['old_rip']['net_distance'].update({ + net_dist : { + 'access_list' : conf.return_effective_value('network-distance {0} access-list'.format(net_dist)), + 'distance' : conf.return_effective_value('network-distance {0} distance'.format(net_dist)), + } + }) + + # Get network-distance + for net_dist in conf.list_nodes('network-distance'): + rip_conf['rip']['net_distance'].update({ + net_dist : { + 'access_list' : conf.return_value('network-distance {0} access-list'.format(net_dist)), + 'distance' : conf.return_value('network-distance {0} distance'.format(net_dist)), + } + }) + + # Get passive-interface + if conf.exists_effective('passive-interface'): + rip_conf['old_rip']['passive_iface'] = conf.return_effective_values('passive-interface') + + if conf.exists('passive-interface'): + rip_conf['rip']['passive_iface'] = conf.return_values('passive-interface') + + # Get redistribute for old_rip + for protocol in conf.list_effective_nodes('redistribute'): + rip_conf['old_rip']['redist'].update({ + protocol : { + 'metric' : conf.return_effective_value('redistribute {0} metric'.format(protocol)), + 'route_map' : conf.return_effective_value('redistribute {0} route-map'.format(protocol)), + } + }) + + # Get redistribute + for protocol in conf.list_nodes('redistribute'): + rip_conf['rip']['redist'].update({ + protocol : { + 'metric' : conf.return_value('redistribute {0} metric'.format(protocol)), + 'route_map' : conf.return_value('redistribute {0} route-map'.format(protocol)), + } + }) + + conf.set_level(base) + + # Get route + if conf.exists_effective('route'): + rip_conf['old_rip']['route'] = conf.return_effective_values('route') + + if conf.exists('route'): + rip_conf['rip']['route'] = conf.return_values('route') + + # Get timers garbage + if conf.exists_effective('timers garbage-collection'): + rip_conf['old_rip']['timer_garbage'] = conf.return_effective_value('timers garbage-collection') + + if conf.exists('timers garbage-collection'): + rip_conf['rip']['timer_garbage'] = conf.return_value('timers garbage-collection') + + # Get timers timeout + if conf.exists_effective('timers timeout'): + rip_conf['old_rip']['timer_timeout'] = conf.return_effective_value('timers timeout') + + if conf.exists('timers timeout'): + rip_conf['rip']['timer_timeout'] = conf.return_value('timers timeout') + + # Get timers update + if conf.exists_effective('timers update'): + rip_conf['old_rip']['timer_update'] = conf.return_effective_value('timers update') + + if conf.exists('timers update'): + rip_conf['rip']['timer_update'] = conf.return_value('timers update') + + return rip_conf + +def verify(rip): + if rip is None: + return None + + # Check for network. If network-distance acl is set and distance not set + for net in rip['rip']['net_distance']: + if not rip['rip']['net_distance'][net]['distance']: + raise ConfigError(f"Must specify distance for network {net}") + +def generate(rip): + if rip is None: + return None + + render(config_file, 'frr/rip.frr.tmpl', rip) + return None + +def apply(rip): + if rip is None: + return None + + if os.path.exists(config_file): + call("sudo vtysh -d ripd -f " + config_file) + os.remove(config_file) + else: + print("File {0} not found".format(config_file)) + + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) + diff --git a/src/conf_mode/service_console-server.py b/src/conf_mode/service_console-server.py index 7f6967983..ace6b8ca4 100755 --- a/src/conf_mode/service_console-server.py +++ b/src/conf_mode/service_console-server.py @@ -22,17 +22,11 @@ from vyos.config import Config from vyos.configdict import dict_merge from vyos.template import render from vyos.util import call +from vyos.xml import defaults from vyos import ConfigError config_file = r'/run/conserver/conserver.cf' -# Default values are necessary until the implementation of T2588 is completed -default_values = { - 'data_bits': '8', - 'parity': 'none', - 'stop_bits': '1' -} - def get_config(): conf = Config() base = ['service', 'console-server'] @@ -52,6 +46,7 @@ def get_config(): # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. + default_values = defaults(base + ['device']) for device in proxy['device'].keys(): tmp = dict_merge(default_values, proxy['device'][device]) proxy['device'][device] = tmp diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index e8777dcad..3149bbb2f 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -248,7 +248,7 @@ def get_config(): conf.set_level(base_path + ['authentication', 'radius', 'server', server]) if conf.exists(['fail-time']): - radius['fail-time'] = conf.return_value(['fail-time']) + radius['fail_time'] = conf.return_value(['fail-time']) if conf.exists(['port']): radius['port'] = conf.return_value(['port']) diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index 7e40be32a..88df2902e 100755 --- a/src/conf_mode/vpn_l2tp.py +++ b/src/conf_mode/vpn_l2tp.py @@ -157,7 +157,7 @@ def get_config(): conf.set_level(base_path + ['authentication', 'radius', 'server', server]) if conf.exists(['fail-time']): - radius['fail-time'] = conf.return_value(['fail-time']) + radius['fail_time'] = conf.return_value(['fail-time']) if conf.exists(['port']): radius['port'] = conf.return_value(['port']) diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py index 5c8b53e1d..4536692d2 100755 --- a/src/conf_mode/vpn_pptp.py +++ b/src/conf_mode/vpn_pptp.py @@ -117,7 +117,7 @@ def get_config(): conf.set_level(base_path + ['authentication', 'radius', 'server', server]) if conf.exists(['fail-time']): - radius['fail-time'] = conf.return_value(['fail-time']) + radius['fail_time'] = conf.return_value(['fail-time']) if conf.exists(['port']): radius['port'] = conf.return_value(['port']) diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index e080ce0dd..4c4d8e403 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -124,7 +124,7 @@ def get_config(): conf.set_level(base_path + ['authentication', 'radius', 'server', server]) if conf.exists(['fail-time']): - radius['fail-time'] = conf.return_value(['fail-time']) + radius['fail_time'] = conf.return_value(['fail-time']) if conf.exists(['port']): radius['port'] = conf.return_value(['port']) |