diff options
45 files changed, 1584 insertions, 89 deletions
diff --git a/.gitignore b/.gitignore index 9082493d3..ff15b4fbb 100644 --- a/.gitignore +++ b/.gitignore @@ -104,7 +104,8 @@ ENV/ .mypy_cache/ # Autogenerated files -templates/* +templates-cfg/* +templates-op/* tests/templates/* # Debian packaging @@ -1,4 +1,5 @@ -TMPL_DIR := templates +TMPL_DIR := templates-cfg +OP_TMPL_DIR := templates-op .PHONY: interface_definitions .ONESHELL: @@ -10,11 +11,27 @@ interface_definitions: # XXX: delete top level node.def's that now live in other packages rm -f $(TMPL_DIR)/system/node.def rm -f $(TMPL_DIR)/service/node.def + rm -f $(TMPL_DIR)/service/dns/node.def rm -f $(TMPL_DIR)/protocols/node.def +.PHONY: op_mode_definitions +.ONESHELL: +op_mode_definitions: + mkdir -p $(OP_TMPL_DIR) + + find $(CURDIR)/op-mode-definitions/ -type f | xargs -I {} $(CURDIR)/scripts/build-command-op-templates {} $(CURDIR)/schema/op-mode-definition.rng $(OP_TMPL_DIR) + + # XXX: delete top level op mode node.def's that now live in other packages + rm -f $(OP_TMPL_DIR)/show/node.def + rm -f $(OP_TMPL_DIR)/show/dns/node.def + rm -f $(OP_TMPL_DIR)/reset/node.def + rm -f $(OP_TMPL_DIR)/restart/node.def + rm -f $(OP_TMPL_DIR)/monitor/node.def + .PHONY: all -all: interface_definitions +all: interface_definitions op_mode_definitions .PHONY: clean clean: rm -rf $(TMPL_DIR)/* + rm -rf $(OP_TMPL_DIR)/* diff --git a/debian/changelog b/debian/changelog index 3db7c422e..0f4183837 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,17 @@ +vyos-1x (1.0.5) unstable; urgency=medium + + * T606: Error in DNS Forwarder listen-on + * T608: Cannot configure broadcast-relay service + + -- Christian Poessinger <christian@poessinger.com> Thu, 19 Apr 2018 21:16:28 +0200 + +vyos-1x (1.0.4) unstable; urgency=medium + + * T560: dns-forwarding: replace dnsmasq with pdns-recursor + * T588: Rewrite 'service dns forwarding' in new XML style format + + -- Christian Poessinger <christian@poessinger.com> Sun, 15 Apr 2018 16:13:32 +0200 + vyos-1x (1.0.3) unstable; urgency=medium * T379: Add UDP broadcast relay support diff --git a/debian/control b/debian/control index a19cc80d4..c31c470e9 100644 --- a/debian/control +++ b/debian/control @@ -2,13 +2,27 @@ Source: vyos-1x Section: contrib/net Priority: extra Maintainer: VyOS Package Maintainers <maintainers@vyos.net> -Build-Depends: debhelper (>= 9), python3, python3-setuptools, quilt, - python3-lxml +Build-Depends: debhelper (>= 9), + quilt, + python3, + python3-setuptools, + quilt, + python3-lxml Standards-Version: 3.9.6 Package: vyos-1x Architecture: all -Depends: python3, ${python3:Depends}, python3-netifaces, - ${shlibs:Depends}, ${misc:Depends} +Depends: python3, + ${python3:Depends}, + python3-netifaces, + python3-jinja2, + python3-pystache, + ipaddrcheck, + tcpdump, + bmon, + hvinfo, + file, + ${shlibs:Depends}, + ${misc:Depends} Description: VyOS configuration scripts and data VyOS configuration scripts, interface definitions, and everything diff --git a/debian/rules b/debian/rules index 5c3aab7f6..ed33706e4 100755 --- a/debian/rules +++ b/debian/rules @@ -2,26 +2,36 @@ DIR := debian/vyos-1x VYOS_SBIN_DIR := opt/vyatta/sbin/ +VYOS_BIN_DIR := opt/vyatta/bin/ VYOS_LIBEXEC_DIR := opt/vyatta/libexec VYOS_CFG_TMPL_DIR := /opt/vyatta/share/vyatta-cfg/templates +VYOS_OP_TMPL_DIR := /opt/vyatta/share/vyatta-op/templates %: dh $@ --with python3, --with quilt override_dh_auto_build: - make + make all override_dh_auto_install: dh_install -pvyos-1x cd python; python3 setup.py install --install-layout=deb --root ../$(DIR); cd .. - # Install configuration scripts + # Install scripts mkdir -p $(DIR)/$(VYOS_SBIN_DIR) + mkdir -p $(DIR)/$(VYOS_BIN_DIR) cp -r src/conf-mode/* $(DIR)/$(VYOS_SBIN_DIR) + cp -r src/op-mode/* $(DIR)/$(VYOS_BIN_DIR) # Install validators mkdir -p $(DIR)/$(VYOS_LIBEXEC_DIR)/validators cp -r src/validators/* $(DIR)/$(VYOS_LIBEXEC_DIR)/validators + # Install helper scripts + cp -r src/helpers/* $(DIR)/$(VYOS_LIBEXEC_DIR)/ + mkdir -p $(DIR)/$(VYOS_CFG_TMPL_DIR) - cp -r templates/* $(DIR)/$(VYOS_CFG_TMPL_DIR) + cp -r templates-cfg/* $(DIR)/$(VYOS_CFG_TMPL_DIR) + + mkdir -p $(DIR)/$(VYOS_OP_TMPL_DIR) + cp -r templates-op/* $(DIR)/$(VYOS_OP_TMPL_DIR) diff --git a/interface-definitions/bcast-relay.xml b/interface-definitions/bcast-relay.xml index e4a009ed4..19f29e340 100644 --- a/interface-definitions/bcast-relay.xml +++ b/interface-definitions/bcast-relay.xml @@ -14,11 +14,13 @@ <properties> <help>Unique ID for each UDP port to forward</help> <valueHelp> - <format>u32:1-99</format> + <format>1-99</format> <description>Numerical ID #</description> </valueHelp> - <type>u32</type> <priority>990</priority> + <constraint> + <validator name="numeric" argument="--range 1-99"/> + </constraint> </properties> <children> <leafNode name="address"> @@ -28,7 +30,9 @@ <format>ipv4</format> <description>Optional source address for forwarded packets</description> </valueHelp> - <type>ipv4</type> + <constraint> + <validator name="ipv4"/> + </constraint> </properties> </leafNode> <leafNode name="description"> @@ -49,10 +53,12 @@ <properties> <help>Destination or source port to listen and retransmit on [REQUIRED]</help> <valueHelp> - <format>u32:1-65535</format> + <format>1-65535</format> <description>UDP port to listen on</description> </valueHelp> - <type>u32</type> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> </properties> </leafNode> </children> diff --git a/interface-definitions/cron.xml b/interface-definitions/cron.xml index a9e9b0401..65b95c5a4 100644 --- a/interface-definitions/cron.xml +++ b/interface-definitions/cron.xml @@ -44,6 +44,9 @@ <format><days>d</format> <description>Execution interval in days</description> </valueHelp> + <constraint> + <regex>[1-9]([0-9]*)([mhd]{0,1})</regex> + </constraint> </properties> </leafNode> <node name="executable"> diff --git a/interface-definitions/dns-forwarding.xml b/interface-definitions/dns-forwarding.xml new file mode 100644 index 000000000..60b90c6a7 --- /dev/null +++ b/interface-definitions/dns-forwarding.xml @@ -0,0 +1,107 @@ +<?xml version="1.0"?> + +<!-- DNS forwarder configuration --> + +<interfaceDefinition> + <node name="service"> + <children> + <node name="dns"> + <children> + <node name="forwarding" owner="${vyos_sbindir}/vyos-config-dns-forwarding.py"> + <properties> + <help>DNS forwarding</help> + <priority>918</priority> + </properties> + <children> + <leafNode name="cache-size"> + <properties> + <help>DNS forwarding cache size</help> + <valueHelp> + <format>0-10000</format> + <description>DNS forwarding cache size</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-10000"/> + </constraint> + </properties> + </leafNode> + <leafNode name="dhcp"> + <properties> + <help>Use nameservers received from DHCP server for specified interface</help> + <completionHelp> + <script>${vyatta_sbindir}/vyatta-interfaces.pl --show all</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + <tagNode name="domain"> + <properties> + <help>DNS domain to forward to a local server</help> + </properties> + <children> + <leafNode name="server"> + <properties> + <help>Domain Name Server (DNS) to forward queries</help> + <valueHelp> + <format>ipv4</format> + <description>Domain Name Server (DNS) IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Domain Name Server (DNS) IPv6 address</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="ignore-hosts-file"> + <properties> + <help>Do not use local /etc/hosts file in name resolution</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="listen-on"> + <properties> + <help>Interface to listen for DNS queries [REQUIRED]</help> + <completionHelp> + <script>${vyatta_sbindir}/vyatta-interfaces.pl --show all</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + <leafNode name="name-server"> + <properties> + <help>Domain Name Server (DNS)</help> + <valueHelp> + <format>ipv4</format> + <description>Domain Name Server (DNS) IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Domain Name Server (DNS) IPv6 address</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="system"> + <properties> + <help>DNS forwarding to system nameservers</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/ntp.xml b/interface-definitions/ntp.xml new file mode 100644 index 000000000..e8cfc539c --- /dev/null +++ b/interface-definitions/ntp.xml @@ -0,0 +1,76 @@ +<?xml version="1.0"?> + +<!-- NTP configuration --> + +<interfaceDefinition> + <node name="system"> + <children> + <node name="ntp" owner="${vyos_sbindir}/vyos-config-ntp.py"> + <properties> + <help>Network Time Protocol (NTP) configuration</help> + <priority>400</priority> + </properties> + <children> + <tagNode name="server"> + <properties> + <help>Network Time Protocol (NTP) server</help> + </properties> + <children> + <leafNode name="dynamic"> + <properties> + <help>Allow server to be configured even if not reachable</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="noselect"> + <properties> + <help>Marks the server as unused</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="preempt"> + <properties> + <help>Specifies the association as preemptable rather than the default persistent</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="prefer"> + <properties> + <help>Marks the server as preferred</help> + <valueless/> + </properties> + </leafNode> + </children> + </tagNode> + <node name="allow-clients"> + <properties> + <help>Network Time Protocol (NTP) server options</help> + <constraint> + <validator name="ip-prefix"/> + </constraint> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IP address</help> + <valueHelp> + <format>ipv4net</format> + <description>IP address and prefix length</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ip-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/ssh.xml b/interface-definitions/ssh.xml new file mode 100644 index 000000000..dfae1d8ed --- /dev/null +++ b/interface-definitions/ssh.xml @@ -0,0 +1,151 @@ +<?xml version="1.0"?> + +<!--SSH configuration --> + +<interfaceDefinition> + <node name="service"> + <children> + <node name="ssh" owner="${vyos_sbindir}/vyos-config-ssh.py"> + <properties> + <help>Secure SHell (SSH) protocol</help> + <priority>500</priority> + </properties> + <children> + <node name="access-control"> + <properties> + <help>SSH user/group access controls. Directives are processed in this: deny-users, allow-users, deny-groups and allow-groups</help> + </properties> + <children> + <node name="allow"> + <children> + <leafNode name="group"> + <properties> + <help>Login is allowed for users whose primary or supplementary group matches</help> + <multi/> + </properties> + </leafNode> + <leafNode name="user"> + <properties> + <help>Login is allowed only for user names that match</help> + <multi/> + </properties> + </leafNode> + </children> + </node> + <node name="deny"> + <children> + <leafNode name="group"> + <properties> + <help>Login is disallowed for users whose primary or supplementary group matches</help> + <multi/> + </properties> + </leafNode> + <leafNode name="user"> + <properties> + <help>Login is disallowed for user names that match</help> + <multi/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + <leafNode name="ciphers"> + <properties> + <help>Specifies allowed Ciphers</help> + <completionHelp> + <script>ssh -Q cipher | tr '\n' ' '</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + <leafNode name="disable-host-validation"> + <properties> + <help>Don't validate the remote host name with DNS</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="disable-password-authentication"> + <properties> + <help>Don't allow unknown user to login with password</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="key-exchange"> + <properties> + <help>Specifies available KEX (Key Exchange) algorithms</help> + <completionHelp> + <script>ssh -Q kex | tr '\n' ' '</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + <leafNode name="listen-address"> + <properties> + <help>Local addresses SSH service should listen on</help> + <valueHelp> + <format>ipv4</format> + <description>IP address to listen for incoming connections</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address to listen for incoming connections</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="loglevel"> + <properties> + <help>Log level</help> + <valueHelp> + <format>QUIET</format> + <description>stay silent</description> + </valueHelp> + <valueHelp> + <format>FATAL</format> + <description>log fatals only</description> + </valueHelp> + <valueHelp> + <format>ERROR</format> + <description>log errors and fatals only</description> + </valueHelp> + <valueHelp> + <format>INFO</format> + <description>default log level</description> + </valueHelp> + <valueHelp> + <format>VERBOSE</format> + <description>enable logging of failed login attempts</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="mac"> + <properties> + <help>Specifies available MAC (message authentication code) algorithms</help> + <completionHelp> + <script>ssh -Q mac | tr '\n' ' '</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + <leafNode name="port"> + <properties> + <help>Port for SSH service</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/bandwidth-monitor.xml b/op-mode-definitions/bandwidth-monitor.xml new file mode 100644 index 000000000..a6ddcfd4b --- /dev/null +++ b/op-mode-definitions/bandwidth-monitor.xml @@ -0,0 +1,23 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="monitor"> + <children> + <node name="bandwidth"> + <properties> + <help>Monitor interface bandwidth in real time</help> + </properties> + <children> + <tagNode name="interface"> + <command>bmon -p $4</command> + <properties> + <help>Monitor bandwidth usage on specified interface</help> + <completionHelp> + <script>${vyos_bindir}/vyos-list-interfaces.py</script> + </completionHelp> + </properties> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/dns-forwarding.xml b/op-mode-definitions/dns-forwarding.xml new file mode 100644 index 000000000..96fa1a6f4 --- /dev/null +++ b/op-mode-definitions/dns-forwarding.xml @@ -0,0 +1,39 @@ +<?xml version="1.0"?> + +<interfaceDefinition> + <node name="show"> + <children> + <node name="dns"> + <children> + <node name="forwarding"> + <properties> + <help>Show DNS forwarding information</help> + </properties> + <children> + <leafNode name="statistics"> + <properties> + <help>Show DNS forwarding statistics</help> + </properties> + <command>${vyos_bindir}/vyos-dns-forwarding-statistics.py</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="restart"> + <children> + <node name="dns"> + <children> + <leafNode name="forwarding"> + <properties> + <help>Restart DNS forwarding service</help> + </properties> + <command>${vyos_bindir}/vyos-restart-dns-forwarding.sh</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/traffic-dump.xml b/op-mode-definitions/traffic-dump.xml new file mode 100644 index 000000000..be53f866b --- /dev/null +++ b/op-mode-definitions/traffic-dump.xml @@ -0,0 +1,45 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="monitor"> + <children> + <node name="traffic"> + <properties> + <help>Monitor traffic dumps</help> + </properties> + <children> + <tagNode name="interface"> + <command>tcpdump -i $4</command> + <properties> + <help>Monitor traffic dump from an interface</help> + <completionHelp> + <script>${vyos_bindir}/vyos-list-dumpable-interfaces.py</script> + </completionHelp> + </properties> + <children> + <tagNode name="filter"> + <command>tcpdump -n -i $4 $6</command> + <properties> + <help>Monitor traffic matching filter conditions</help> + </properties> + </tagNode> + <tagNode name="save"> + <command>tcpdump -n -i $4 -w $6</command> + <properties> + <help>Save traffic dump from an interface to a file</help> + </properties> + <children> + <tagNode name="filter"> + <command>tcpdump -n -i $4 -w $6 $8</command> + <properties> + <help>Save a dump of traffic matching filter conditions to a file</help> + </properties> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/version.xml b/op-mode-definitions/version.xml new file mode 100644 index 000000000..a03b41dcb --- /dev/null +++ b/op-mode-definitions/version.xml @@ -0,0 +1,27 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="version"> + <properties> + <help>Show system version information</help> + </properties> + <command>${vyos_bindir}/vyos-show-version.py</command> + <children> + <leafNode name="funny"> + <properties> + <help>Show system version and some fun stuff</help> + </properties> + <command>${vyos_bindir}/vyos-show-version.py --funny</command> + </leafNode> + <leafNode name="all"> + <properties> + <help>Show system version and versions of all packages</help> + </properties> + <command>${vyos_bindir}/vyos-show-version.py --all</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/python/vyos/__init__.py b/python/vyos/__init__.py index e69de29bb..9b5ed21c9 100644 --- a/python/vyos/__init__.py +++ b/python/vyos/__init__.py @@ -0,0 +1 @@ +from .base import * diff --git a/python/vyos/base.py b/python/vyos/base.py new file mode 100644 index 000000000..6197ed074 --- /dev/null +++ b/python/vyos/base.py @@ -0,0 +1,22 @@ +# Copyright (c) 2018 VyOS maintainers and contributors +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the Software +# is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +# IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +class ConfigError(Exception): + pass diff --git a/python/vyos/limericks.py b/python/vyos/limericks.py new file mode 100644 index 000000000..a0779904a --- /dev/null +++ b/python/vyos/limericks.py @@ -0,0 +1,49 @@ +import random + +limericks = [ + +""" +A programmer who's name was Searle +Once wrote a long program in Perl. +Despite very few quirks +No one got how it works, +Not even the interpreter. +""", + +""" +There was a young lady of Maine +Who set up IPsec VPN. +Problems didn't arise +'til other vendors' device +had to add she to that VPN. +""", + +""" +One day a programmer from York +started his own Vyatta fork. +Though he was a huge geek, +it still took him a week +to get the damn build scripts to work. +""", + +""" +A network admin from Hong Kong +knew MPPE cipher's not strong. +But he was behind NAT, +so he put up we that, +sad network admin from Hong Kong. +""", + +""" +A network admin named Drake +greeted friends with a three-way handshake +and refused to proceed +if they didn't complete it, +that standards-compliant guy Drake. +""" + +] + + +def get_random(): + return limericks[random.randint(0, len(limericks) - 1)] diff --git a/python/vyos/util.py b/python/vyos/version.py index b3eff3965..5d32d878d 100644 --- a/python/vyos/util.py +++ b/python/vyos/version.py @@ -19,10 +19,6 @@ import json -class ConfigError(Exception): - pass - - def get_version_data(file='/opt/vyatta/etc/version.json'): with open(file, 'r') as f: version_data = json.load(f) diff --git a/schema/interface_definition.rng b/schema/interface_definition.rng index 5a0a48845..d1bd9a708 100644 --- a/schema/interface_definition.rng +++ b/schema/interface_definition.rng @@ -169,11 +169,6 @@ </element> </optional> <optional> - <element name="type"> - <text/> - </element> - </optional> - <optional> <!-- These are meaningful only for tag nodes --> <group> <element name="keepChildOrder"> diff --git a/schema/op-mode-definition.rnc b/schema/op-mode-definition.rnc index 01276123c..9c84de0e4 100644 --- a/schema/op-mode-definition.rnc +++ b/schema/op-mode-definition.rnc @@ -52,8 +52,7 @@ tagNode = element tagNode leafNode = element leafNode { nodeNameAttr, - command, - properties + (command & properties) } # Normal and tag nodes may have children @@ -105,4 +104,4 @@ completionHelp = element completionHelp (element list { text })* & (element path { text })* & (element script { text })* -}
\ No newline at end of file +} diff --git a/schema/op-mode-definition.rng b/schema/op-mode-definition.rng index 98a231e53..e9e7887cf 100644 --- a/schema/op-mode-definition.rng +++ b/schema/op-mode-definition.rng @@ -67,7 +67,9 @@ <optional> <ref name="properties"/> </optional> - <ref name="children"/> + <optional> + <ref name="children"/> + </optional> <optional> <ref name="command"/> </optional> @@ -81,8 +83,10 @@ <define name="leafNode"> <element name="leafNode"> <ref name="nodeNameAttr"/> - <ref name="command"/> - <ref name="properties"/> + <interleave> + <ref name="command"/> + <ref name="properties"/> + </interleave> </element> </define> <!-- Normal and tag nodes may have children --> diff --git a/scripts/build-command-op-templates b/scripts/build-command-op-templates index 6b6688fba..865590c2c 100755 --- a/scripts/build-command-op-templates +++ b/scripts/build-command-op-templates @@ -93,7 +93,7 @@ def get_properties(p): try: props["help"] = p.find("help").text except: - pass + props["help"] = "No help available" # Get the completion help strings @@ -113,7 +113,7 @@ def get_properties(p): for i in paths: comp_exprs.append("/bin/cli-shell-api listNodes {0}".format(i.text)) for i in scripts: - comp_exprs.append("sh -c \"{0}\"".format(i.text)) + comp_exprs.append("{0}".format(i.text)) comp_help = " && ".join(comp_exprs) props["comp_help"] = comp_help except: @@ -128,14 +128,6 @@ def make_node_def(props, command): node_def = "" - if "tag" in props: - node_def += "tag:\n" - - - if "type" in props: - node_def += "type: {0}\n".format(props["type"]) - - if "help" in props: node_def += "help: {0}\n".format(props["help"]) @@ -145,7 +137,7 @@ def make_node_def(props, command): if command is not None: - node_def += "run: sudo sh -c {0}\n".format(command.text) + node_def += "run: sudo sh -c \"{0}\"\n".format(command.text) if debug: @@ -173,32 +165,45 @@ def process_node(n, tmpl_dir): props = get_properties(props_elem) - # Type should not be set for non-tag, non-leaf nodes - if node_type != "node": - props["type"] = "txt" + if node_type == "node": + if debug: + print("Processing node {}".format(name)) + + with open(os.path.join(make_path(my_tmpl_dir), "node.def"), "w") as f: + f.write(make_node_def(props, command)) + + if children is not None: + inner_nodes = children.iterfind("*") + for inner_n in inner_nodes: + process_node(inner_n, my_tmpl_dir) if node_type == "tagNode": - props["tag"] = "True" - + if debug: + print("Processing tag node {}".format(name)) - with open(os.path.join(make_path(my_tmpl_dir), "node.def"), "w") as f: - f.write(make_node_def(props, command)) + os.makedirs(make_path(my_tmpl_dir), exist_ok=True) + with open(os.path.join(make_path(my_tmpl_dir), "node.def"), "w") as f: + f.write('help: {0}\0'.format(props['help'])) - if node_type == "node": - inner_nodes = children.iterfind("*") - for inner_n in inner_nodes: - process_node(inner_n, my_tmpl_dir) - if node_type == "tagNode": my_tmpl_dir.append("node.tag") - if debug: - print("Created path for the tagNode:", end="") os.makedirs(make_path(my_tmpl_dir), exist_ok=True) - inner_nodes = children.iterfind("*") - for inner_n in inner_nodes: - process_node(inner_n, my_tmpl_dir) + if debug: + print("Created path for the tagNode: {}".format(make_path(my_tmpl_dir)), end="") + + with open(os.path.join(make_path(my_tmpl_dir), "node.def"), "w") as f: + f.write(make_node_def(props, command)) + + if children is not None: + inner_nodes = children.iterfind("*") + for inner_n in inner_nodes: + process_node(inner_n, my_tmpl_dir) else: # This is a leaf node - pass + if debug: + print("Processing leaf node {}".format(name)) + + with open(os.path.join(make_path(my_tmpl_dir), "node.def"), "w") as f: + f.write(make_node_def(props, command)) root = xml.getroot() diff --git a/scripts/build-command-templates b/scripts/build-command-templates index d1871c1c8..9f51c00cd 100755 --- a/scripts/build-command-templates +++ b/scripts/build-command-templates @@ -49,6 +49,8 @@ schema_file = args.SCHEMA_FILE output_dir = args.OUTPUT_DIR debug = args.debug +debug = True + ## Load and validate the inputs try: @@ -114,27 +116,44 @@ def get_properties(p): except: pass - vce = p.findall("constraint") + vce = p.find("constraint") vc = [] - for v in vce: - if v.find("regex") is not None: - vc.append("pattern $VAR(@) \"{0}\"; {1}".format(v.find("regex").text, error_msg)) - else: - validator = v.find("validator") - v_name = validator.get("name") + + # The old backend doesn't support multiple validators in OR mode + # so we emulate it + + regex_elements = vce.findall("regex") + regexes = [] + if regex_elements is not None: + regexes = list(map(lambda e: e.text, regex_elements)) + + validator_elements = vce.findall("validator") + validators = [] + if validator_elements is not None: + for v in validator_elements: + v_name = os.path.join(validator_dir, v.get("name")) # XXX: lxml returns None for empty arguments v_argument = None try: - v_argument = validator.get("argument") + v_argument = v.get("argument") except: pass if v_argument is None: v_argument = "" - vc.append("exec {0}/{1} {2} $VAR(@); {3}".format(validator_dir, v_name, v_argument, error_msg)) - props["constraints"] = vc - except: - props["constraints"] = [] + + validators.append("{0} {1}".format(v_name, v_argument)) + + + regex_args = " ".join(map(lambda s: "--regex \\\'{0}\\\'".format(s), regexes)) + validator_args = " ".join(map(lambda s: "--exec \\\"{0}\\\"".format(s), validators)) + validator_script = '${vyos_libexecdir}/validate-value.py' + validator_string = "exec \"{0} {1} {2} --value \\\'$VAR(@)\\\'\"; \"{3}\"".format(validator_script, regex_args, validator_args, error_msg) + + props["constraint"] = validator_string + except Exception as exn: + print(exn) + pass # Get the completion help strings try: @@ -165,16 +184,14 @@ def get_properties(p): except: pass - # Get type - try: - props["type"] = p.find("type").text - except: - pass - # Get "multi" if p.find("multi") is not None: props["multi"] = True + # Get "valueless" + if p.find("valueless") is not None: + props["valueless"] = True + return props def make_node_def(props): @@ -190,6 +207,7 @@ def make_node_def(props): node_def += "multi:\n" if "type" in props: + # Will always be txt in practice if it's set node_def += "type: {0}\n".format(props["type"]) if "priority" in props: @@ -205,9 +223,8 @@ def make_node_def(props): if "comp_help" in props: node_def += "allowed: {0}\n".format(props["comp_help"]) - if "constraints" in props: - for c in props["constraints"]: - node_def += "syntax:expression: {0}\n".format(c) + if "constraint" in props: + node_def += "syntax:expression: {0}\n".format(props["constraint"]) if "owner" in props: node_def += "end: sudo sh -c \"{0}\"\n".format(props["owner"]) @@ -238,8 +255,10 @@ def process_node(n, tmpl_dir): if owner: props["owner"] = owner # Type should not be set for non-tag, non-leaf nodes + # For non-valueless leaf nodes, set the type to txt: to make them have some type, + # actual value validation is handled by constraints translated to syntax:expression: if node_type != "node": - if "type" not in props.keys(): + if "valueless" not in props.keys(): props["type"] = "txt" if node_type == "tagNode": props["tag"] = "True" diff --git a/src/conf-mode/vyos-config-bcast-relay.py b/src/conf-mode/vyos-config-bcast-relay.py index 3107dce43..785690d9c 100755 --- a/src/conf-mode/vyos-config-bcast-relay.py +++ b/src/conf-mode/vyos-config-bcast-relay.py @@ -23,7 +23,7 @@ import time import subprocess from vyos.config import Config -from vyos.util import ConfigError +from vyos import ConfigError config_file = r'/etc/default/udp-broadcast-relay' @@ -41,9 +41,9 @@ def get_config(): # split the interface name listing and form a list if conf.exists("{0} interface".format(id)): + intfs_names = [] intfs_names = conf.return_values("{0} interface".format(id)) - intfs_names=intfs_names.replace("'", "") - intfs_names=intfs_names.split() + for name in intfs_names: interface_list.append(name) @@ -90,7 +90,7 @@ def generate(relays): for relay in relays: file = config_file + str(relay["id"]) interfaces = ' '.join(str(intf) for intf in relay["interfaces"]) - config_args = 'DAEMON_ARGS="{0} {1} {2}"\n'.format(relay["id"], relay["port"], interfaces) + config_args = 'DAEMON_ARGS="{0} {1}"\n'.format(relay["port"], interfaces) f = open(file, 'w') f.write(config_header) diff --git a/src/conf-mode/vyos-config-dns-forwarding.py b/src/conf-mode/vyos-config-dns-forwarding.py new file mode 100755 index 000000000..be48cde60 --- /dev/null +++ b/src/conf-mode/vyos-config-dns-forwarding.py @@ -0,0 +1,213 @@ +#!/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 os + +import netifaces +import jinja2 + +from vyos.config import Config +from vyos import ConfigError + +config_file = r'/etc/powerdns/recursor.conf' + +# XXX: pdns recursor doesn't like whitespace near entry separators, +# especially in the semicolon-separated lists of name servers. +# Please be careful if you edit the template. +config_tmpl = """ +### Autogenerated by vyos-config-dns-forwarding.py ### + +# Non-configurable defaults +daemon=yes +threads=1 +allow-from=0.0.0.0/0 +log-common-errors=yes + +# cache-size +max-cache-entries={{ cache_size }} + +# ignore-hosts-file +export-etc-hosts={{ export_hosts_file }} + +# listen-on +local-address={{ listen_on | join(',') }} + +# domain ... server ... +{% if domains -%} + +forward-zones={% for d in domains %} +{{ d.name }}={{ d.servers | join(";") }} +{%- if loop.first %}, {% endif %} +{% endfor %} + +{% endif %} + +# name-server +forward-zones-recurse=.={{ name_servers | join(';') }} + +""" + +default_config_data = { + 'cache_size' : 10000, + 'export_hosts_file': 'yes', + 'listen_on': [], + 'interfaces': [], + 'name_servers': [], + 'domains': [] +} + + +# borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX! +def get_resolvers(file): + resolvers = [] + try: + with open(file, 'r') as resolvconf: + for line in resolvconf.readlines(): + line = line.split('#',1)[0]; + line = line.rstrip(); + if 'nameserver' in line: + resolvers.append(line.split()[1]) + return resolvers + except IOError: + return [] + +def get_config(): + dns = default_config_data + conf = Config() + if not conf.exists('service dns forwarding'): + return None + else: + conf.set_level('service dns forwarding') + + if conf.exists('cache-size'): + cache_size = conf.return_value('cache-size') + dns['cache_size'] = cache_size + + if conf.exists('domain'): + for node in conf.list_nodes('domain'): + server = conf.return_values("domain {0} server".format(node)) + domain = { + "name": node, + "servers": server + } + dns['domains'].append(domain) + + if conf.exists('ignore-hosts-file'): + dns.setdefault('export_hosts_file', "no") + + if conf.exists('name-server'): + name_servers = conf.return_values('name-server') + dns['name_servers'] = dns['name_servers'] + name_servers + + if conf.exists('system'): + conf.set_level('system') + system_name_servers = [] + system_name_servers = conf.return_values('name-server') + if not system_name_servers: + print("DNS forwarding warning: No name-servers set under 'system name-server'\n") + else: + dns['name_servers'] = dns['name_servers'] + system_name_servers + conf.set_level('service dns forwarding') + + ## Hacks and tricks + + # The old VyOS syntax that comes from dnsmasq was "listen-on $interface". + # pdns wants addresses instead, so we emulate it by looking up all addresses + # of a given interface and writing them to the config + if conf.exists('listen-on'): + interfaces = conf.return_values('listen-on') + + listen4 = [] + listen6 = [] + for interface in interfaces: + addrs = netifaces.ifaddresses(interface) + for ip4 in addrs[netifaces.AF_INET]: + listen4.append(ip4['addr']) + + for ip6 in addrs[netifaces.AF_INET6]: + listen6.append(ip6['addr']) + + dns['listen_on'] = listen4 + listen6 + + # Save interfaces in the dict for the reference + dns['interfaces'] = interfaces + + # Add name servers received from DHCP + if conf.exists('dhcp'): + interfaces = [] + interfaces = conf.return_values('dhcp') + for interface in interfaces: + dhcp_resolvers = get_resolvers("/etc/resolv.conf.dhclient-new-{0}".format(interface)) + if dhcp_resolvers: + dns['name_servers'] = dns['name_servers'] + dhcp_resolvers + + return dns + +def verify(dns): + # bail out early - looks like removal from running config + if dns is None: + return None + + if not dns['interfaces']: + raise ConfigError('Error: DNS forwarding requires a configured listen interface!') + + for interface in dns['interfaces']: + try: + netifaces.ifaddresses(interface)[netifaces.AF_INET] + except KeyError as e: + raise ConfigError('Error: Interface {0} has no IP address assigned'.format(interface)) + + if dns['domains']: + for domain in dns['domains']: + if not domain['servers']: + raise ConfigError('Error: No server configured for domain {0}'.format(domain['name'])) + + return None + +def generate(dns): + # bail out early - looks like removal from running config + if dns is None: + return None + + tmpl = jinja2.Template(config_tmpl, trim_blocks=True) + + config_text = tmpl.render(dns) + with open(config_file, 'w') as f: + f.write(config_text) + return None + +def apply(dns): + if dns is not None: + os.system("systemctl restart pdns-recursor") + else: + # DNS forwarding is removed in the commit + os.system("systemctl stop pdns-recursor") + os.unlink(config_file) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf-mode/vyos-config-mdns-repeater.py b/src/conf-mode/vyos-config-mdns-repeater.py index 04a17c126..e648fd64f 100755 --- a/src/conf-mode/vyos-config-mdns-repeater.py +++ b/src/conf-mode/vyos-config-mdns-repeater.py @@ -22,7 +22,7 @@ import netifaces import time from vyos.config import Config -from vyos.util import ConfigError +from vyos import ConfigError config_file = r'/etc/default/mdns-repeater' @@ -35,9 +35,8 @@ def get_config(): return interface_list if conf.exists('interface'): + intfs_names = [] intfs_names = conf.return_values('interface') - intfs_names=intfs_names.replace("'", "") - intfs_names=intfs_names.split() for name in intfs_names: interface_list.append(name) diff --git a/src/conf-mode/vyos-config-ntp.py b/src/conf-mode/vyos-config-ntp.py new file mode 100755 index 000000000..8be12e44e --- /dev/null +++ b/src/conf-mode/vyos-config-ntp.py @@ -0,0 +1,161 @@ +#!/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 os + +import jinja2 +import ipaddress + +from vyos.config import Config +from vyos import ConfigError + +config_file = r'/etc/ntp.conf' + +# Please be careful if you edit the template. +config_tmpl = """ +### Autogenerated by vyos-config-ntp.py ### + +# +# Non-configurable defaults +# +driftfile /var/lib/ntp/ntp.drift +# By default, only allow ntpd to query time sources, ignore any incoming requests +restrict default ignore +# Local users have unrestricted access, allowing reconfiguration via ntpdc +restrict 127.0.0.1 +restrict -6 ::1 + + +# +# Configurable section +# + +{% if servers -%} +{% for s in servers -%} +# Server configuration for: {{ s.name }} +server {{ s.name }} iburst {{ s.options | join(" ") }} + +{% endfor -%} +{% endif %} + +{% if allowed_networks -%} +{% for n in allowed_networks -%} +# Client configuration for network: {{ n.network }} +restrict {{ n.address }} mask {{ n.netmask }} nomodify notrap nopeer + +{% endfor -%} +{% endif %} + +""" + +default_config_data = { + 'servers': [], + 'allowed_networks': [] +} + +def get_config(): + ntp = default_config_data + conf = Config() + if not conf.exists('system ntp'): + return None + else: + conf.set_level('system ntp') + + if conf.exists('allow-clients address'): + networks = conf.return_values('allow-clients address') + for n in networks: + addr = ipaddress.ip_network(n) + net = { + "network" : n, + "address" : addr.network_address, + "netmask" : addr.netmask + } + + ntp['allowed_networks'].append(net) + + if conf.exists('server'): + for node in conf.list_nodes('server'): + options = [] + server = { + "name": node, + "options": [] + } + if conf.exists('server {0} dynamic'.format(node)): + options.append('dynamic') + if conf.exists('server {0} noselect'.format(node)): + options.append('noselect') + if conf.exists('server {0} preempt'.format(node)): + options.append('preempt') + if conf.exists('server {0} prefer'.format(node)): + options.append('prefer') + + server['options'] = options + ntp['servers'].append(server) + + return ntp + +def verify(ntp): + # bail out early - looks like removal from running config + if ntp is None: + return None + + # Configuring allowed clients without a server makes no sense + if len(ntp['allowed_networks']) and not len(ntp['servers']): + raise ConfigError('NTP server not configured') + + for n in ntp['allowed_networks']: + try: + addr = ipaddress.ip_network( n['network'] ) + break + except ValueError: + raise ConfigError("{0} does not appear to be a valid IPv4 or IPv6 network, check host bits!".format(n['network'])) + + return None + +def generate(ntp): + # bail out early - looks like removal from running config + if ntp is None: + return None + + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(ntp) + with open(config_file, 'w') as f: + f.write(config_text) + + return None + +def apply(ntp): + if ntp is not None: + os.system('sudo /usr/sbin/invoke-rc.d ntp force-reload') + else: + # NTP suuport is removed in the commit + os.system('sudo /usr/sbin/invoke-rc.d ntp stop') + os.unlink(config_file) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf-mode/vyos-config-ssh.py b/src/conf-mode/vyos-config-ssh.py new file mode 100755 index 000000000..a4857bba9 --- /dev/null +++ b/src/conf-mode/vyos-config-ssh.py @@ -0,0 +1,249 @@ +#!/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 os + +import jinja2 + +from vyos.config import Config +from vyos import ConfigError + +config_file = r'/etc/ssh/sshd_config' + +# Please be careful if you edit the template. +config_tmpl = """ + +### Autogenerated by vyos-config-ssh.py ### + +# Non-configurable defaults +Protocol 2 +HostKey /etc/ssh/ssh_host_rsa_key +HostKey /etc/ssh/ssh_host_dsa_key +HostKey /etc/ssh/ssh_host_ecdsa_key +HostKey /etc/ssh/ssh_host_ed25519_key +UsePrivilegeSeparation yes +KeyRegenerationInterval 3600 +ServerKeyBits 1024 +SyslogFacility AUTH +LoginGraceTime 120 +StrictModes yes +RSAAuthentication yes +PubkeyAuthentication yes +IgnoreRhosts yes +RhostsRSAAuthentication no +HostbasedAuthentication no +PermitEmptyPasswords no +ChallengeResponseAuthentication no +X11Forwarding yes +X11DisplayOffset 10 +PrintMotd no +PrintLastLog yes +TCPKeepAlive yes +Banner /etc/issue.net +Subsystem sftp /usr/lib/openssh/sftp-server +UsePAM yes +HostKey /etc/ssh/ssh_host_key +PermitRootLogin no + +# Specifies whether sshd should look up the remote host name, +# and to check that the resolved host name for the remote IP +# address maps back to the very same IP address. +UseDNS {{ host_validation }} + +# Specifies the port number that sshd listens on. The default is 22. +# Multiple options of this type are permitted. +Port {{ port }} + +# Gives the verbosity level that is used when logging messages from sshd +LogLevel {{ log_level }} + +# Specifies whether password authentication is allowed +PasswordAuthentication {{ password_authentication }} + +{% if listen_on -%} +# Specifies the local addresses sshd should listen on +{% for a in listen_on -%} +ListenAddress {{ a }} +{% endfor -%} +{% endif %} + +{% if ciphers -%} +# Specifies the ciphers allowed. Multiple ciphers must be comma-separated. +# +# NOTE: As of now, there is no 'multi' node for 'ciphers', thus we have only one :/ +Ciphers {{ ciphers | join(",") }} +{% endif %} + +{% if mac -%} +# Specifies the available MAC (message authentication code) algorithms. The MAC +# algorithm is used for data integrity protection. Multiple algorithms must be +# comma-separated. +# +# NOTE: As of now, there is no 'multi' node for 'mac', thus we have only one :/ +MACs {{ mac | join(",") }} +{% endif %} + +{% if key_exchange -%} +# Specifies the available KEX (Key Exchange) algorithms. Multiple algorithms must +# be comma-separated. +# +# NOTE: As of now, there is no 'multi' node for 'key-exchange', thus we have only one :/ +KexAlgorithms {{ key_exchange | join(",") }} +{% endif %} + +{% if allow_users -%} +# This keyword can be followed by a list of user name patterns, separated by spaces. +# If specified, login is allowed only for user names that match one of the patterns. +# Only user names are valid, a numerical user ID is not recognized. +AllowUsers {{ allow_users | join(" ") }} +{% endif %} + +{% if allow_groups -%} +# This keyword can be followed by a list of group name patterns, separated by spaces. +# If specified, login is allowed only for users whose primary group or supplementary +# group list matches one of the patterns. Only group names are valid, a numerical group +# ID is not recognized. +AllowGroups {{ allow_groups | join(" ") }} +{% endif %} + +{% if deny_users -%} +# This keyword can be followed by a list of user name patterns, separated by spaces. +# Login is disallowed for user names that match one of the patterns. Only user names +# are valid, a numerical user ID is not recognized. +DenyUsers {{ deny_users | join(" ") }} +{% endif %} + +{% if deny_groups -%} +# This keyword can be followed by a list of group name patterns, separated by spaces. +# Login is disallowed for users whose primary group or supplementary group list matches +# one of the patterns. Only group names are valid, a numerical group ID is not recognized. +DenyGroups {{ deny_groups | join(" ") }} +{% endif %} +""" + +default_config_data = { + 'port' : '22', + 'log_level': 'INFO', + 'password_authentication': 'yes', + 'host_validation': 'yes' +} + +def get_config(): + ssh = default_config_data + conf = Config() + if not conf.exists('service ssh'): + return None + else: + conf.set_level('service ssh') + + if conf.exists('access-control allow user'): + allow_users = conf.return_values('access-control allow user') + ssh.setdefault('allow_users', allow_users) + + if conf.exists('access-control allow group'): + allow_groups = conf.return_values('access-control allow group') + ssh.setdefault('allow_groups', allow_groups) + + if conf.exists('access-control deny user'): + deny_users = conf.return_values('access-control deny user') + ssh.setdefault('deny_users', deny_users) + + if conf.exists('access-control deny group'): + deny_groups = conf.return_values('access-control deny group') + ssh.setdefault('deny_groups', deny_groups) + + if conf.exists('ciphers'): + ciphers = conf.return_values('ciphers') + ssh.setdefault('ciphers', ciphers) + + if conf.exists('disable-host-validation'): + ssh['host_validation'] = 'no' + + if conf.exists('disable-password-authentication'): + ssh['password_authentication'] = 'no' + + if conf.exists('key-exchange'): + kex = conf.return_values('key-exchange') + ssh.setdefault('key_exchange', kex) + + if conf.exists('listen-address'): + # We can listen on both IPv4 and IPv6 addresses + # Maybe there could be a check in the future if the configured IP address + # is configured on this system at all? + addresses = conf.return_values('listen-address') + listen = [] + + for addr in addresses: + listen.append(addr) + + ssh.setdefault('listen_on', listen) + + if conf.exists('loglevel'): + ssh['log_level'] = conf.return_value('loglevel') + + if conf.exists('mac'): + mac = conf.return_values('mac') + ssh.setdefault('mac', mac) + + if conf.exists('port'): + port = conf.return_value('port') + ssh.setdefault('port', port) + + return ssh + +def verify(ssh): + if ssh is None: + return None + + if 'loglevel' in ssh.keys(): + allowed_loglevel = 'QUIET, FATAL, ERROR, INFO, VERBOSE' + if not ssh['loglevel'] in allowed_loglevel: + raise ConfigError('loglevel must be one of "{0}"\n'.format(allowed_loglevel)) + + return None + +def generate(ssh): + if ssh is None: + return None + + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(ssh) + with open(config_file, 'w') as f: + f.write(config_text) + return None + +def apply(ssh): + if ssh is not None and 'port' in ssh.keys(): + os.system("sudo systemctl restart ssh") + else: + # SSH access is removed in the commit + os.system("sudo systemctl stop ssh") + os.unlink(config_file) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf-mode/vyos-update-crontab.py b/src/conf-mode/vyos-update-crontab.py index 2d15de8ea..c19b88007 100755 --- a/src/conf-mode/vyos-update-crontab.py +++ b/src/conf-mode/vyos-update-crontab.py @@ -21,7 +21,7 @@ import re import sys from vyos.config import Config -from vyos.util import ConfigError +from vyos import ConfigError crontab_file = "/etc/cron.d/vyos-crontab" diff --git a/src/helpers/validate-value.py b/src/helpers/validate-value.py new file mode 100755 index 000000000..d702739b5 --- /dev/null +++ b/src/helpers/validate-value.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +import re +import os +import sys +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument('--regex', action='append') +parser.add_argument('--exec', action='append') +parser.add_argument('--value', action='store') + +args = parser.parse_args() + +debug = False + +# Multiple arguments work like logical OR + +try: + for r in args.regex: + if re.fullmatch(r, args.value): + sys.exit(0) +except Exception as exn: + if debug: + print(exn) + else: + pass + +try: + for cmd in args.exec: + cmd = "{0} {1}".format(cmd, args.value) + if debug: + print(cmd) + res = os.system(cmd) + if res == 0: + sys.exit(0) +except Exception as exn: + if debug: + print(exn) + else: + pass + +sys.exit(1) diff --git a/src/op-mode/vyos-dns-forwarding-statistics.py b/src/op-mode/vyos-dns-forwarding-statistics.py new file mode 100755 index 000000000..3d1e30aee --- /dev/null +++ b/src/op-mode/vyos-dns-forwarding-statistics.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +import subprocess +import jinja2 + +PDNS_CMD='/usr/bin/rec_control' + +OUT_TMPL_SRC = """ +DNS forwarding statistics: + +Cache entries: {{ cache_entries -}} +Cache size: {{ cache_size }} kbytes + +""" + + +if __name__ == '__main__': + data = {} + + data['cache_entries'] = subprocess.check_output([PDNS_CMD, 'get cache-entries']).decode() + data['cache_size'] = "{0:.2f}".format( int(subprocess.check_output([PDNS_CMD, 'get cache-bytes']).decode()) / 1024 ) + + tmpl = jinja2.Template(OUT_TMPL_SRC) + print(tmpl.render(data)) diff --git a/src/op-mode/vyos-list-dumpable-interfaces.py b/src/op-mode/vyos-list-dumpable-interfaces.py new file mode 100755 index 000000000..53ee89633 --- /dev/null +++ b/src/op-mode/vyos-list-dumpable-interfaces.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 + +# Extract the list of interfaces available for traffic dumps from tcpdump -D + +import re +import subprocess + +if __name__ == '__main__': + out = subprocess.check_output(['/usr/sbin/tcpdump', '-D']).decode().strip() + out = out.split("\n") + + intfs = " ".join(map(lambda s: re.search(r'\d+\.(\S+)\s', s).group(1), out)) + + print(intfs) diff --git a/src/op-mode/vyos-list-interfaces.py b/src/op-mode/vyos-list-interfaces.py new file mode 100755 index 000000000..59c9dffad --- /dev/null +++ b/src/op-mode/vyos-list-interfaces.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 + +import netifaces + +if __name__ == '__main__': + interfaces = netifaces.interfaces() + + print(" ".join(interfaces)) diff --git a/src/op-mode/vyos-restart-dns-forwarding.sh b/src/op-mode/vyos-restart-dns-forwarding.sh new file mode 100755 index 000000000..12106fcc1 --- /dev/null +++ b/src/op-mode/vyos-restart-dns-forwarding.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +if cli-shell-api exists service dns forwarding; then + echo "Restarting the DNS forwarding service" + systemctl restart pdns-recursor +else + echo "DNS forwarding is not configured" +fi diff --git a/src/op-mode/vyos-show-version.py b/src/op-mode/vyos-show-version.py new file mode 100755 index 000000000..ce3b3b54f --- /dev/null +++ b/src/op-mode/vyos-show-version.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2016 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/>. +# +# File: vyos-show-version +# Purpose: +# Displays image version and system information. +# Used by the "run show version" command. + + +import os +import sys +import subprocess +import argparse +import json + +import pystache + +import vyos.version +import vyos.limericks + + +parser = argparse.ArgumentParser() +parser.add_argument("-a", "--all", action="store_true", help="Include individual package versions") +parser.add_argument("-f", "--funny", action="store_true", help="Add something funny to the output") +parser.add_argument("-j", "--json", action="store_true", help="Produce JSON output") + +def read_file(name): + try: + with open (name, "r") as f: + data = f.read() + return data.strip() + except: + # This works since we only read /sys/class/* stuff + # with this function + return "Unknown" + +version_output_tmpl = """ +Version: VyOS {{version}} +Built by: {{built_by}} +Built on: {{built_on}} +Build ID: {{build_id}} + +Architecture: {{system_arch}} +Boot via: {{boot_via}} +System type: {{system_type}} + +Hardware vendor: {{hardware_vendor}} +Hardware model: {{hardware_model}} +Hardware S/N: {{hardware_serial}} +Hardware UUID: {{hardware_uuid}} + +Copyright: VyOS maintainers and contributors + +""" + +if __name__ == '__main__': + args = parser.parse_args() + + version_data = vyos.version.get_version_data() + + # Get system architecture (well, kernel architecture rather) + version_data['system_arch'] = subprocess.check_output('uname -m', shell=True).decode().strip() + + + # Get hypervisor name, if any + system_type = "bare metal" + try: + hypervisor = subprocess.check_output('hvinfo 2>/dev/null', shell=True).decode().strip() + system_type = "{0} guest".format(hypervisor) + except subprocess.CalledProcessError: + # hvinfo returns 1 if it cannot detect any hypervisor + pass + version_data['system_type'] = system_type + + + # Get boot type, it can be livecd, installed image, or, possible, a system installed + # via legacy "install system" mechanism + # In installed images, the squashfs image file is named after its image version, + # while on livecd it's just "filesystem.squashfs", that's how we tell a livecd boot + # from an installed image + boot_via = "installed image" + if subprocess.call(""" grep -e '^overlay.*/filesystem.squashfs' /proc/mounts >/dev/null""", shell=True) == 0: + boot_via = "livecd" + elif subprocess.call(""" grep '^overlay /' /proc/mounts >/dev/null """, shell=True) != 0: + boot_via = "legacy non-image installation" + version_data['boot_via'] = boot_via + + + # Get hardware details from DMI + version_data['hardware_vendor'] = read_file('/sys/class/dmi/id/sys_vendor') + version_data['hardware_model'] = read_file('/sys/class/dmi/id/product_name') + + # These two assume script is run as root, normal users can't access those files + version_data['hardware_serial'] = read_file('/sys/class/dmi/id/subsystem/id/product_serial') + version_data['hardware_uuid'] = read_file('/sys/class/dmi/id/subsystem/id/product_uuid') + + + if args.json: + print(json.dumps(version_data)) + sys.exit(0) + else: + output = pystache.render(version_output_tmpl, version_data).strip() + print(output) + + if args.all: + print("Package versions:") + os.system("dpkg -l") + + if args.funny: + print(vyos.limericks.get_random()) diff --git a/src/validators/interface-address b/src/validators/interface-address new file mode 100755 index 000000000..4c203956b --- /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 100755 index 000000000..51fb72c85 --- /dev/null +++ b/src/validators/ip-address @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-any-single $1 diff --git a/src/validators/ip-host b/src/validators/ip-host new file mode 100755 index 000000000..f2906e8cf --- /dev/null +++ b/src/validators/ip-host @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-any-host $1 diff --git a/src/validators/ip-prefix b/src/validators/ip-prefix new file mode 100755 index 000000000..e58aad395 --- /dev/null +++ b/src/validators/ip-prefix @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-any-net $1 diff --git a/src/validators/ipv4-address b/src/validators/ipv4-address new file mode 100755 index 000000000..872a7645a --- /dev/null +++ b/src/validators/ipv4-address @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-ipv4-single $1 diff --git a/src/validators/ipv4-host b/src/validators/ipv4-host new file mode 100755 index 000000000..f42feffa4 --- /dev/null +++ b/src/validators/ipv4-host @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-ipv4-host $1 diff --git a/src/validators/ipv4-prefix b/src/validators/ipv4-prefix new file mode 100755 index 000000000..8ec8a2c45 --- /dev/null +++ b/src/validators/ipv4-prefix @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-ipv4-net $1 diff --git a/src/validators/ipv6-address b/src/validators/ipv6-address new file mode 100755 index 000000000..e5d68d756 --- /dev/null +++ b/src/validators/ipv6-address @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-ipv6-single $1 diff --git a/src/validators/ipv6-host b/src/validators/ipv6-host new file mode 100755 index 000000000..f7a745077 --- /dev/null +++ b/src/validators/ipv6-host @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-ipv6-host $1 diff --git a/src/validators/ipv6-prefix b/src/validators/ipv6-prefix new file mode 100755 index 000000000..e43616350 --- /dev/null +++ b/src/validators/ipv6-prefix @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-ipv6-net $1 |