summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniil Baturin <daniil@baturin.org>2019-09-11 23:14:46 +0200
committerDaniil Baturin <daniil@baturin.org>2019-09-11 23:14:46 +0200
commita9a68a6f1086fd4c978deaf5ddace69c18443756 (patch)
treef3da329903d4e758408851f9b22c4834c130363f
parent501908ae54a1aaae1337673617ebfcc281b02662 (diff)
parent59e5e64cfbb67a5eb1a9d4d21dd54d946897b8d7 (diff)
downloadvyos-1x-a9a68a6f1086fd4c978deaf5ddace69c18443756.tar.gz
vyos-1x-a9a68a6f1086fd4c978deaf5ddace69c18443756.zip
Merge branch 'current' into equuleus
-rw-r--r--.gitignore6
-rw-r--r--Makefile7
-rw-r--r--debian/control5
-rw-r--r--interface-definitions/dns-forwarding.xml26
-rw-r--r--interface-definitions/https.xml18
-rw-r--r--interface-definitions/interfaces-bonding.xml673
-rw-r--r--interface-definitions/interfaces-bridge.xml12
-rw-r--r--interface-definitions/interfaces-dummy.xml55
-rw-r--r--interface-definitions/interfaces-loopback.xml46
-rw-r--r--interface-definitions/interfaces-openvpn.xml2
-rw-r--r--interface-definitions/interfaces-vxlan.xml151
-rw-r--r--interface-definitions/interfaces-wireguard.xml8
-rw-r--r--op-mode-definitions/bandwidth-test.xml29
-rw-r--r--op-mode-definitions/disks.xml50
-rw-r--r--op-mode-definitions/dns-forwarding.xml3
-rw-r--r--op-mode-definitions/generate-ssh-server-key.xml16
-rw-r--r--op-mode-definitions/ipv4-route.xml125
-rw-r--r--op-mode-definitions/ipv6-route.xml133
-rw-r--r--op-mode-definitions/openvpn.xml64
-rw-r--r--op-mode-definitions/reset-conntrack.xml16
-rw-r--r--op-mode-definitions/show-disk.xml23
-rw-r--r--op-mode-definitions/show-history.xml31
-rw-r--r--op-mode-definitions/show-host.xml12
-rw-r--r--op-mode-definitions/telnet.xml30
-rw-r--r--op-mode-definitions/terminal.xml93
-rw-r--r--op-mode-definitions/traceroute.xml53
-rw-r--r--op-mode-definitions/wireguard.xml71
-rw-r--r--python/vyos/configdict.py116
-rw-r--r--python/vyos/configinterface.py153
-rw-r--r--python/vyos/configsession.py3
-rw-r--r--python/vyos/configtree.py8
-rw-r--r--python/vyos/defaults.py2
-rw-r--r--python/vyos/hostsd_client.py69
-rw-r--r--python/vyos/ifconfig.py1449
-rw-r--r--python/vyos/interfaceconfig.py376
-rw-r--r--python/vyos/interfaces.py11
-rw-r--r--python/vyos/util.py23
-rw-r--r--python/vyos/validate.py6
-rwxr-xr-xsrc/completion/list_disks.py36
-rwxr-xr-xsrc/completion/list_disks.sh5
-rwxr-xr-xsrc/completion/list_interfaces.py14
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py85
-rwxr-xr-xsrc/conf_mode/host_name.py196
-rwxr-xr-xsrc/conf_mode/http-api.py3
-rwxr-xr-xsrc/conf_mode/https.py59
-rwxr-xr-xsrc/conf_mode/interface-bonding.py469
-rwxr-xr-xsrc/conf_mode/interface-bridge.py234
-rwxr-xr-xsrc/conf_mode/interface-dummy.py115
-rwxr-xr-xsrc/conf_mode/interface-loopback.py101
-rwxr-xr-xsrc/conf_mode/interface-openvpn.py88
-rwxr-xr-xsrc/conf_mode/interface-vxlan.py208
-rwxr-xr-xsrc/conf_mode/interface-wireguard.py541
-rwxr-xr-xsrc/conf_mode/ipsec-settings.py2
-rwxr-xr-xsrc/conf_mode/syslog.py418
-rwxr-xr-xsrc/helpers/vyos-boot-config-loader.py101
-rwxr-xr-xsrc/helpers/vyos-bridge-sync.py53
-rwxr-xr-xsrc/helpers/vyos-sudo.py9
-rwxr-xr-xsrc/migration-scripts/dns-forwarding/0-to-150
-rwxr-xr-xsrc/migration-scripts/dns-forwarding/1-to-278
-rwxr-xr-xsrc/migration-scripts/interfaces/0-to-126
-rwxr-xr-xsrc/migration-scripts/interfaces/1-to-263
-rwxr-xr-xsrc/op_mode/clear_conntrack.py26
-rwxr-xr-xsrc/op_mode/format_disk.py148
-rwxr-xr-xsrc/op_mode/generate_ssh_server_key.py27
-rwxr-xr-xsrc/op_mode/powerctrl.py17
-rwxr-xr-xsrc/op_mode/show_openvpn.py169
-rwxr-xr-xsrc/op_mode/toggle_help_binding.sh25
-rwxr-xr-xsrc/op_mode/wireguard.py188
-rwxr-xr-xsrc/services/vyos-hostsd284
-rwxr-xr-xsrc/system/unpriv-ip2
-rw-r--r--src/systemd/vyos-hostsd.service31
-rwxr-xr-xsrc/utils/vyos-hostsd-client82
72 files changed, 6273 insertions, 1654 deletions
diff --git a/.gitignore b/.gitignore
index b42512134..2d84a4209 100644
--- a/.gitignore
+++ b/.gitignore
@@ -119,3 +119,9 @@ debian/debhelper-build-stamp
# Sonar Cloud
.scannerwork
/.vs
+
+# SlickEdit
+*.vpj
+*.vpw
+*.vpwhist
+*.vtg
diff --git a/Makefile b/Makefile
index ee01e5ad3..ad05acff5 100644
--- a/Makefile
+++ b/Makefile
@@ -12,6 +12,8 @@ interface_definitions:
rm -f $(TMPL_DIR)/firewall/node.def
rm -f $(TMPL_DIR)/interfaces/node.def
rm -f $(TMPL_DIR)/interfaces/bridge/node.tag/ip/node.def
+ rm -f $(TMPL_DIR)/interfaces/bonding/node.tag/ip/node.def
+ rm -f $(TMPL_DIR)/interfaces/vxlan/node.tag/ip/node.def
rm -f $(TMPL_DIR)/protocols/node.def
rm -f $(TMPL_DIR)/protocols/static/node.def
rm -f $(TMPL_DIR)/system/node.def
@@ -32,12 +34,15 @@ op_mode_definitions:
rm -f $(OP_TMPL_DIR)/show/node.def
rm -f $(OP_TMPL_DIR)/show/interfaces/node.def
rm -f $(OP_TMPL_DIR)/show/ip/node.def
- rm -f $(OP_TMPL_DIR)/reset/node.def
+ rm -f $(OP_TMPL_DIR)/show/ip/route/node.def
+ rm -f $(OP_TMPL_DIR)/show/ipv6/node.def
+ rm -f $(OP_TMPL_DIR)/show/ipv6/route/node.def
rm -f $(OP_TMPL_DIR)/restart/node.def
rm -f $(OP_TMPL_DIR)/monitor/node.def
rm -f $(OP_TMPL_DIR)/generate/node.def
rm -f $(OP_TMPL_DIR)/show/vpn/node.def
rm -f $(OP_TMPL_DIR)/show/system/node.def
+ rm -f $(OP_TMPL_DIR)/delete/node.def
.PHONY: all
all: clean interface_definitions op_mode_definitions
diff --git a/debian/control b/debian/control
index a65d0158e..dce463157 100644
--- a/debian/control
+++ b/debian/control
@@ -28,6 +28,7 @@ Depends: python3,
python3-hurry.filesize,
python3-vici (>= 5.7.2),
python3-bottle,
+ python3-zmq,
ipaddrcheck,
tcpdump,
tshark,
@@ -61,6 +62,10 @@ Depends: python3,
openvpn,
openvpn-auth-ldap,
openvpn-auth-radius,
+ mtr-tiny,
+ telnet,
+ traceroute,
+ ssl-cert, nginx-light,
${shlibs:Depends},
${misc:Depends}
Description: VyOS configuration scripts and data
diff --git a/interface-definitions/dns-forwarding.xml b/interface-definitions/dns-forwarding.xml
index 56820608c..a88c174e3 100644
--- a/interface-definitions/dns-forwarding.xml
+++ b/interface-definitions/dns-forwarding.xml
@@ -97,6 +97,23 @@
<valueless/>
</properties>
</leafNode>
+ <leafNode name="allow-from">
+ <properties>
+ <help>Networks allowed to query this server</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-prefix"/>
+ </constraint>
+ </properties>
+ </leafNode>
<leafNode name="listen-address">
<properties>
<help>Addresses to listen for DNS queries [REQUIRED]</help>
@@ -115,15 +132,6 @@
</constraint>
</properties>
</leafNode>
- <leafNode name="listen-on">
- <properties>
- <help>Interface to listen for DNS queries [DEPRECATED]</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py</script>
- </completionHelp>
- <multi/>
- </properties>
- </leafNode>
<leafNode name="negative-ttl">
<properties>
<help>Maximum amount of time negative entries are cached</help>
diff --git a/interface-definitions/https.xml b/interface-definitions/https.xml
index 13d5c43ea..2fb3bf082 100644
--- a/interface-definitions/https.xml
+++ b/interface-definitions/https.xml
@@ -9,7 +9,7 @@
<priority>1001</priority>
</properties>
<children>
- <leafNode name="listen-address">
+ <tagNode name="listen-address">
<properties>
<help>Addresses to listen for HTTPS requests</help>
<valueHelp>
@@ -20,13 +20,25 @@
<format>ipv6</format>
<description>HTTPS IPv6 address</description>
</valueHelp>
- <multi/>
+ <valueHelp>
+ <format>'*'</format>
+ <description>any</description>
+ </valueHelp>
<constraint>
<validator name="ipv4-address"/>
<validator name="ipv6-address"/>
+ <regex>^\\*$</regex>
</constraint>
</properties>
- </leafNode>
+ <children>
+ <leafNode name="server-name">
+ <properties>
+ <help>Server names: exact, wildcard, regex, or '_' (any)</help>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
<node name="certificates">
<properties>
<help>TLS certificates</help>
diff --git a/interface-definitions/interfaces-bonding.xml b/interface-definitions/interfaces-bonding.xml
new file mode 100644
index 000000000..88dbab6ab
--- /dev/null
+++ b/interface-definitions/interfaces-bonding.xml
@@ -0,0 +1,673 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="interfaces">
+ <children>
+ <tagNode name="bonding" owner="${vyos_conf_scripts_dir}/interface-bonding.py">
+ <properties>
+ <help>Bonding interface name</help>
+ <priority>315</priority>
+ <constraint>
+ <regex>bond[0-9]+$</regex>
+ </constraint>
+ <constraintErrorMessage>Bonding interface must be named bondN</constraintErrorMessage>
+ <valueHelp>
+ <format>bondN</format>
+ <description>Bonding interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <leafNode name="address">
+ <properties>
+ <help>IP address</help>
+ <completionHelp>
+ <list>dhcp dhcpv6</list>
+ </completionHelp>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 address and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>dhcp</format>
+ <description>Dynamic Host Configuration Protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>dhcpv6</format>
+ <description>Dynamic Host Configuration Protocol for IPv6</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-cidr"/>
+ <regex>(dhcp|dhcpv6)</regex>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <node name="arp-monitor">
+ <properties>
+ <help>ARP link monitoring parameters</help>
+ </properties>
+ <children>
+ <leafNode name="interval">
+ <properties>
+ <help>ARP link monitoring interval</help>
+ <valueHelp>
+ <format>0-4294967295</format>
+ <description>Specifies the ARP link monitoring frequency in milliseconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="target">
+ <properties>
+ <help>IP address used for ARP monitoring</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Network Time Protocol (NTP) IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="description">
+ <properties>
+ <help>Interface description</help>
+ <constraint>
+ <regex>^.{1,256}$</regex>
+ </constraint>
+ <constraintErrorMessage>Interface description too long (limit 256 characters)</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <node name="dhcp-options">
+ <properties>
+ <help>DHCP options</help>
+ </properties>
+ <children>
+ <leafNode name="client-id">
+ <properties>
+ <help>DHCP client identifier</help>
+ </properties>
+ </leafNode>
+ <leafNode name="host-name">
+ <properties>
+ <help>DHCP client host name (overrides the system host name)</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="dhcpv6-options">
+ <properties>
+ <help>DHCPv6 options</help>
+ <priority>319</priority>
+ </properties>
+ <children>
+ <leafNode name="parameters-only">
+ <properties>
+ <help>Acquire only config parameters, no address</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="temporary">
+ <properties>
+ <help>IPv6 "temporary" address</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="disable-link-detect">
+ <properties>
+ <help>Ignore link state changes</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="disable">
+ <properties>
+ <help>Disable this bridge interface</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="hash-policy">
+ <properties>
+ <help>Bonding transmit hash policy</help>
+ <completionHelp>
+ <list>layer2 layer2+3 layer3+4</list>
+ </completionHelp>
+ <valueHelp>
+ <format>layer2</format>
+ <description>use MAC addresses to generate the hash (802.3ad, default)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>layer2+3</format>
+ <description>combine MAC address and IP address to make hash</description>
+ </valueHelp>
+ <valueHelp>
+ <format>layer3+4</format>
+ <description>combine IP address and port to make hash</description>
+ </valueHelp>
+ <constraint>
+ <regex>(layer2\\+3|layer3\\+4|layer2)</regex>
+ </constraint>
+ <constraintErrorMessage>hash-policy must be layer2 layer2+3 or layer3+4</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <node name="ip">
+ <children>
+ <leafNode name="arp-cache-timeout">
+ <properties>
+ <help>ARP cache entry timeout in seconds</help>
+ <valueHelp>
+ <format>1-86400</format>
+ <description>ARP cache entry timout in seconds (default 30)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-86400"/>
+ </constraint>
+ <constraintErrorMessage>ARP cache entry timeout must be between 1 and 86400 seconds</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="enable-proxy-arp">
+ <properties>
+ <help>Enable proxy-arp on this interface</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="proxy-arp-pvlan">
+ <properties>
+ <help>Enable private VLAN proxy ARP on this interface</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="mac">
+ <properties>
+ <help>Media Access Control (MAC) address</help>
+ <valueHelp>
+ <format>h:h:h:h:h:h</format>
+ <description>Hardware (MAC) address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="mac-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="mode">
+ <properties>
+ <help>Bonding mode</help>
+ <completionHelp>
+ <list>802.3ad active-backup broadcast round-robin transmit-load-balance adaptive-load-balance xor-hash</list>
+ </completionHelp>
+ <valueHelp>
+ <format>802.3ad</format>
+ <description>IEEE 802.3ad Dynamic link aggregation (Default)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>active-backup</format>
+ <description>Fault tolerant: only one slave in the bond is active</description>
+ </valueHelp>
+ <valueHelp>
+ <format>broadcast</format>
+ <description>Fault tolerant: transmits everything on all slave interfaces</description>
+ </valueHelp>
+ <valueHelp>
+ <format>round-robin</format>
+ <description>Load balance: transmit packets in sequential order</description>
+ </valueHelp>
+ <valueHelp>
+ <format>transmit-load-balance</format>
+ <description>Load balance: adapts based on transmit load and speed</description>
+ </valueHelp>
+ <valueHelp>
+ <format>adaptive-load-balance</format>
+ <description>Load balance: adapts based on transmit and receive plus ARP</description>
+ </valueHelp>
+ <valueHelp>
+ <format>xor-hash</format>
+ <description>Distribute based on MAC address</description>
+ </valueHelp>
+ <constraint>
+ <regex>(802.3ad|active-backup|broadcast|round-robin|transmit-load-balance|adaptive-load-balance|xor-hash)</regex>
+ </constraint>
+ <constraintErrorMessage>mode must be 802.3ad, active-backup, broadcast, round-robin, transmit-load-balance, adaptive-load-balance, or xor</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <node name="member">
+ <properties>
+ <help>Bridge member interfaces</help>
+ </properties>
+ <children>
+ <leafNode name="interface">
+ <properties>
+ <help>Member interface name</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py --bondable</script>
+ </completionHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="mtu">
+ <properties>
+ <help>Maximum Transmission Unit (MTU)</help>
+ <valueHelp>
+ <format>68-9000</format>
+ <description>Maximum Transmission Unit</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 68-9000"/>
+ </constraint>
+ <constraintErrorMessage>MTU must be between 68 and 9000</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="primary">
+ <properties>
+ <help>Primary device interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py --bondable</script>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <tagNode name="vif-s">
+ <properties>
+ <help>QinQ TAG-S Virtual Local Area Network (VLAN) ID</help>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4094"/>
+ </constraint>
+ <constraintErrorMessage>VLAN ID must be between 0 and 4094</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="address">
+ <properties>
+ <help>IP address</help>
+ <completionHelp>
+ <list>dhcp dhcpv6</list>
+ </completionHelp>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 address and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>dhcp</format>
+ <description>Dynamic Host Configuration Protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>dhcpv6</format>
+ <description>Dynamic Host Configuration Protocol for IPv6</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-cidr"/>
+ <regex>(dhcp|dhcpv6)</regex>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="description">
+ <properties>
+ <help>Interface description</help>
+ <constraint>
+ <regex>^.{1,256}$</regex>
+ </constraint>
+ <constraintErrorMessage>Interface description too long (limit 256 characters)</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <node name="dhcp-options">
+ <properties>
+ <help>DHCP options</help>
+ </properties>
+ <children>
+ <leafNode name="client-id">
+ <properties>
+ <help>DHCP client identifier</help>
+ </properties>
+ </leafNode>
+ <leafNode name="host-name">
+ <properties>
+ <help>DHCP client host name (overrides the system host name)</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="dhcpv6-options">
+ <properties>
+ <help>DHCPv6 options</help>
+ <priority>319</priority>
+ </properties>
+ <children>
+ <leafNode name="parameters-only">
+ <properties>
+ <help>Acquire only config parameters, no address</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="temporary">
+ <properties>
+ <help>IPv6 "temporary" address</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="disable-link-detect">
+ <properties>
+ <help>Ignore link state changes</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="disable">
+ <properties>
+ <help>Disable this bridge interface</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="ethertype">
+ <properties>
+ <help>Set Ethertype</help>
+ <completionHelp>
+ <list>0x88A8 0x8100</list>
+ </completionHelp>
+ <valueHelp>
+ <format>0x88A8</format>
+ <description>802.1ad</description>
+ </valueHelp>
+ <valueHelp>
+ <format>0x8100</format>
+ <description>802.1q</description>
+ </valueHelp>
+ <constraint>
+ <regex>(0x88A8|0x8100)</regex>
+ </constraint>
+ <constraintErrorMessage>Ethertype must be 0x88A8 or 0x8100</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="mac">
+ <properties>
+ <help>Media Access Control (MAC) address</help>
+ <valueHelp>
+ <format>h:h:h:h:h:h</format>
+ <description>Hardware (MAC) address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="mac-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="mtu">
+ <properties>
+ <help>Maximum Transmission Unit (MTU)</help>
+ <valueHelp>
+ <format>68-9000</format>
+ <description>Maximum Transmission Unit</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 68-9000"/>
+ </constraint>
+ <constraintErrorMessage>MTU must be between 68 and 9000</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <tagNode name="vif-c">
+ <properties>
+ <help>QinQ TAG-C Virtual Local Area Network (VLAN) ID</help>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4094"/>
+ </constraint>
+ <constraintErrorMessage>VLAN ID must be between 0 and 4094</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="address">
+ <properties>
+ <help>IP address</help>
+ <completionHelp>
+ <list>dhcp dhcpv6</list>
+ </completionHelp>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 address and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>dhcp</format>
+ <description>Dynamic Host Configuration Protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>dhcpv6</format>
+ <description>Dynamic Host Configuration Protocol for IPv6</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-cidr"/>
+ <regex>(dhcp|dhcpv6)</regex>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="description">
+ <properties>
+ <help>Interface description</help>
+ <constraint>
+ <regex>^.{1,256}$</regex>
+ </constraint>
+ <constraintErrorMessage>Interface description too long (limit 256 characters)</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <node name="dhcp-options">
+ <properties>
+ <help>DHCP options</help>
+ </properties>
+ <children>
+ <leafNode name="client-id">
+ <properties>
+ <help>DHCP client identifier</help>
+ </properties>
+ </leafNode>
+ <leafNode name="host-name">
+ <properties>
+ <help>DHCP client host name (overrides the system host name)</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="dhcpv6-options">
+ <properties>
+ <help>DHCPv6 options</help>
+ <priority>319</priority>
+ </properties>
+ <children>
+ <leafNode name="parameters-only">
+ <properties>
+ <help>Acquire only config parameters, no address</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="temporary">
+ <properties>
+ <help>IPv6 "temporary" address</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="disable-link-detect">
+ <properties>
+ <help>Ignore link state changes</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="disable">
+ <properties>
+ <help>Disable this bridge interface</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="mac">
+ <properties>
+ <help>Media Access Control (MAC) address</help>
+ <valueHelp>
+ <format>h:h:h:h:h:h</format>
+ <description>Hardware (MAC) address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="mac-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="mtu">
+ <properties>
+ <help>Maximum Transmission Unit (MTU)</help>
+ <valueHelp>
+ <format>68-9000</format>
+ <description>Maximum Transmission Unit</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 68-9000"/>
+ </constraint>
+ <constraintErrorMessage>MTU must be between 68 and 9000</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ <tagNode name="vif">
+ <properties>
+ <help>Virtual Local Area Network (VLAN) ID</help>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4094"/>
+ </constraint>
+ <constraintErrorMessage>VLAN ID must be between 0 and 4094</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="address">
+ <properties>
+ <help>IP address</help>
+ <completionHelp>
+ <list>dhcp dhcpv6</list>
+ </completionHelp>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 address and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>dhcp</format>
+ <description>Dynamic Host Configuration Protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>dhcpv6</format>
+ <description>Dynamic Host Configuration Protocol for IPv6</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-cidr"/>
+ <regex>(dhcp|dhcpv6)</regex>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="description">
+ <properties>
+ <help>Interface description</help>
+ <constraint>
+ <regex>^.{1,256}$</regex>
+ </constraint>
+ <constraintErrorMessage>Interface description too long (limit 256 characters)</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <node name="dhcp-options">
+ <properties>
+ <help>DHCP options</help>
+ </properties>
+ <children>
+ <leafNode name="client-id">
+ <properties>
+ <help>DHCP client identifier</help>
+ </properties>
+ </leafNode>
+ <leafNode name="host-name">
+ <properties>
+ <help>DHCP client host name (overrides the system host name)</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="dhcpv6-options">
+ <properties>
+ <help>DHCPv6 options</help>
+ <priority>319</priority>
+ </properties>
+ <children>
+ <leafNode name="parameters-only">
+ <properties>
+ <help>Acquire only config parameters, no address</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="temporary">
+ <properties>
+ <help>IPv6 "temporary" address</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="disable-link-detect">
+ <properties>
+ <help>Ignore link state changes</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="disable">
+ <properties>
+ <help>Disable this bridge interface</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="mac">
+ <properties>
+ <help>Media Access Control (MAC) address</help>
+ <valueHelp>
+ <format>h:h:h:h:h:h</format>
+ <description>Hardware (MAC) address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="mac-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="mtu">
+ <properties>
+ <help>Maximum Transmission Unit (MTU)</help>
+ <valueHelp>
+ <format>68-9000</format>
+ <description>Maximum Transmission Unit</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 68-9000"/>
+ </constraint>
+ <constraintErrorMessage>MTU must be between 68 and 9000</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/interfaces-bridge.xml b/interface-definitions/interfaces-bridge.xml
index adb525a46..4b82972dc 100644
--- a/interface-definitions/interfaces-bridge.xml
+++ b/interface-definitions/interfaces-bridge.xml
@@ -47,18 +47,17 @@
</leafNode>
<leafNode name="aging">
<properties>
- <help>Interval addresses are retained</help>
+ <help>MAC address aging interval</help>
<valueHelp>
<format>0</format>
- <description>Disable retaining address in bridge (always flood)</description>
+ <description>Disable MAC address learning (always flood)</description>
</valueHelp>
<valueHelp>
<format>10-1000000</format>
- <description>Address aging time for bridge seconds (default 300)</description>
+ <description>MAC address aging time in seconds (default: 300)</description>
</valueHelp>
<constraint>
- <validator name="numeric" argument="--range 0-0"/>
- <validator name="numeric" argument="--range 10-1000000"/>
+ <validator name="numeric" argument="--range 0-0 --range 10-1000000"/>
</constraint>
</properties>
</leafNode>
@@ -117,6 +116,7 @@
<leafNode name="disable">
<properties>
<help>Disable this bridge interface</help>
+ <valueless/>
</properties>
</leafNode>
<leafNode name="forwarding-delay">
@@ -170,7 +170,7 @@
<constraint>
<validator name="numeric" argument="--range 1-86400"/>
</constraint>
- <constraintErrorMessage>Bridge max aging value must be between 6 and 86400 seconds</constraintErrorMessage>
+ <constraintErrorMessage>ARP cache entry timeout must be between 1 and 86400 seconds</constraintErrorMessage>
</properties>
</leafNode>
</children>
diff --git a/interface-definitions/interfaces-dummy.xml b/interface-definitions/interfaces-dummy.xml
new file mode 100644
index 000000000..c9860fe3b
--- /dev/null
+++ b/interface-definitions/interfaces-dummy.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="interfaces">
+ <children>
+ <tagNode name="dummy" owner="${vyos_conf_scripts_dir}/interface-dummy.py">
+ <properties>
+ <help>Dummy interface name</help>
+ <priority>300</priority>
+ <constraint>
+ <regex>dum[0-9]+$</regex>
+ </constraint>
+ <constraintErrorMessage>Dummy interface must be named dumN</constraintErrorMessage>
+ <valueHelp>
+ <format>dumN</format>
+ <description>Dummy interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <leafNode name="address">
+ <properties>
+ <help>IP address</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 address and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <multi/>
+ <constraint>
+ <validator name="ip-cidr"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="description">
+ <properties>
+ <help>Interface description</help>
+ <constraint>
+ <regex>^.{1,256}$</regex>
+ </constraint>
+ <constraintErrorMessage>Interface description too long (limit 256 characters)</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="disable">
+ <properties>
+ <help>Disable interface</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/interfaces-loopback.xml b/interface-definitions/interfaces-loopback.xml
new file mode 100644
index 000000000..267731b1c
--- /dev/null
+++ b/interface-definitions/interfaces-loopback.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="interfaces">
+ <children>
+ <tagNode name="loopback" owner="${vyos_conf_scripts_dir}/interface-loopback.py">
+ <properties>
+ <help>Loopback interface</help>
+ <priority>300</priority>
+ <constraint>
+ <regex>lo$</regex>
+ </constraint>
+ <constraintErrorMessage>Loopback interface must be named lo</constraintErrorMessage>
+ <valueHelp>
+ <format>lo</format>
+ <description>Loopback interface</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <leafNode name="address">
+ <properties>
+ <help>IP address</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 address and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="description">
+ <properties>
+ <help>Interface description</help>
+ <constraint>
+ <regex>^.{1,256}$</regex>
+ </constraint>
+ <constraintErrorMessage>Interface description too long (limit 256 characters)</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/interfaces-openvpn.xml b/interface-definitions/interfaces-openvpn.xml
index bb5c5a965..d282a8773 100644
--- a/interface-definitions/interfaces-openvpn.xml
+++ b/interface-definitions/interfaces-openvpn.xml
@@ -42,7 +42,7 @@
<properties>
<help>Interface to a bridge-group</help>
<completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py -t bridge</script>
+ <script>${vyos_completion_dir}/list_interfaces.py --type bridge</script>
</completionHelp>
</properties>
</leafNode>
diff --git a/interface-definitions/interfaces-vxlan.xml b/interface-definitions/interfaces-vxlan.xml
new file mode 100644
index 000000000..b06c2860c
--- /dev/null
+++ b/interface-definitions/interfaces-vxlan.xml
@@ -0,0 +1,151 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="interfaces">
+ <children>
+ <tagNode name="vxlan" owner="${vyos_conf_scripts_dir}/interface-vxlan.py">
+ <properties>
+ <help>Virtual extensible LAN interface (VXLAN)</help>
+ <priority>460</priority>
+ <constraint>
+ <regex>vxlan[0-9]+$</regex>
+ </constraint>
+ <constraintErrorMessage>VXLAN interface must be named vxlanN</constraintErrorMessage>
+ <valueHelp>
+ <format>vxlanN</format>
+ <description>VXLAN interface name</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <leafNode name="address">
+ <properties>
+ <help>IP address</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 address and prefix length</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 address and prefix length</description>
+ </valueHelp>
+ <multi/>
+ <constraint>
+ <validator name="ip-cidr"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="description">
+ <properties>
+ <help>Interface description</help>
+ <constraint>
+ <regex>^.{1,256}$</regex>
+ </constraint>
+ <constraintErrorMessage>Interface description too long (limit 256 characters)</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="disable">
+ <properties>
+ <help>Disable interface</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="group">
+ <properties>
+ <help>Multicast group address for VXLAN interface</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Multicast group address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="ip">
+ <children>
+ <leafNode name="arp-cache-timeout">
+ <properties>
+ <help>ARP cache entry timeout in seconds</help>
+ <valueHelp>
+ <format>1-86400</format>
+ <description>ARP cache entry timout in seconds (default 30)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-86400"/>
+ </constraint>
+ <constraintErrorMessage>ARP cache entry timeout must be between 1 and 86400 seconds</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="enable-proxy-arp">
+ <properties>
+ <help>Enable proxy-arp on this interface</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="link">
+ <properties>
+ <help>Underlay device of VXLAN interface</help>
+ <valueHelp>
+ <format>interface</format>
+ <description>Interface used for VXLAN underlay</description>
+ </valueHelp>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="mtu">
+ <properties>
+ <help>Maximum Transmission Unit (MTU)</help>
+ <valueHelp>
+ <format>1450-9000</format>
+ <description>Maximum Transmission Unit</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1450-9000"/>
+ </constraint>
+ <constraintErrorMessage>MTU must be between 1450 and 9000</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="remote">
+ <properties>
+ <help>Remote address of VXLAN tunnel</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>Remote address of VXLAN tunnel</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="port">
+ <properties>
+ <help>Destination port of VXLAN tunnel (default: 8472)</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="vni">
+ <properties>
+ <help>Virtual Network Identifier</help>
+ <valueHelp>
+ <format>0-16777214</format>
+ <description>VXLAN virtual network identifier</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-16777214"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/interfaces-wireguard.xml b/interface-definitions/interfaces-wireguard.xml
index 6e2622018..f2a7cc316 100644
--- a/interface-definitions/interfaces-wireguard.xml
+++ b/interface-definitions/interfaces-wireguard.xml
@@ -77,6 +77,14 @@
</constraint>
</properties>
</leafNode>
+ <leafNode name="private-key">
+ <properties>
+ <help>Private key to use on that interface</help>
+ <completionHelp>
+ <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script>
+ </completionHelp>
+ </properties>
+ </leafNode>
<tagNode name="peer">
<properties>
<help>peer alias</help>
diff --git a/op-mode-definitions/bandwidth-test.xml b/op-mode-definitions/bandwidth-test.xml
new file mode 100644
index 000000000..d1e459b17
--- /dev/null
+++ b/op-mode-definitions/bandwidth-test.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="monitor">
+ <children>
+ <node name="bandwidth-test">
+ <properties>
+ <help>Initiate or wait for bandwidth test</help>
+ </properties>
+ <children>
+ <leafNode name="accept">
+ <properties>
+ <help>Wait for bandwidth test connections (port TCP/5001)</help>
+ </properties>
+ <command>iperf -s</command>
+ </leafNode>
+ <tagNode name="initiate">
+ <properties>
+ <help>Initiate a bandwidth test to specified host (port TCP/5001)</help>
+ <completionHelp>
+ <list>&lt;hostname&gt; &lt;x.x.x.x&gt; &lt;h:h:h:h:h:h:h:h&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>iperf -c $4</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/disks.xml b/op-mode-definitions/disks.xml
new file mode 100644
index 000000000..fb39c4f3c
--- /dev/null
+++ b/op-mode-definitions/disks.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="format">
+ <properties>
+ <help>Format a device</help>
+ </properties>
+ <children>
+ <tagNode name="disk">
+ <properties>
+ <help>Format a disk drive</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_disks.py</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="like">
+ <properties>
+ <help>Format this disk the same as another disk</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_disks.py --exclude ${COMP_WORDS[2]}</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/format_disk.py --target $3 --proto $5</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+
+ <node name="show">
+ <children>
+ <tagNode name="disk">
+ <properties>
+ <help>Show status of disk device</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_disks.py</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <leafNode name="format">
+ <properties>
+ <help>Show disk drive formatting</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_disk_format.sh $3</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/dns-forwarding.xml b/op-mode-definitions/dns-forwarding.xml
index ac141174f..785a05e9c 100644
--- a/op-mode-definitions/dns-forwarding.xml
+++ b/op-mode-definitions/dns-forwarding.xml
@@ -42,6 +42,9 @@
</children>
</node>
<node name="reset">
+ <properties>
+ <help>Reset a service</help>
+ </properties>
<children>
<node name="dns">
<properties>
diff --git a/op-mode-definitions/generate-ssh-server-key.xml b/op-mode-definitions/generate-ssh-server-key.xml
new file mode 100644
index 000000000..a6ebf1b78
--- /dev/null
+++ b/op-mode-definitions/generate-ssh-server-key.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="generate">
+ <properties>
+ <help>Generate an object</help>
+ </properties>
+ <children>
+ <node name="ssh-server-key">
+ <properties>
+ <help>Regenerate the host SSH keys and restart the SSH server</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/generate_ssh_server_key.py</command>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/ipv4-route.xml b/op-mode-definitions/ipv4-route.xml
new file mode 100644
index 000000000..d2846a6f2
--- /dev/null
+++ b/op-mode-definitions/ipv4-route.xml
@@ -0,0 +1,125 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <properties>
+ <help>Show system information</help>
+ </properties>
+ <children>
+ <node name="ip">
+ <properties>
+ <help>Show IPv4 information</help>
+ </properties>
+ <children>
+ <leafNode name="groups">
+ <properties>
+ <help>Show IP multicast group membership</help>
+ </properties>
+ <command>netstat -gn4</command>
+ </leafNode>
+
+ <node name="route">
+ <properties>
+ <help>Show IP routes</help>
+ </properties>
+ <children>
+ <node name="cache">
+ <properties>
+ <help>Show kernel route cache</help>
+ </properties>
+ <command>ip -s route list cache</command>
+ </node>
+ <tagNode name="cache">
+ <properties>
+ <help>Show kernel route cache for a given route</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt; &lt;x.x.x.x/x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>ip -s route list cache $5</command>
+ </tagNode>
+ <node name="forward">
+ <properties>
+ <help>Show kernel route table</help>
+ </properties>
+ <command>ip route list</command>
+ </node>
+ <tagNode name="forward">
+ <properties>
+ <help>Show kernel route table for a given route</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt; &lt;x.x.x.x/x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>ip -s route list $5</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+
+ <node name="reset">
+ <properties>
+ <help>Reset a service</help>
+ </properties>
+ <children>
+ <node name="ip">
+ <properties>
+ <help>Reset Internet Protocol (IP) parameters</help>
+ </properties>
+ <children>
+ <node name="arp">
+ <properties>
+ <help>Reset Address Resolution Protocol (ARP) cache</help>
+ </properties>
+ <children>
+ <tagNode name="address">
+ <properties>
+ <help>Reset ARP cache for an IPv4 address</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo /sbin/ip neigh flush to "$5"</command>
+ </tagNode>
+ <tagNode name="interface">
+ <properties>
+ <help>Reset ARP cache for interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <command>sudo /sbin/ip neigh flush dev "$5"</command>
+ </tagNode>
+ </children>
+ </node>
+
+ <node name="route">
+ <properties>
+ <help>Reset IP route</help>
+ </properties>
+ <children>
+ <leafNode name= "cache">
+ <properties>
+ <help>Flush the kernel route cache</help>
+ </properties>
+ <command>sudo /sbin/ip route flush cache</command>
+ </leafNode>
+
+ <tagNode name="cache">
+ <properties>
+ <help>Flush the kernel route cache for a given route</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt; &lt;x.x.x.x/x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo /sbin/ip route flush cache "$5"</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/ipv6-route.xml b/op-mode-definitions/ipv6-route.xml
new file mode 100644
index 000000000..fbf6489ba
--- /dev/null
+++ b/op-mode-definitions/ipv6-route.xml
@@ -0,0 +1,133 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <properties>
+ <help>Show system information</help>
+ </properties>
+ <children>
+ <node name="ipv6">
+ <properties>
+ <help>Show IPv6 routing information</help>
+ </properties>
+ <children>
+ <leafNode name="groups">
+ <properties>
+ <help>Show IPv6 multicast group membership</help>
+ </properties>
+ <command>netstat -gn6</command>
+ </leafNode>
+
+ <leafNode name="neighbors">
+ <properties>
+ <help>Show IPv6 Neighbor Discovery (ND) information</help>
+ </properties>
+ <command>ip -f inet6 neigh list</command>
+ </leafNode>
+
+ <node name="route">
+ <properties>
+ <help>Show IPv6 routes</help>
+ </properties>
+ <children>
+ <node name="cache">
+ <properties>
+ <help>Show kernel IPv6 route cache</help>
+ </properties>
+ <command>ip -s -f inet6 route list cache</command>
+ </node>
+ <tagNode name="cache">
+ <properties>
+ <help>Show kernel IPv6 route cache for a given route</help>
+ <completionHelp>
+ <list>&lt;h:h:h:h:h:h:h:h&gt; &lt;h:h:h:h:h:h:h:h/x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>ip -s -f inet6 route list cache $5</command>
+ </tagNode>
+ <node name="forward">
+ <properties>
+ <help>Show kernel IPv6 route table</help>
+ </properties>
+ <command>ip -f inet6 route list</command>
+ </node>
+ <tagNode name="forward">
+ <properties>
+ <help>Show kernel IPv6 route table for a given route</help>
+ <completionHelp>
+ <list>&lt;h:h:h:h:h:h:h:h&gt; &lt;h:h:h:h:h:h:h:h/x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>ip -s -f inet6 route list $5</command>
+ </tagNode>
+ </children>
+ </node>
+
+ </children>
+ </node>
+ </children>
+ </node>
+
+ <node name="reset">
+ <properties>
+ <help>Reset a service</help>
+ </properties>
+ <children>
+ <node name="ipv6">
+ <properties>
+ <help>Reset Internet Protocol version 6 (IPv6) parameters</help>
+ </properties>
+ <children>
+ <node name="neighbors">
+ <properties>
+ <help>Reset IPv6 Neighbor Discovery (ND) cache</help>
+ </properties>
+ <children>
+ <tagNode name="address">
+ <properties>
+ <help>Reset ND cache for an IPv6 address</help>
+ <completionHelp>
+ <list>&lt;h:h:h:h:h:h:h:h&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ip -f inet6 neigh flush to "$5"</command>
+ </tagNode>
+ <tagNode name="interface">
+ <properties>
+ <help>Reset IPv6 ND cache for interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <command>sudo ip -f inet6 neigh flush dev "$5"</command>
+ </tagNode>
+ </children>
+ </node>
+
+ <node name="route">
+ <properties>
+ <help>Reset IPv6 route</help>
+ </properties>
+ <children>
+ <leafNode name= "cache">
+ <properties>
+ <help>Flush the kernel IPv6 route cache</help>
+ </properties>
+ <command>sudo ip -f inet6 route flush cache</command>
+ </leafNode>
+
+ <tagNode name="cache">
+ <properties>
+ <help>Flush the kernel IPv6 route cache for a given route</help>
+ <completionHelp>
+ <list>&lt;h:h:h:h:h:h:h:h&gt; &lt;h:h:h:h:h:h:h:h/x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ip -f inet6 route flush cache "$5"</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/openvpn.xml b/op-mode-definitions/openvpn.xml
index 4c958257a..368cc9115 100644
--- a/op-mode-definitions/openvpn.xml
+++ b/op-mode-definitions/openvpn.xml
@@ -2,25 +2,25 @@
<interfaceDefinition>
<node name="generate">
<children>
- <node name="openvpn">
- <properties>
- <help>OpenVPN key generation tool</help>
- </properties>
- <children>
- <tagNode name="key">
- <properties>
- <help>Generate shared-secret key with specified file name</help>
- <completionHelp>
- <list>&lt;filename&gt;</list>
- </completionHelp>
- </properties>
- <command>
+ <node name="openvpn">
+ <properties>
+ <help>OpenVPN key generation tool</help>
+ </properties>
+ <children>
+ <tagNode name="key">
+ <properties>
+ <help>Generate shared-secret key with specified file name</help>
+ <completionHelp>
+ <list>&lt;filename&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>
result=1;
key_path=$4
full_path=
# Prepend /config/auth if the path is not absolute
- if echo $key_path | egrep -ve '^/.*' > /dev/null; then
+ if echo $key_path | egrep -ve '^/.*' &gt; /dev/null; then
full_path=/config/auth/$key_path
else
full_path=$key_path
@@ -40,12 +40,15 @@
fi
/usr/libexec/vyos/validators/file-exists --directory /config/auth "$full_path"
</command>
- </tagNode>
- </children>
- </node>
+ </tagNode>
+ </children>
+ </node>
</children>
</node>
<node name="reset">
+ <properties>
+ <help>Reset a service</help>
+ </properties>
<children>
<node name="openvpn">
<children>
@@ -56,7 +59,7 @@
<script>sudo ${vyos_completion_dir}/list_openvpn_clients.py --all</script>
</completionHelp>
</properties>
- <command>echo kill $4 | socat - UNIX-CONNECT:/tmp/openvpn-mgmt-intf > /dev/null</command>
+ <command>echo kill $4 | socat - UNIX-CONNECT:/tmp/openvpn-mgmt-intf &gt; /dev/null</command>
</tagNode>
<tagNode name="interface">
<properties>
@@ -107,6 +110,31 @@
</tagNode>
</children>
</node>
+ <node name="openvpn">
+ <properties>
+ <help>Show OpenVPN information</help>
+ </properties>
+ <children>
+ <leafNode name="client">
+ <properties>
+ <help>Show tunnel status for OpenVPN client interfaces</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=client</command>
+ </leafNode>
+ <leafNode name="server">
+ <properties>
+ <help>Show tunnel status for OpenVPN server interfaces</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=server</command>
+ </leafNode>
+ <leafNode name="site-to-site">
+ <properties>
+ <help>Show tunnel status for OpenVPN site-to-site interfaces</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=site-to-site</command>
+ </leafNode>
+ </children>
+ </node>
</children>
</node>
</interfaceDefinition>
diff --git a/op-mode-definitions/reset-conntrack.xml b/op-mode-definitions/reset-conntrack.xml
new file mode 100644
index 000000000..827ba4af4
--- /dev/null
+++ b/op-mode-definitions/reset-conntrack.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="reset">
+ <properties>
+ <help>Reset a service</help>
+ </properties>
+ <children>
+ <node name="conntrack">
+ <properties>
+ <help>Reset all currently tracked connections</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/clear_conntrack.py</command>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-disk.xml b/op-mode-definitions/show-disk.xml
deleted file mode 100644
index 37da07fbe..000000000
--- a/op-mode-definitions/show-disk.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0"?>
-<interfaceDefinition>
- <node name="show">
- <children>
- <tagNode name="disk">
- <properties>
- <help>Show status of disk device</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_disks.sh</script>
- </completionHelp>
- </properties>
- <children>
- <leafNode name="format">
- <properties>
- <help>Show disk drive formatting</help>
- </properties>
- <command>${vyos_op_scripts_dir}/show_disk_format.sh $3</command>
- </leafNode>
- </children>
- </tagNode>
- </children>
- </node>
-</interfaceDefinition>
diff --git a/op-mode-definitions/show-history.xml b/op-mode-definitions/show-history.xml
new file mode 100644
index 000000000..7fb286264
--- /dev/null
+++ b/op-mode-definitions/show-history.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="history">
+ <properties>
+ <help>Show command history</help>
+ </properties>
+ <command>HISTTIMEFORMAT='%FT%T%z ' HISTFILE="$HOME/.bash_history" \set -o history; history</command>
+ <children>
+ <leafNode name="brief">
+ <properties>
+ <help>Show recent command history</help>
+ </properties>
+ <command>HISTTIMEFORMAT='%FT%T%z ' HISTFILE="$HOME/.bash_history" \set -o history; history 20</command>
+ </leafNode>
+ </children>
+ </node>
+
+ <tagNode name="history">
+ <properties>
+ <help>Show last N commands in history</help>
+ <completionHelp>
+ <list>&lt;NUMBER&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>HISTTIMEFORMAT='%FT%T%z ' HISTFILE="$HOME/.bash_history" \set -o history; history $3</command>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-host.xml b/op-mode-definitions/show-host.xml
index d7f8104aa..eee1288a1 100644
--- a/op-mode-definitions/show-host.xml
+++ b/op-mode-definitions/show-host.xml
@@ -7,6 +7,12 @@
<help>Show host information</help>
</properties>
<children>
+ <leafNode name="date">
+ <properties>
+ <help>Show host current date</help>
+ </properties>
+ <command>/bin/date</command>
+ </leafNode>
<leafNode name="domain">
<properties>
<help>Show domain name</help>
@@ -25,6 +31,12 @@
</properties>
<command>/usr/bin/host $4</command>
</tagNode>
+ <leafNode name="os">
+ <properties>
+ <help>Show host operating system details</help>
+ </properties>
+ <command>/bin/uname -a</command>
+ </leafNode>
</children>
</node>
</children>
diff --git a/op-mode-definitions/telnet.xml b/op-mode-definitions/telnet.xml
new file mode 100644
index 000000000..c5bb6d283
--- /dev/null
+++ b/op-mode-definitions/telnet.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="telnet">
+ <properties>
+ <help>Telnet to a node</help>
+ </properties>
+ <children>
+ <tagNode name="to">
+ <properties>
+ <help>Telnet to a host</help>
+ <completionHelp>
+ <list>&lt;hostname&gt; &lt;x.x.x.x&gt; &lt;h:h:h:h:h:h:h:h&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/telnet $3</command>
+ <children>
+ <tagNode name="port">
+ <properties>
+ <help>Telnet to a host:port</help>
+ <completionHelp>
+ <list>&lt;0-65535&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/telnet $3 $5</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/terminal.xml b/op-mode-definitions/terminal.xml
index db74f867e..9c4e629cb 100644
--- a/op-mode-definitions/terminal.xml
+++ b/op-mode-definitions/terminal.xml
@@ -26,4 +26,97 @@
</node>
</children>
</node>
+ <node name="set">
+ <properties>
+ <help>Set operational options</help>
+ </properties>
+ <children>
+ <tagNode name="builtin">
+ <properties>
+ <help>Bash builtin set command</help>
+ <completionHelp>
+ <list>&lt;OPTION&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>builtin $3</command>
+ </tagNode>
+
+ <node name="console">
+ <properties>
+ <help>Control console behaviors</help>
+ </properties>
+ <children>
+ <leafNode name="keymap">
+ <properties>
+ <help>Reconfigure console keyboard layout</help>
+ </properties>
+ <command>sudo dpkg-reconfigure -f dialog keyboard-configuration &amp;&amp; sudo systemctl restart keyboard-setup</command>
+ </leafNode>
+ </children>
+ </node>
+
+ <node name="terminal">
+ <properties>
+ <help>Control terminal behaviors</help>
+ </properties>
+ <children>
+
+ <node name="key">
+ <properties>
+ <help>Set key behaviors</help>
+ </properties>
+ <children>
+ <tagNode name="query-help">
+ <properties>
+ <help>Enable/disable getting help using question mark (default enabled)</help>
+ <completionHelp>
+ <list>enable disable</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/toggle_help_binding.sh $5</command>
+ </tagNode>
+ </children>
+ </node>
+
+ <node name="pager">
+ <properties>
+ <help>Set terminal pager to default (less)</help>
+ </properties>
+ <command>VYATTA_PAGER=${_vyatta_default_pager}</command>
+ </node>
+ <tagNode name="pager">
+ <properties>
+ <help>Set terminal pager</help>
+ <completionHelp>
+ <list>&lt;PROGRAM&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>VYATTA_PAGER=$4</command>
+ </tagNode>
+
+ <tagNode name="length">
+ <properties>
+ <help>Set terminal to given number of rows (0 disables paging)</help>
+ <completionHelp>
+ <list>&lt;NUMBER&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>if [ "$4" -eq 0 ]; then VYATTA_PAGER=cat; else VYATTA_PAGER=${_vyatta_default_pager}; stty rows $4; fi</command>
+ </tagNode>
+
+ <tagNode name="width">
+ <properties>
+ <help>Set terminal to given number of columns</help>
+ <completionHelp>
+ <list>&lt;NUMBER&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>stty columns $4</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+
+
</interfaceDefinition>
diff --git a/op-mode-definitions/traceroute.xml b/op-mode-definitions/traceroute.xml
new file mode 100644
index 000000000..85f6047c1
--- /dev/null
+++ b/op-mode-definitions/traceroute.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="traceroute">
+ <properties>
+ <help>Track network path to node</help>
+ </properties>
+ <children>
+ <tagNode name="">
+ <properties>
+ <help>Track network path to specified node</help>
+ <completionHelp>
+ <list>&lt;hostname&gt; &lt;x.x.x.x&gt; &lt;h:h:h:h:h:h:h:h&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/traceroute $2</command>
+ </tagNode>
+
+ <tagNode name="ipv4">
+ <properties>
+ <help>Track network path to &lt;hostname|IPv4 address&gt;</help>
+ <completionHelp>
+ <list>&lt;hostname&gt; &lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/traceroute -4 $3</command>
+ </tagNode>
+
+ <tagNode name="ipv6">
+ <properties>
+ <help>Track network path to &lt;hostname|IPv6 address&gt;</help>
+ <completionHelp>
+ <list>&lt;hostname&gt; &lt;h:h:h:h:h:h:h:h&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/traceroute -6 $3</command>
+ </tagNode>
+ </children>
+ </node>
+
+ <node name="monitor">
+ <children>
+ <tagNode name="traceroute">
+ <properties>
+ <help>Monitor the path to a destination in realtime</help>
+ <completionHelp>
+ <list>&lt;hostname&gt; &lt;x.x.x.x&gt; &lt;h:h:h:h:h:h:h:h&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>/usr/bin/mtr $3</command>
+ </tagNode>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/wireguard.xml b/op-mode-definitions/wireguard.xml
index 681bb5f47..c5c4c9914 100644
--- a/op-mode-definitions/wireguard.xml
+++ b/op-mode-definitions/wireguard.xml
@@ -8,18 +8,24 @@
<help>wireguard key generation utility</help>
</properties>
<children>
- <leafNode name="keypair">
+ <leafNode name="default-keypair">
<properties>
- <help>generate a wireguard keypair</help>
+ <help>generates the wireguard default-keypair</help>
</properties>
- <command>${vyos_op_scripts_dir}/wireguard.py --genkey</command>
+ <command>sudo ${vyos_op_scripts_dir}/wireguard.py --genkey</command>
</leafNode>
<leafNode name="preshared-key">
<properties>
<help>generate a wireguard preshared key</help>
</properties>
<command>${vyos_op_scripts_dir}/wireguard.py --genpsk</command>
- </leafNode>
+ </leafNode>
+ <tagNode name="named-keypairs">
+ <properties>
+ <help>Generates named wireguard keypairs</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/wireguard.py --genkey --location "$4"</command>
+ </tagNode>
</children>
</node>
</children>
@@ -31,18 +37,31 @@
<help>Show wireguard properties</help>
</properties>
<children>
- <leafNode name="pubkey">
- <properties>
- <help>show wireguard public key</help>
- </properties>
- <command>${vyos_op_scripts_dir}/wireguard.py --showpub</command>
- </leafNode>
- <leafNode name="privkey">
+ <node name="keypairs">
<properties>
- <help>show wireguard private key</help>
+ <help>Shows named wireguard keys</help>
</properties>
- <command>${vyos_op_scripts_dir}/wireguard.py --showpriv</command>
- </leafNode>
+ <children>
+ <tagNode name="pubkey">
+ <properties>
+ <help>Show wireguard private named key</help>
+ <completionHelp>
+ <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/wireguard.py --showpub --location "$5"</command>
+ </tagNode>
+ <tagNode name="privkey">
+ <properties>
+ <help>Show wireguard public named key</help>
+ <completionHelp>
+ <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/wireguard.py --showpriv --location "$5"</command>
+ </tagNode>
+ </children>
+ </node>
</children>
</node>
<node name="interfaces">
@@ -51,7 +70,7 @@
<properties>
<help>show wireguard interface information</help>
<completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py -t wireguard</script>
+ <script>${vyos_completion_dir}/list_interfaces.py --type wireguard</script>
</completionHelp>
</properties>
<command>sudo wg show "$4"</command>
@@ -74,12 +93,32 @@
</properties>
<command>sudo wg show "$4" peers</command>
</leafNode>
- <!-- more commands upon request -->
+ <!-- more commands upon request -->
</children>
</tagNode>
</children>
</node>
</children>
</node>
+ <node name="delete">
+ <children>
+ <node name="wireguard">
+ <properties>
+ <help>Delete wireguard properties</help>
+ </properties>
+ <children>
+ <tagNode name="keypair">
+ <properties>
+ <help>Delete a wireguard keypair</help>
+ <completionHelp>
+ <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/wireguard.py --delkdir --location "$4"</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
</interfaceDefinition>
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index 157011839..4bc8863bb 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -18,6 +18,7 @@ A library for retrieving value dicts from VyOS configs in a declarative fashion.
"""
+from vyos import ConfigError
def retrieve_config(path_hash, base_path, config):
"""
@@ -78,3 +79,118 @@ def retrieve_config(path_hash, base_path, config):
config_hash[k][node] = retrieve_config(inner_hash, path + [node], config)
return config_hash
+
+
+def list_diff(first, second):
+ """
+ Diff two dictionaries and return only unique items
+ """
+ second = set(second)
+ return [item for item in first if item not in second]
+
+
+def get_ethertype(ethertype_val):
+ if ethertype_val == '0x88A8':
+ return '802.1ad'
+ elif ethertype_val == '0x8100':
+ return '802.1q'
+ else:
+ raise ConfigError('invalid ethertype "{}"'.format(ethertype_val))
+
+
+def vlan_to_dict(conf):
+ """
+ Common used function which will extract VLAN related information from config
+ and represent the result as Python dictionary.
+
+ Function call's itself recursively if a vif-s/vif-c pair is detected.
+ """
+ vlan = {
+ 'id': conf.get_level().split()[-1], # get the '100' in 'interfaces bonding bond0 vif-s 100'
+ 'address': [],
+ 'address_remove': [],
+ 'description': '',
+ 'dhcp_client_id': '',
+ 'dhcp_hostname': '',
+ 'dhcpv6_prm_only': False,
+ 'dhcpv6_temporary': False,
+ 'disable': False,
+ 'disable_link_detect': 1,
+ 'mac': '',
+ 'mtu': 1500
+ }
+ # retrieve configured interface addresses
+ if conf.exists('address'):
+ vlan['address'] = conf.return_values('address')
+
+ # Determine interface addresses (currently effective) - to determine which
+ # address is no longer valid and needs to be removed from the bond
+ eff_addr = conf.return_effective_values('address')
+ act_addr = conf.return_values('address')
+ vlan['address_remove'] = list_diff(eff_addr, act_addr)
+
+ # retrieve interface description
+ if conf.exists('description'):
+ vlan['description'] = conf.return_value('description')
+
+ # get DHCP client identifier
+ if conf.exists('dhcp-options client-id'):
+ vlan['dhcp_client_id'] = conf.return_value('dhcp-options client-id')
+
+ # DHCP client host name (overrides the system host name)
+ if conf.exists('dhcp-options host-name'):
+ vlan['dhcp_hostname'] = conf.return_value('dhcp-options host-name')
+
+ # DHCPv6 only acquire config parameters, no address
+ if conf.exists('dhcpv6-options parameters-only'):
+ vlan['dhcpv6_prm_only'] = conf.return_value('dhcpv6-options parameters-only')
+
+ # DHCPv6 temporary IPv6 address
+ if conf.exists('dhcpv6-options temporary'):
+ vlan['dhcpv6_temporary'] = conf.return_value('dhcpv6-options temporary')
+
+ # ignore link state changes
+ if conf.exists('disable-link-detect'):
+ vlan['disable_link_detect'] = 2
+
+ # disable bond interface
+ if conf.exists('disable'):
+ vlan['disable'] = True
+
+ # Media Access Control (MAC) address
+ if conf.exists('mac'):
+ vlan['mac'] = conf.return_value('mac')
+
+ # Maximum Transmission Unit (MTU)
+ if conf.exists('mtu'):
+ vlan['mtu'] = int(conf.return_value('mtu'))
+
+ # ethertype is mandatory on vif-s nodes and only exists here!
+ # check if this is a vif-s node at all:
+ if conf.get_level().split()[-2] == 'vif-s':
+ vlan['vif_c'] = []
+ vlan['vif_c_remove'] = []
+
+ # ethertype uses a default of 0x88A8
+ tmp = '0x88A8'
+ if conf.exists('ethertype'):
+ tmp = conf.return_value('ethertype')
+ vlan['ethertype'] = get_ethertype(tmp)
+
+ # get vif-c interfaces (currently effective) - to determine which vif-c
+ # interface is no longer present and needs to be removed
+ eff_intf = conf.list_effective_nodes('vif-c')
+ act_intf = conf.list_nodes('vif-c')
+ vlan['vif_c_remove'] = list_diff(eff_intf, act_intf)
+
+ # check if there is a Q-in-Q vlan customer interface
+ # and call this function recursively
+ if conf.exists('vif-c'):
+ cfg_level = conf.get_level()
+ # add new key (vif-c) to dictionary
+ for vif in conf.list_nodes('vif-c'):
+ # set config level to vif interface
+ conf.set_level(cfg_level + ' vif-c ' + vif)
+ vlan['vif_c'].append(vlan_to_dict(conf))
+
+ return vlan
diff --git a/python/vyos/configinterface.py b/python/vyos/configinterface.py
deleted file mode 100644
index 0f5b0842c..000000000
--- a/python/vyos/configinterface.py
+++ /dev/null
@@ -1,153 +0,0 @@
-# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
-#
-# This library is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-#
-# This library is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with this library. If not, see <http://www.gnu.org/licenses/>.
-
-import os
-import vyos.validate
-
-def validate_mac_address(addr):
- # a mac address consits out of 6 octets
- octets = len(addr.split(':'))
- if octets != 6:
- raise ValueError('wrong number of MAC octets: {} '.format(octets))
-
- # validate against the first mac address byte if it's a multicast address
- if int(addr.split(':')[0]) & 1:
- raise ValueError('{} is a multicast MAC address'.format(addr))
-
- # overall mac address is not allowed to be 00:00:00:00:00:00
- if sum(int(i, 16) for i in addr.split(':')) == 0:
- raise ValueError('00:00:00:00:00:00 is not a valid MAC address')
-
- # check for VRRP mac address
- if addr.split(':')[0] == '0' and addr.split(':')[1] == '0' and addr.split(':')[2] == '94' and addr.split(':')[3] == '0' and addr.split(':')[4] == '1':
- raise ValueError('{} is a VRRP MAC address')
-
- pass
-
-def set_mac_address(intf, addr):
- """
- Configure interface mac address using iproute2 command
- """
- validate_mac_address(addr)
-
- os.system('ip link set {} address {}'.format(intf, addr))
- pass
-
-def set_description(intf, desc):
- """
- Sets the interface secription reported usually by SNMP
- """
- with open('/sys/class/net/' + intf + '/ifalias', 'w') as f:
- f.write(desc)
-
- pass
-
-def set_arp_cache_timeout(intf, tmoMS):
- """
- Configure the ARP cache entry timeout in milliseconds
- """
- with open('/proc/sys/net/ipv4/neigh/' + intf + '/base_reachable_time_ms', 'w') as f:
- f.write(tmoMS)
-
- pass
-
-def set_multicast_querier(intf, enable):
- """
- Sets whether the bridge actively runs a multicast querier or not. When a
- bridge receives a 'multicast host membership' query from another network host,
- that host is tracked based on the time that the query was received plus the
- multicast query interval time.
-
- use enable=1 to enable or enable=0 to disable
- """
-
- if int(enable) >= 0 and int(enable) <= 1:
- with open('/sys/devices/virtual/net/' + intf + '/bridge/multicast_querier', 'w') as f:
- f.write(str(enable))
- else:
- raise ValueError("malformed configuration string on interface {}: enable={}".format(intf, enable))
-
- pass
-
-def set_link_detect(intf, enable):
- """
- 0 - Allow packets to be received for the address on this interface
- even if interface is disabled or no carrier.
-
- 1 - Ignore packets received if interface associated with the incoming
- address is down.
-
- 2 - Ignore packets received if interface associated with the incoming
- address is down or has no carrier.
-
- Kernel Source: Documentation/networking/ip-sysctl.txt
- """
-
- # Note can't use sysctl it is broken for vif name because of dots
- # link_filter values:
- # 0 - always receive
- # 1 - ignore receive if admin_down
- # 2 - ignore receive if admin_down or link down
-
- with open('/proc/sys/net/ipv4/conf/' + intf + '/link_filter', 'w') as f:
- if enable == True or enable == 1:
- f.write('2')
- if os.path.isfile('/usr/bin/vtysh'):
- os.system('/usr/bin/vtysh -c "configure terminal" -c "interface {}" -c "link-detect"'.format(intf))
- else:
- f.write('1')
- if os.path.isfile('/usr/bin/vtysh'):
- os.system('/usr/bin/vtysh -c "configure terminal" -c "interface {}" -c "no link-detect"'.format(intf))
-
- pass
-
-def add_interface_address(intf, addr):
- """
- Configure an interface IPv4/IPv6 address
- """
- if addr == "dhcp":
- os.system('/opt/vyatta/sbin/vyatta-interfaces.pl --dev="{}" --dhcp=start'.format(intf))
- elif addr == "dhcpv6":
- os.system('/opt/vyatta/sbin/vyatta-dhcpv6-client.pl --start -ifname "{}"'.format(intf))
- elif vyos.validate.is_ipv4(addr):
- if not vyos.validate.is_intf_addr_assigned(intf, addr):
- print("Assigning {} to {}".format(addr, intf))
- os.system('sudo ip -4 addr add "{}" broadcast + dev "{}"'.format(addr, intf))
- elif vyos.validate.is_ipv6(addr):
- if not vyos.validate.is_intf_addr_assigned(intf, addr):
- print("Assigning {} to {}".format(addr, intf))
- os.system('sudo ip -6 addr add "{}" dev "{}"'.format(addr, intf))
- else:
- raise ConfigError('{} is not a valid interface address'.format(addr))
-
- pass
-
-def remove_interface_address(intf, addr):
- """
- Remove IPv4/IPv6 address from given interface
- """
-
- if addr == "dhcp":
- os.system('/opt/vyatta/sbin/vyatta-interfaces.pl --dev="{}" --dhcp=stop'.format(intf))
- elif addr == "dhcpv6":
- os.system('/opt/vyatta/sbin/vyatta-dhcpv6-client.pl --stop -ifname "{}"'.format(intf))
- elif vyos.validate.is_ipv4(addr):
- os.system('ip -4 addr del "{}" dev "{}"'.format(addr, intf))
- elif vyos.validate.is_ipv6(addr):
- os.system('ip -6 addr del "{}" dev "{}"'.format(addr, intf))
- else:
- raise ConfigError('{} is not a valid interface address'.format(addr))
-
- pass
diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py
index 8626839f2..acbdd3d5f 100644
--- a/python/vyos/configsession.py
+++ b/python/vyos/configsession.py
@@ -24,6 +24,7 @@ COMMENT = '/opt/vyatta/sbin/my_comment'
COMMIT = '/opt/vyatta/sbin/my_commit'
DISCARD = '/opt/vyatta/sbin/my_discard'
SHOW_CONFIG = ['/bin/cli-shell-api', 'showConfig']
+LOAD_CONFIG = ['/bin/cli-shell-api', 'loadFile']
# Default "commit via" string
APP = "vyos-http-api"
@@ -155,3 +156,5 @@ class ConfigSession(object):
if format == 'raw':
return config_data
+ def load_config(self, file_path):
+ self.__run_command(LOAD_CONFIG + [file_path])
diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
index a812b62ec..8832a5a63 100644
--- a/python/vyos/configtree.py
+++ b/python/vyos/configtree.py
@@ -185,6 +185,14 @@ class ConfigTree(object):
return self.__to_commands(self.__config).decode()
def set(self, path, value=None, replace=True):
+ """Set new entry in VyOS configuration.
+ path: configuration path e.g. 'system dns forwarding listen-address'
+ value: value to be added to node, e.g. '172.18.254.201'
+ replace: True: current occurance will be replaced
+ False: new value will be appended to current occurances - use
+ this for adding values to a multi node
+ """
+
check_path(path)
path_str = " ".join(map(str, path)).encode()
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
index 3e4c02562..85d27d60d 100644
--- a/python/vyos/defaults.py
+++ b/python/vyos/defaults.py
@@ -29,7 +29,7 @@ cfg_vintage = 'vyatta'
commit_lock = '/opt/vyatta/config/.lock'
https_data = {
- 'listen_address' : [ '127.0.0.1' ]
+ 'listen_addresses' : { '*': ['_'] }
}
api_data = {
diff --git a/python/vyos/hostsd_client.py b/python/vyos/hostsd_client.py
new file mode 100644
index 000000000..f009aba98
--- /dev/null
+++ b/python/vyos/hostsd_client.py
@@ -0,0 +1,69 @@
+import json
+
+import zmq
+
+
+SOCKET_PATH = "ipc:///run/vyos-hostsd.sock"
+
+
+class VyOSHostsdError(Exception):
+ pass
+
+
+class Client(object):
+ def __init__(self):
+ try:
+ context = zmq.Context()
+ self.__socket = context.socket(zmq.REQ)
+ self.__socket.RCVTIMEO = 10000 #ms
+ self.__socket.setsockopt(zmq.LINGER, 0)
+ self.__socket.connect(SOCKET_PATH)
+ except zmq.error.Again:
+ raise VyOSHostsdError("Could not connect to vyos-hostsd")
+
+ def _communicate(self, msg):
+ try:
+ request = json.dumps(msg).encode()
+ self.__socket.send(request)
+
+ reply_msg = self.__socket.recv().decode()
+ reply = json.loads(reply_msg)
+ if 'error' in reply:
+ raise VyOSHostsdError(reply['error'])
+ else:
+ return reply["data"]
+ except zmq.error.Again:
+ raise VyOSHostsdError("Could not connect to vyos-hostsd")
+
+ def set_host_name(self, host_name, domain_name, search_domains):
+ msg = {
+ 'type': 'host_name',
+ 'op': 'set',
+ 'data': {
+ 'host_name': host_name,
+ 'domain_name': domain_name,
+ 'search_domains': search_domains
+ }
+ }
+ self._communicate(msg)
+
+ def add_hosts(self, tag, hosts):
+ msg = {'type': 'hosts', 'op': 'add', 'tag': tag, 'data': hosts}
+ self._communicate(msg)
+
+ def delete_hosts(self, tag):
+ msg = {'type': 'hosts', 'op': 'delete', 'tag': tag}
+ self._communicate(msg)
+
+ def add_name_servers(self, tag, servers):
+ msg = {'type': 'name_servers', 'op': 'add', 'tag': tag, 'data': servers}
+ self._communicate(msg)
+
+ def delete_name_servers(self, tag):
+ msg = {'type': 'name_servers', 'op': 'delete', 'tag': tag}
+ self._communicate(msg)
+
+ def get_name_servers(self, tag):
+ msg = {'type': 'name_servers', 'op': 'get', 'tag': tag}
+ return self._communicate(msg)
+
diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py
new file mode 100644
index 000000000..62bf94d79
--- /dev/null
+++ b/python/vyos/ifconfig.py
@@ -0,0 +1,1449 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+import subprocess
+import jinja2
+
+from vyos.validate import *
+from ipaddress import IPv4Network, IPv6Address
+from netifaces import ifaddresses, AF_INET, AF_INET6
+from time import sleep
+
+dhcp_cfg = """
+# generated by ifconfig.py
+option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;
+interface "{{ intf }}" {
+ send host-name "{{ hostname }}";
+ request subnet-mask, broadcast-address, routers, domain-name-servers, rfc3442-classless-static-routes, domain-name, interface-mtu;
+}
+"""
+
+dhcpv6_cfg = """
+# generated by ifconfig.py
+interface "{{ intf }}" {
+ request routers, domain-name-servers, domain-name;
+}
+"""
+
+dhclient_base = r'/var/lib/dhcp/dhclient_'
+
+
+class Interface:
+
+ def __init__(self, ifname, type=None):
+ """
+ This is the base interface class which supports basic IP/MAC address
+ operations as well as DHCP(v6). Other interface which represent e.g.
+ and ethernet bridge are implemented as derived classes adding all
+ additional functionality.
+
+ DEBUG:
+ This class has embedded debugging (print) which can be enabled by
+ creating the following file:
+ vyos@vyos# touch /tmp/vyos.ifconfig.debug
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> i = Interface('eth0')
+ """
+ self._ifname = str(ifname)
+ self._state = 'down'
+
+ if not os.path.exists('/sys/class/net/{}'.format(ifname)) and not type:
+ raise Exception('interface "{}" not found'.format(self._ifname))
+
+ if not os.path.exists('/sys/class/net/{}'.format(self._ifname)):
+ cmd = 'ip link add dev {} type {}'.format(self._ifname, type)
+ self._cmd(cmd)
+
+ # per interface DHCP config files
+ self._dhcp_cfg_file = dhclient_base + self._ifname + '.conf'
+ self._dhcp_pid_file = dhclient_base + self._ifname + '.pid'
+ self._dhcp_lease_file = dhclient_base + self._ifname + '.leases'
+
+ # per interface DHCPv6 config files
+ self._dhcpv6_cfg_file = dhclient_base + self._ifname + '.v6conf'
+ self._dhcpv6_pid_file = dhclient_base + self._ifname + '.v6pid'
+ self._dhcpv6_lease_file = dhclient_base + self._ifname + '.v6leases'
+
+ def _debug_msg(self, msg):
+ if os.path.isfile('/tmp/vyos.ifconfig.debug'):
+ print('DEBUG/{:<6} {}'.format(self._ifname, msg))
+
+ def remove(self):
+ """
+ Remove interface from operating system. Removing the interface
+ deconfigures all assigned IP addresses and clear possible DHCP(v6)
+ client processes.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> i = Interface('eth0')
+ >>> i.remove()
+ """
+
+ # do we have sub interfaces (VLANs)?
+ # we apply a regex matching subinterfaces (indicated by a .) of a
+ # parent interface. 'bond0(?:\.\d+){1,2}' will match vif and vif-s/vif-c
+ # subinterfaces
+ vlan_ifs = [f for f in os.listdir(r'/sys/class/net') \
+ if re.match(self._ifname + r'(?:\.\d+){1,2}', f)]
+
+ for vlan in vlan_ifs:
+ Interface(vlan).remove()
+
+ # All subinterfaces are now removed, continue on the physical interface
+
+ # stop DHCP(v6) if running
+ self._del_dhcp()
+ self._del_dhcpv6()
+
+ # NOTE (Improvement):
+ # after interface removal no other commands should be allowed
+ # to be called and instead should raise an Exception:
+ cmd = 'ip link del dev {}'.format(self._ifname)
+ self._cmd(cmd)
+
+ def _cmd(self, command):
+ self._debug_msg("cmd '{}'".format(command))
+
+ process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)
+ proc_stdout = process.communicate()[0].strip()
+
+ # add exception handling code
+ pass
+
+ def _read_sysfs(self, filename):
+ """
+ Provide a single primitive w/ error checking for reading from sysfs.
+ """
+ value = None
+ with open(filename, 'r') as f:
+ value = f.read().rstrip('\n')
+
+ self._debug_msg("read '{}' < '{}'".format(value, filename))
+ return value
+
+ def _write_sysfs(self, filename, value):
+ """
+ Provide a single primitive w/ error checking for writing to sysfs.
+ """
+ self._debug_msg("write '{}' > '{}'".format(value, filename))
+ with open(filename, 'w') as f:
+ f.write(str(value))
+
+ return None
+
+ @property
+ def mtu(self):
+ """
+ Get/set interface mtu in bytes.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').mtu
+ '1500'
+ """
+ return self._read_sysfs('/sys/class/net/{0}/mtu'
+ .format(self._ifname))
+
+ @mtu.setter
+ def mtu(self, mtu):
+ """
+ Get/set interface mtu in bytes.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').mtu = 1400
+ >>> Interface('eth0').mtu
+ '1400'
+ """
+ if mtu < 68 or mtu > 9000:
+ raise ValueError('Invalid MTU size: "{}"'.format(mru))
+
+ return self._write_sysfs('/sys/class/net/{0}/mtu'
+ .format(self._ifname), mtu)
+
+ @property
+ def mac(self):
+ """
+ Get/set interface mac address
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').mac
+ '00:0c:29:11:aa:cc'
+ """
+ return self._read_sysfs('/sys/class/net/{0}/address'
+ .format(self._ifname))
+
+ @mac.setter
+ def mac(self, mac):
+ """
+ Get/set interface mac address
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').mac = '00:90:43:fe:fe:1b'
+ >>> Interface('eth0').mac
+ '00:90:43:fe:fe:1b'
+ """
+ # a mac address consits out of 6 octets
+ octets = len(mac.split(':'))
+ if octets != 6:
+ raise ValueError('wrong number of MAC octets: {} '.format(octets))
+
+ # validate against the first mac address byte if it's a multicast
+ # address
+ if int(mac.split(':')[0]) & 1:
+ raise ValueError('{} is a multicast MAC address'.format(mac))
+
+ # overall mac address is not allowed to be 00:00:00:00:00:00
+ if sum(int(i, 16) for i in mac.split(':')) == 0:
+ raise ValueError('00:00:00:00:00:00 is not a valid MAC address')
+
+ # check for VRRP mac address
+ if mac.split(':')[0] == '0' and addr.split(':')[1] == '0' and mac.split(':')[2] == '94' and mac.split(':')[3] == '0' and mac.split(':')[4] == '1':
+ raise ValueError('{} is a VRRP MAC address'.format(mac))
+
+ # Assemble command executed on system. Unfortunately there is no way
+ # of altering the MAC address via sysfs
+ cmd = 'ip link set dev {} address {}'.format(self._ifname, mac)
+ self._cmd(cmd)
+
+ @property
+ def arp_cache_tmo(self):
+ """
+ Get configured ARP cache timeout value from interface in seconds.
+ Internal Kernel representation is in milliseconds.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').arp_cache_tmo
+ '30'
+ """
+ return (self._read_sysfs('/proc/sys/net/ipv4/neigh/{0}/base_reachable_time_ms'
+ .format(self._ifname)) / 1000)
+
+ @arp_cache_tmo.setter
+ def arp_cache_tmo(self, tmo):
+ """
+ Set ARP cache timeout value in seconds. Internal Kernel representation
+ is in milliseconds.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').arp_cache_tmo = '40'
+ """
+ return self._write_sysfs('/proc/sys/net/ipv4/neigh/{0}/base_reachable_time_ms'
+ .format(self._ifname), (int(tmo) * 1000))
+
+ @property
+ def link_detect(self):
+ """
+ How does the kernel act when receiving packets on 'down' interfaces
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').link_detect
+ '0'
+ """
+ return self._read_sysfs('/proc/sys/net/ipv4/conf/{0}/link_filter'
+ .format(self._ifname))
+
+ @link_detect.setter
+ def link_detect(self, link_filter):
+ """
+ Konfigure kernel response in packets received on interfaces that are 'down'
+
+ 0 - Allow packets to be received for the address on this interface
+ even if interface is disabled or no carrier.
+
+ 1 - Ignore packets received if interface associated with the incoming
+ address is down.
+
+ 2 - Ignore packets received if interface associated with the incoming
+ address is down or has no carrier.
+
+ Default value is 0. Note that some distributions enable it in startup
+ scripts.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').link_detect = '1'
+ """
+ if link_filter >= 0 and link_filter <= 2:
+ return self._write_sysfs('/proc/sys/net/ipv4/conf/{0}/link_filter'
+ .format(self._ifname), link_filter)
+ else:
+ raise ValueError("Value out of range")
+
+ @property
+ def ifalias(self):
+ """
+ Get/set interface alias name
+
+ Example:
+
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').ifalias
+ ''
+ """
+ return self._read_sysfs('/sys/class/net/{0}/ifalias'
+ .format(self._ifname))
+
+ @ifalias.setter
+ def ifalias(self, ifalias=None):
+ """
+ Get/set interface alias name
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').ifalias = 'VyOS upstream interface'
+ >>> Interface('eth0').ifalias
+ 'VyOS upstream interface'
+
+ to clear interface alias e.g. delete it use:
+
+ >>> Interface('eth0').ifalias = ''
+ >>> Interface('eth0').ifalias
+ ''
+ """
+ if not ifalias:
+ # clear interface alias
+ ifalias = '\0'
+
+ self._write_sysfs('/sys/class/net/{0}/ifalias'
+ .format(self._ifname), ifalias)
+
+ @property
+ def state(self):
+ """
+ Enable (up) / Disable (down) an interface
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').state
+ 'up'
+ """
+ return self._read_sysfs('/sys/class/net/{0}/operstate'
+ .format(self._ifname))
+
+ @state.setter
+ def state(self, state):
+ """
+ Enable (up) / Disable (down) an interface
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').state = 'down'
+ >>> Interface('eth0').state
+ 'down'
+ """
+ if state not in ['up', 'down']:
+ raise ValueError('state must be "up" or "down"')
+
+ self._state = state
+
+ # Assemble command executed on system. Unfortunately there is no way
+ # to up/down an interface via sysfs
+ cmd = 'ip link set dev {} {}'.format(self._ifname, state)
+ self._cmd(cmd)
+
+ @property
+ def proxy_arp(self):
+ """
+ Get current proxy ARP configuration from sysfs. Default: 0
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').proxy_arp
+ '0'
+ """
+ return self._read_sysfs('/proc/sys/net/ipv4/conf/{}/proxy_arp'
+ .format(self._ifname))
+
+ @proxy_arp.setter
+ def proxy_arp(self, enable):
+ """
+ Set per interface proxy ARP configuration
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').proxy_arp = 1
+ >>> Interface('eth0').proxy_arp
+ '1'
+ """
+ if int(enable) >= 0 and int(enable) <= 1:
+ return self._write_sysfs('/proc/sys/net/ipv4/conf/{}/proxy_arp'
+ .format(self._ifname), enable)
+ else:
+ raise ValueError("Value out of range")
+
+ @property
+ def proxy_arp_pvlan(self):
+ """
+ Private VLAN proxy arp.
+ Basically allow proxy arp replies back to the same interface
+ (from which the ARP request/solicitation was received).
+
+ This is done to support (ethernet) switch features, like RFC
+ 3069, where the individual ports are NOT allowed to
+ communicate with each other, but they are allowed to talk to
+ the upstream router. As described in RFC 3069, it is possible
+ to allow these hosts to communicate through the upstream
+ router by proxy_arp'ing. Don't need to be used together with
+ proxy_arp.
+
+ This technology is known by different names:
+ In RFC 3069 it is called VLAN Aggregation.
+ Cisco and Allied Telesyn call it Private VLAN.
+ Hewlett-Packard call it Source-Port filtering or port-isolation.
+ Ericsson call it MAC-Forced Forwarding (RFC Draft).
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').proxy_arp_pvlan
+ '0'
+ """
+ return self._read_sysfs('/proc/sys/net/ipv4/conf/{}/proxy_arp_pvlan'
+ .format(self._ifname))
+
+ @proxy_arp_pvlan.setter
+ def proxy_arp_pvlan(self, enable):
+ """
+ Private VLAN proxy arp.
+ Basically allow proxy arp replies back to the same interface
+ (from which the ARP request/solicitation was received).
+
+ This is done to support (ethernet) switch features, like RFC
+ 3069, where the individual ports are NOT allowed to
+ communicate with each other, but they are allowed to talk to
+ the upstream router. As described in RFC 3069, it is possible
+ to allow these hosts to communicate through the upstream
+ router by proxy_arp'ing. Don't need to be used together with
+ proxy_arp.
+
+ This technology is known by different names:
+ In RFC 3069 it is called VLAN Aggregation.
+ Cisco and Allied Telesyn call it Private VLAN.
+ Hewlett-Packard call it Source-Port filtering or port-isolation.
+ Ericsson call it MAC-Forced Forwarding (RFC Draft).
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').proxy_arp_pvlan = 1
+ >>> Interface('eth0').proxy_arp_pvlan
+ '1'
+ """
+ if int(enable) >= 0 and int(enable) <= 1:
+ return self._write_sysfs('/proc/sys/net/ipv4/conf/{}/proxy_arp_pvlan'
+ .format(self._ifname), enable)
+ else:
+ raise ValueError("Value out of range")
+
+ def get_addr(self):
+ """
+ Retrieve assigned IPv4 and IPv6 addresses from given interface.
+ This is done using the netifaces and ipaddress python modules.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').get_addrs()
+ ['172.16.33.30/24', 'fe80::20c:29ff:fe11:a174/64']
+ """
+
+ ipv4 = []
+ ipv6 = []
+
+ if AF_INET in ifaddresses(self._ifname).keys():
+ for v4_addr in ifaddresses(self._ifname)[AF_INET]:
+ # we need to manually assemble a list of IPv4 address/prefix
+ prefix = '/' + \
+ str(IPv4Network('0.0.0.0/' + v4_addr['netmask']).prefixlen)
+ ipv4.append(v4_addr['addr'] + prefix)
+
+ if AF_INET6 in ifaddresses(self._ifname).keys():
+ for v6_addr in ifaddresses(self._ifname)[AF_INET6]:
+ # Note that currently expanded netmasks are not supported. That means
+ # 2001:db00::0/24 is a valid argument while 2001:db00::0/ffff:ff00:: not.
+ # see https://docs.python.org/3/library/ipaddress.html
+ bits = bin(
+ int(v6_addr['netmask'].replace(':', ''), 16)).count('1')
+ prefix = '/' + str(bits)
+
+ # we alsoneed to remove the interface suffix on link local
+ # addresses
+ v6_addr['addr'] = v6_addr['addr'].split('%')[0]
+ ipv6.append(v6_addr['addr'] + prefix)
+
+ return ipv4 + ipv6
+
+ def add_addr(self, addr):
+ """
+ Add IP(v6) address to interface. Address is only added if it is not
+ already assigned to that interface.
+
+ addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6!
+ IPv4: add IPv4 address to interface
+ IPv6: add IPv6 address to interface
+ dhcp: start dhclient (IPv4) on interface
+ dhcpv6: start dhclient (IPv6) on interface
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> j = Interface('eth0')
+ >>> j.add_addr('192.0.2.1/24')
+ >>> j.add_addr('2001:db8::ffff/64')
+ >>> j.get_addr()
+ ['192.0.2.1/24', '2001:db8::ffff/64']
+ """
+ if addr == 'dhcp':
+ self._set_dhcp()
+ elif addr == 'dhcpv6':
+ self._set_dhcpv6()
+ else:
+ if not is_intf_addr_assigned(self._ifname, addr):
+ cmd = 'ip addr add "{}" dev "{}"'.format(addr, self._ifname)
+ self._cmd(cmd)
+
+ def del_addr(self, addr):
+ """
+ Delete IP(v6) address to interface. Address is only added if it is
+ assigned to that interface.
+
+ addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6!
+ IPv4: delete IPv4 address from interface
+ IPv6: delete IPv6 address from interface
+ dhcp: stop dhclient (IPv4) on interface
+ dhcpv6: stop dhclient (IPv6) on interface
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> j = Interface('eth0')
+ >>> j.add_addr('2001:db8::ffff/64')
+ >>> j.add_addr('192.0.2.1/24')
+ >>> j.get_addr()
+ ['192.0.2.1/24', '2001:db8::ffff/64']
+ >>> j.del_addr('192.0.2.1/24')
+ >>> j.get_addr()
+ ['2001:db8::ffff/64']
+ """
+ if addr == 'dhcp':
+ self._del_dhcp()
+ elif addr == 'dhcpv6':
+ self._del_dhcpv6()
+ else:
+ if is_intf_addr_assigned(self._ifname, addr):
+ cmd = 'ip addr del "{}" dev "{}"'.format(addr, self._ifname)
+ self._cmd(cmd)
+
+ # replace dhcpv4/v6 with systemd.networkd?
+ def _set_dhcp(self):
+ """
+ Configure interface as DHCP client. The dhclient binary is automatically
+ started in background!
+
+ Example:
+
+ >>> from vyos.ifconfig import Interface
+ >>> j = Interface('eth0')
+ >>> j.set_dhcp()
+ """
+ dhcp = {
+ 'hostname': 'vyos',
+ 'intf': self._ifname
+ }
+
+ # read configured system hostname.
+ # maybe change to vyos hostd client ???
+ with open('/etc/hostname', 'r') as f:
+ dhcp['hostname'] = f.read().rstrip('\n')
+
+ # render DHCP configuration
+ tmpl = jinja2.Template(dhcp_cfg)
+ dhcp_text = tmpl.render(dhcp)
+ with open(self._dhcp_cfg_file, 'w') as f:
+ f.write(dhcp_text)
+
+ if self._state == 'up':
+ cmd = 'start-stop-daemon --start --quiet --pidfile ' + \
+ self._dhcp_pid_file
+ cmd += ' --exec /sbin/dhclient --'
+ # now pass arguments to dhclient binary
+ cmd += ' -4 -nw -cf {} -pf {} -lf {} {}'.format(
+ self._dhcp_cfg_file, self._dhcp_pid_file, self._dhcp_lease_file, self._ifname)
+ self._cmd(cmd)
+
+
+ def _del_dhcp(self):
+ """
+ De-configure interface as DHCP clinet. All auto generated files like
+ pid, config and lease will be removed.
+
+ Example:
+
+ >>> from vyos.ifconfig import Interface
+ >>> j = Interface('eth0')
+ >>> j.del_dhcp()
+ """
+ pid = 0
+ if os.path.isfile(self._dhcp_pid_file):
+ with open(self._dhcp_pid_file, 'r') as f:
+ pid = int(f.read())
+ else:
+ self._debug_msg('No DHCP client PID found')
+ return None
+
+ # stop dhclient
+ cmd = 'start-stop-daemon --stop --quiet --pidfile {}'.format(
+ self._dhcp_pid_file)
+ self._cmd(cmd)
+
+ # cleanup old config file
+ if os.path.isfile(self._dhcp_cfg_file):
+ os.remove(self._dhcp_cfg_file)
+
+ # cleanup old pid file
+ if os.path.isfile(self._dhcp_pid_file):
+ os.remove(self._dhcp_pid_file)
+
+ # cleanup old lease file
+ if os.path.isfile(self._dhcp_lease_file):
+ os.remove(self._dhcp_lease_file)
+
+
+ def _set_dhcpv6(self):
+ """
+ Configure interface as DHCPv6 client. The dhclient binary is automatically
+ started in background!
+
+ Example:
+
+ >>> from vyos.ifconfig import Interface
+ >>> j = Interface('eth0')
+ >>> j.set_dhcpv6()
+ """
+ dhcpv6 = {
+ 'intf': self._ifname
+ }
+
+ # render DHCP configuration
+ tmpl = jinja2.Template(dhcpv6_cfg)
+ dhcpv6_text = tmpl.render(dhcpv6)
+ with open(self._dhcpv6_cfg_file, 'w') as f:
+ f.write(dhcpv6_text)
+
+ if self._state == 'up':
+ # https://bugs.launchpad.net/ubuntu/+source/ifupdown/+bug/1447715
+ #
+ # wee need to wait for IPv6 DAD to finish once and interface is added
+ # this suxx :-(
+ sleep(5)
+
+ # no longer accept router announcements on this interface
+ cmd = 'sysctl -q -w net.ipv6.conf.{}.accept_ra=0'.format(self._ifname)
+ self._cmd(cmd)
+
+ # assemble command-line to start DHCPv6 client (dhclient)
+ cmd = 'start-stop-daemon --start --quiet --pidfile ' + \
+ self._dhcpv6_pid_file
+ cmd += ' --exec /sbin/dhclient --'
+ # now pass arguments to dhclient binary
+ cmd += ' -6 -nw -cf {} -pf {} -lf {} {}'.format(
+ self._dhcpv6_cfg_file, self._dhcpv6_pid_file, self._dhcpv6_lease_file, self._ifname)
+ self._cmd(cmd)
+
+
+ def _del_dhcpv6(self):
+ """
+ De-configure interface as DHCPv6 clinet. All auto generated files like
+ pid, config and lease will be removed.
+
+ Example:
+
+ >>> from vyos.ifconfig import Interface
+ >>> j = Interface('eth0')
+ >>> j.del_dhcpv6()
+ """
+ pid = 0
+ if os.path.isfile(self._dhcpv6_pid_file):
+ with open(self._dhcpv6_pid_file, 'r') as f:
+ pid = int(f.read())
+ else:
+ self._debug_msg('No DHCPv6 client PID found')
+ return None
+
+ # stop dhclient
+ cmd = 'start-stop-daemon --stop --quiet --pidfile {}'.format(
+ self._dhcpv6_pid_file)
+ self._cmd(cmd)
+
+ # accept router announcements on this interface
+ cmd = 'sysctl -q -w net.ipv6.conf.{}.accept_ra=1'.format(self._ifname)
+ self._cmd(cmd)
+
+ # cleanup old config file
+ if os.path.isfile(self._dhcpv6_cfg_file):
+ os.remove(self._dhcpv6_cfg_file)
+
+ # cleanup old pid file
+ if os.path.isfile(self._dhcpv6_pid_file):
+ os.remove(self._dhcpv6_pid_file)
+
+ # cleanup old lease file
+ if os.path.isfile(self._dhcpv6_lease_file):
+ os.remove(self._dhcpv6_lease_file)
+
+
+class LoopbackIf(Interface):
+
+ """
+ The loopback device is a special, virtual network interface that your router
+ uses to communicate with itself.
+ """
+
+ def __init__(self, ifname):
+ super().__init__(ifname, type='loopback')
+
+
+class DummyIf(Interface):
+
+ """
+ A dummy interface is entirely virtual like, for example, the loopback
+ interface. The purpose of a dummy interface is to provide a device to route
+ packets through without actually transmitting them.
+ """
+
+ def __init__(self, ifname):
+ super().__init__(ifname, type='dummy')
+
+
+class BridgeIf(Interface):
+
+ """
+ A bridge is a way to connect two Ethernet segments together in a protocol
+ independent way. Packets are forwarded based on Ethernet address, rather
+ than IP address (like a router). Since forwarding is done at Layer 2, all
+ protocols can go transparently through a bridge.
+
+ The Linux bridge code implements a subset of the ANSI/IEEE 802.1d standard.
+ """
+
+ def __init__(self, ifname):
+ super().__init__(ifname, type='bridge')
+
+ @property
+ def ageing_time(self):
+ """
+ Return configured bridge interface MAC address aging time in seconds.
+ Internal kernel representation is in centiseconds, thus its converted
+ in the end. Kernel default is 300 seconds.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').aging_time
+ '300'
+ """
+ return (self._read_sysfs('/sys/class/net/{0}/bridge/ageing_time'
+ .format(self._ifname)) / 100)
+
+ @ageing_time.setter
+ def ageing_time(self, time):
+ """
+ Set bridge interface MAC address aging time in seconds. Internal kernel
+ representation is in centiseconds. Kernel default is 300 seconds.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').ageing_time = 2
+ """
+ time = int(time) * 100
+ return self._write_sysfs('/sys/class/net/{0}/bridge/ageing_time'
+ .format(self._ifname), time)
+
+ @property
+ def forward_delay(self):
+ """
+ Get bridge forwarding delay in seconds. Internal Kernel representation
+ is in centiseconds.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').ageing_time
+ '3'
+ """
+ return (self._read_sysfs('/sys/class/net/{0}/bridge/forward_delay'
+ .format(self._ifname)) / 100)
+
+ @forward_delay.setter
+ def forward_delay(self, time):
+ """
+ Set bridge forwarding delay in seconds. Internal Kernel representation
+ is in centiseconds.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').forward_delay = 15
+ """
+ return self._write_sysfs('/sys/class/net/{0}/bridge/forward_delay'
+ .format(self._ifname), (int(time) * 100))
+
+ @property
+ def hello_time(self):
+ """
+ Get bridge hello time in seconds. Internal Kernel representation
+ is in centiseconds.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').hello_time
+ '2'
+ """
+ return (self._read_sysfs('/sys/class/net/{0}/bridge/hello_time'
+ .format(self._ifname)) / 100)
+
+ @hello_time.setter
+ def hello_time(self, time):
+ """
+ Set bridge hello time in seconds. Internal Kernel representation
+ is in centiseconds.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').hello_time = 2
+ """
+ return self._write_sysfs('/sys/class/net/{0}/bridge/hello_time'
+ .format(self._ifname), (int(time) * 100))
+
+ @property
+ def max_age(self):
+ """
+ Get bridge max max message age in seconds. Internal Kernel representation
+ is in centiseconds.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').max_age
+ '20'
+ """
+
+ return (self._read_sysfs('/sys/class/net/{0}/bridge/max_age'
+ .format(self._ifname)) / 100)
+
+ @max_age.setter
+ def max_age(self, time):
+ """
+ Set bridge max message age in seconds. Internal Kernel representation
+ is in centiseconds.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').max_age = 30
+ """
+ return self._write_sysfs('/sys/class/net/{0}/bridge/max_age'
+ .format(self._ifname), (int(time) * 100))
+
+ @property
+ def priority(self):
+ """
+ Get bridge max aging time in seconds.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').priority
+ '32768'
+ """
+ return self._read_sysfs('/sys/class/net/{0}/bridge/priority'
+ .format(self._ifname))
+
+ @priority.setter
+ def priority(self, priority):
+ """
+ Set bridge max aging time in seconds.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').priority = 8192
+ """
+ return self._write_sysfs('/sys/class/net/{0}/bridge/priority'
+ .format(self._ifname), priority)
+
+ @property
+ def stp_state(self):
+ """
+ Get current bridge STP (Spanning Tree) state.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').stp_state
+ '0'
+ """
+
+ state = 0
+ with open('/sys/class/net/{0}/bridge/stp_state'.format(self._ifname), 'r') as f:
+ state = int(f.read().rstrip('\n'))
+
+ return state
+
+ @stp_state.setter
+ def stp_state(self, state):
+ """
+ Set bridge STP (Spannign Tree) state. 0 -> STP disabled, 1 -> STP enabled
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').stp_state = 1
+ """
+
+ if int(state) >= 0 and int(state) <= 1:
+ return self._write_sysfs('/sys/class/net/{0}/bridge/stp_state'
+ .format(self._ifname), state)
+ else:
+ raise ValueError("Value out of range")
+
+ @property
+ def multicast_querier(self):
+ """
+ Get bridge multicast querier membership state.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').multicast_querier
+ '0'
+ """
+ return self._read_sysfs('/sys/class/net/{0}/bridge/multicast_querier'
+ .format(self._ifname))
+
+ @multicast_querier.setter
+ def multicast_querier(self, enable):
+ """
+ Sets whether the bridge actively runs a multicast querier or not. When a
+ bridge receives a 'multicast host membership' query from another network
+ host, that host is tracked based on the time that the query was received
+ plus the multicast query interval time.
+
+ Use enable=1 to enable or enable=0 to disable
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').multicast_querier = 1
+ """
+ if int(enable) >= 0 and int(enable) <= 1:
+ return self._write_sysfs('/sys/class/net/{0}/bridge/multicast_querier'
+ .format(self._ifname), enable)
+ else:
+ raise ValueError("Value out of range")
+
+ def add_port(self, interface):
+ """
+ Add physical interface to bridge (member port)
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').add_port('eth0')
+ >>> BridgeIf('br0').add_port('eth1')
+ """
+ cmd = 'ip link set dev {} master {}'.format(interface, self._ifname)
+ self._cmd(cmd)
+
+ def del_port(self, interface):
+ """
+ Remove member port from bridge instance.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').del_port('eth1')
+ """
+ cmd = 'ip link set dev {} nomaster'.format(interface)
+ self._cmd(cmd)
+
+ def set_cost(self, interface, cost):
+ """
+ Set interface path cost, only relevant for STP enabled interfaces
+
+ Example:
+
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').path_cost(4)
+ """
+ return self._write_sysfs('/sys/class/net/{}/brif/{}/path_cost'
+ .format(self._ifname, interface), cost)
+
+ def set_priority(self, interface, priority):
+ """
+ Set interface path priority, only relevant for STP enabled interfaces
+
+ Example:
+
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').priority(4)
+ """
+ return self._write_sysfs('/sys/class/net/{}/brif/{}/priority'
+ .format(self._ifname, interface), priority)
+
+
+class EthernetIf(Interface):
+
+ def __init__(self, ifname, type=None):
+ super().__init__(ifname, type)
+
+ def add_vlan(self, vlan_id, ethertype=''):
+ """
+ A virtual LAN (VLAN) is any broadcast domain that is partitioned and
+ isolated in a computer network at the data link layer (OSI layer 2).
+ Use this function to create a new VLAN interface on a given physical
+ interface.
+
+ This function creates both 802.1q and 802.1ad (Q-in-Q) interfaces. Proto
+ parameter is used to indicate VLAN type.
+
+ A new object of type EthernetIf is returned once the interface has been
+ created.
+ """
+ vlan_ifname = self._ifname + '.' + str(vlan_id)
+ if not os.path.exists('/sys/class/net/{}'.format(vlan_ifname)):
+ self._vlan_id = int(vlan_id)
+
+ if ethertype:
+ self._ethertype = ethertype
+ ethertype = 'proto {}'.format(ethertype)
+
+ # create interface in the system
+ cmd = 'ip link add link {intf} name {intf}.{vlan} type vlan {proto} id {vlan}'.format(
+ intf=self._ifname, vlan=self._vlan_id, proto=ethertype)
+ self._cmd(cmd)
+
+ # return new object mapping to the newly created interface
+ # we can now work on this object for e.g. IP address setting
+ # or interface description and so on
+ return EthernetIf(vlan_ifname)
+
+ def del_vlan(self, vlan_id):
+ """
+ Remove VLAN interface from operating system. Removing the interface
+ deconfigures all assigned IP addresses and clear possible DHCP(v6)
+ client processes.
+ """
+ vlan_ifname = self._ifname + '.' + str(vlan_id)
+ tmp = EthernetIf(vlan_ifname)
+ tmp.remove()
+
+
+class BondIf(EthernetIf):
+
+ """
+ The Linux bonding driver provides a method for aggregating multiple network
+ interfaces into a single logical "bonded" interface. The behavior of the
+ bonded interfaces depends upon the mode; generally speaking, modes provide
+ either hot standby or load balancing services. Additionally, link integrity
+ monitoring may be performed.
+ """
+
+ def __init__(self, ifname):
+ super().__init__(ifname, type='bond')
+
+ @property
+ def xmit_hash_policy(self):
+ """
+ Selects the transmit hash policy to use for slave selection in
+ balance-xor, 802.3ad, and tlb modes. Possible values are: layer2,
+ layer2+3, layer3+4, encap2+3, encap3+4.
+
+ The default value is layer2
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').xmit_hash_policy
+ 'layer3+4'
+ """
+ # Linux Kernel appends has policy value to string, e.g. 'layer3+4 1',
+ # so remove the later part and only return the mode as string.
+ return self._read_sysfs('/sys/class/net/{}/bonding/xmit_hash_policy'
+ .format(self._ifname)).split()[0]
+
+ @xmit_hash_policy.setter
+ def xmit_hash_policy(self, mode):
+ """
+ Selects the transmit hash policy to use for slave selection in
+ balance-xor, 802.3ad, and tlb modes. Possible values are: layer2,
+ layer2+3, layer3+4, encap2+3, encap3+4.
+
+ The default value is layer2
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BondIf('bond0').xmit_hash_policy = 'layer2+3'
+ >>> BondIf('bond0').proxy_arp
+ '1'
+ """
+ if not mode in ['layer2', 'layer2+3', 'layer3+4', 'encap2+3', 'encap3+4']:
+ raise ValueError("Value out of range")
+ return self._write_sysfs('/sys/class/net/{}/bonding/xmit_hash_policy'
+ .format(self._ifname), mode)
+
+ @property
+ def arp_interval(self):
+ """
+ Specifies the ARP link monitoring frequency in milliseconds.
+
+ The ARP monitor works by periodically checking the slave devices to
+ determine whether they have sent or received traffic recently (the
+ precise criteria depends upon the bonding mode, and the state of the
+ slave). Regular traffic is generated via ARP probes issued for the
+ addresses specified by the arp_ip_target option.
+
+ The default value is 0.
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').arp_interval
+ '0'
+ """
+ return self._read_sysfs('/sys/class/net/{}/bonding/arp_interval'
+ .format(self._ifname))
+
+ @arp_interval.setter
+ def arp_interval(self, time):
+ """
+ Specifies the IP addresses to use as ARP monitoring peers when
+ arp_interval is > 0. These are the targets of the ARP request sent to
+ determine the health of the link to the targets. Specify these values
+ in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by
+ a comma. At least one IP address must be given for ARP monitoring to
+ function. The maximum number of targets that can be specified is 16.
+
+ The default value is no IP addresses.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BondIf('bond0').arp_interval = '100'
+ >>> BondIf('bond0').arp_interval
+ '100'
+ """
+ return self._write_sysfs('/sys/class/net/{}/bonding/arp_interval'
+ .format(self._ifname), time)
+
+ @property
+ def arp_ip_target(self):
+ """
+ Specifies the IP addresses to use as ARP monitoring peers when
+ arp_interval is > 0. These are the targets of the ARP request sent to
+ determine the health of the link to the targets. Specify these values
+ in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by
+ a comma. At least one IP address must be given for ARP monitoring to
+ function. The maximum number of targets that can be specified is 16.
+
+ The default value is no IP addresses.
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').arp_ip_target
+ '192.0.2.1'
+ """
+ return self._read_sysfs('/sys/class/net/{}/bonding/arp_ip_target'
+ .format(self._ifname))
+
+ @arp_ip_target.setter
+ def arp_ip_target(self, target):
+ """
+ Specifies the IP addresses to use as ARP monitoring peers when
+ arp_interval is > 0. These are the targets of the ARP request sent to
+ determine the health of the link to the targets. Specify these values
+ in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by
+ a comma. At least one IP address must be given for ARP monitoring to
+ function. The maximum number of targets that can be specified is 16.
+
+ The default value is no IP addresses.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BondIf('bond0').arp_ip_target = '192.0.2.1'
+ >>> BondIf('bond0').arp_ip_target
+ '192.0.2.1'
+ """
+ return self._write_sysfs('/sys/class/net/{}/bonding/arp_ip_target'
+ .format(self._ifname), target)
+
+ def add_port(self, interface):
+ """
+ Enslave physical interface to bond.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BondIf('bond0').add_port('eth0')
+ >>> BondIf('bond0').add_port('eth1')
+ """
+ # An interface can only be added to a bond if it is in 'down' state. If
+ # interface is in 'up' state, the following Kernel error will be thrown:
+ # bond0: eth1 is up - this may be due to an out of date ifenslave.
+ Interface(interface).state = 'down'
+
+ return self._write_sysfs('/sys/class/net/{}/bonding/slaves'
+ .format(self._ifname), '+' + interface)
+
+ def del_port(self, interface):
+ """
+ Remove physical port from bond
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BondIf('bond0').del_port('eth1')
+ """
+ return self._write_sysfs('/sys/class/net/{}/bonding/slaves'
+ .format(self._ifname), '-' + interface)
+
+ def get_slaves(self):
+ """
+ Return a list with all configured slave interfaces on this bond.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BondIf('bond0').get_slaves()
+ ['eth1', 'eth2']
+ """
+ slaves = self._read_sysfs('/sys/class/net/{}/bonding/slaves'
+ .format(self._ifname))
+ return list(map(str, slaves.split()))
+
+ @property
+ def primary(self):
+ """
+ A string (eth0, eth2, etc) specifying which slave is the primary
+ device. The specified device will always be the active slave while it
+ is available. Only when the primary is off-line will alternate devices
+ be used. This is useful when one slave is preferred over another, e.g.,
+ when one slave has higher throughput than another.
+
+ The primary option is only valid for active-backup, balance-tlb and
+ balance-alb mode.
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').primary
+ 'eth1'
+ """
+ return self._read_sysfs('/sys/class/net/{}/bonding/primary'
+ .format(self._ifname))
+
+ @primary.setter
+ def primary(self, interface):
+ """
+ A string (eth0, eth2, etc) specifying which slave is the primary
+ device. The specified device will always be the active slave while it
+ is available. Only when the primary is off-line will alternate devices
+ be used. This is useful when one slave is preferred over another, e.g.,
+ when one slave has higher throughput than another.
+
+ The primary option is only valid for active-backup, balance-tlb and
+ balance-alb mode.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BondIf('bond0').primary = 'eth2'
+ >>> BondIf('bond0').primary
+ 'eth2'
+ """
+ if not interface:
+ # reset primary interface
+ interface = '\0'
+
+ return self._write_sysfs('/sys/class/net/{}/bonding/primary'
+ .format(self._ifname), interface)
+
+ @property
+ def mode(self):
+ """
+ Specifies one of the bonding policies. The default is balance-rr
+ (round robin).
+
+ Possible values are: balance-rr (0), active-backup (1), balance-xor (2),
+ broadcast (3), 802.3ad (4), balance-tlb (5), balance-alb (6)
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').mode
+ 'balance-rr'
+ """
+ return self._read_sysfs('/sys/class/net/{}/bonding/mode'
+ .format(self._ifname)).split()[0]
+
+ @mode.setter
+ def mode(self, mode):
+ """
+ Specifies one of the bonding policies. The default is balance-rr
+ (round robin).
+
+ Possible values are: balance-rr, active-backup, balance-xor,
+ broadcast, 802.3ad, balance-tlb, balance-alb
+
+ NOTE: the bonding mode can not be changed when the bond itself has
+ slaves
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BondIf('bond0').mode = '802.3ad'
+ >>> BondIf('bond0').mode
+ '802.3ad'
+ """
+ if not mode in [
+ 'balance-rr', 'active-backup', 'balance-xor', 'broadcast',
+ '802.3ad', 'balance-tlb', 'balance-alb']:
+ raise ValueError("Value out of range")
+
+ return self._write_sysfs('/sys/class/net/{}/bonding/mode'
+ .format(self._ifname), mode)
+
+
+class WireGuardIf(Interface):
+ """
+ Wireguard interface class, contains a comnfig dictionary since
+ wireguard VPN is being comnfigured via the wg command rather than
+ writing the config into a file. Otherwise if a pre-shared key is used
+ (symetric enryption key), it would we exposed within multiple files.
+ Currently it's only within the config.boot if the config was saved.
+
+ Example:
+ >>> 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'}
+ >>> 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'}
+ """
+
+ def __init__(self, ifname):
+ super().__init__(ifname, type='wireguard')
+ self.config = {
+ 'port': 0,
+ 'private-key': None,
+ 'pubkey': None,
+ 'psk': '/dev/null',
+ 'allowed-ips': [],
+ 'fwmark': 0x00,
+ 'endpoint': None,
+ 'keepalive': 0
+ }
+
+ def update(self):
+ if not self.config['private-key']:
+ raise ValueError("private key required")
+ else:
+ # fmask permission check?
+ pass
+
+ cmd = "wg set {} ".format(self._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
+ if self.config['endpoint']:
+ cmd += " endpoint {}".format(self.config['endpoint'])
+ cmd += " persistent-keepalive {}".format(self.config['keepalive'])
+
+ 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'])
+
+
+ def remove_peer(self, peerkey):
+ """
+ Remove a peer of an interface, peers are identified by their public key.
+ Giving it a readable name is a vyos feature, to remove a peer the pubkey
+ and the interface is needed, to remove the entry.
+ """
+ cmd = "wg set {0} peer {1} remove".format(
+ self._ifname, str(peerkey))
+ self._cmd(cmd)
+
+
+class VXLANIf(Interface, ):
+ """
+ The VXLAN protocol is a tunnelling protocol designed to solve the
+ problem of limited VLAN IDs (4096) in IEEE 802.1q. With VXLAN the
+ size of the identifier is expanded to 24 bits (16777216).
+
+ VXLAN is described by IETF RFC 7348, and has been implemented by a
+ number of vendors. The protocol runs over UDP using a single
+ destination port. This document describes the Linux kernel tunnel
+ device, there is also a separate implementation of VXLAN for
+ Openvswitch.
+
+ Unlike most tunnels, a VXLAN is a 1 to N network, not just point to
+ point. A VXLAN device can learn the IP address of the other endpoint
+ either dynamically in a manner similar to a learning bridge, or make
+ use of statically-configured forwarding entries.
+
+ For more information please refer to:
+ https://www.kernel.org/doc/Documentation/networking/vxlan.txt
+ """
+ def __init__(self, ifname, config=''):
+ if config:
+ self._ifname = ifname
+
+ if not os.path.exists('/sys/class/net/{}'.format(self._ifname)):
+ # we assume that by default a multicast interface is created
+ group = 'group {}'.format(config['group'])
+
+ # if remote host is specified we ignore the multicast address
+ if config['remote']:
+ group = 'remote {}'.format(config['remote'])
+
+ # an underlay device is not always specified
+ dev = ''
+ if config['dev']:
+ dev = 'dev {}'.format(config['dev'])
+
+ cmd = 'ip link add {intf} type vxlan id {vni} {grp_rem} {dev} dstport {port}' \
+ .format(intf=self._ifname, vni=config['vni'], grp_rem=group, dev=dev, port=config['port'])
+ self._cmd(cmd)
+
+ super().__init__(ifname, type='vxlan')
+
+ @staticmethod
+ def get_config():
+ """
+ VXLAN interfaces require a configuration when they are added using
+ iproute2. This static method will provide the configuration dictionary
+ used by this class.
+
+ Example:
+ >> dict = VXLANIf().get_config()
+ """
+ config = {
+ 'vni': 0,
+ 'dev': '',
+ 'group': '',
+ 'port': 8472, # The Linux implementation of VXLAN pre-dates
+ # the IANA's selection of a standard destination port
+ 'remote': ''
+ }
+ return config
diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py
deleted file mode 100644
index b8bfb707e..000000000
--- a/python/vyos/interfaceconfig.py
+++ /dev/null
@@ -1,376 +0,0 @@
-#!/usr/bin/python3
-
-# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
-#
-# This library is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-#
-# This library is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with this library. If not, see <http://www.gnu.org/licenses/>.
-
-import sys
-import os
-import re
-import json
-import socket
-import subprocess
-
-dhclient_conf_dir = r'/var/lib/dhcp/dhclient_'
-
-class Interface:
- def __init__(self, ifname=None, type=None):
- if not ifname:
- raise Exception("interface name required")
- if not os.path.exists('/sys/class/net/{0}'.format(ifname)) and not type:
- raise Exception("interface {0} not found".format(str(ifname)))
- else:
- if not os.path.exists('/sys/class/net/{0}'.format(ifname)):
- try:
- ret = subprocess.check_output(['ip link add dev ' + str(ifname) + ' type ' + type], stderr=subprocess.STDOUT, shell=True).decode()
- except subprocess.CalledProcessError as e:
- if self._debug():
- self._debug(e)
- if "Operation not supported" in str(e.output.decode()):
- print(str(e.output.decode()))
- sys.exit(0)
-
- self._ifname = str(ifname)
-
-
- @property
- def mtu(self):
- return self._mtu
-
- @mtu.setter
- def mtu(self, mtu=None):
- if mtu < 68 or mtu > 9000:
- raise ValueError("mtu size invalid value")
- self._mtu = mtu
- try:
- ret = subprocess.check_output(['ip link set mtu ' + str(mtu) + ' dev ' + self._ifname], shell=True).decode()
- except subprocess.CalledProcessError as e:
- if self._debug():
- self._debug(e)
-
-
- @property
- def macaddr(self):
- return self._macaddr
-
- @macaddr.setter
- def macaddr(self, mac=None):
- if not re.search('^[a-f0-9:]{17}$', str(mac)):
- raise ValueError("mac address invalid")
- self._macaddr = str(mac)
- try:
- ret = subprocess.check_output(['ip link set address ' + mac + ' ' + self._ifname], shell=True).decode()
- except subprocess.CalledProcessError as e:
- if self._debug():
- self._debug(e)
-
- @property
- def ifalias(self):
- return self._ifalias
-
- @ifalias.setter
- def ifalias(self, ifalias=None):
- if not ifalias:
- self._ifalias = self._ifname
- else:
- self._ifalias = str(ifalias)
- open('/sys/class/net/{0}/ifalias'.format(self._ifname),'w').write(self._ifalias)
-
- @property
- def linkstate(self):
- return self._linkstate
-
- @linkstate.setter
- def linkstate(self, state='up'):
- if str(state).lower() == 'up' or str(state).lower() == 'down':
- self._linkstate = str(state).lower()
- else:
- self._linkstate = 'up'
- try:
- ret = subprocess.check_output(['ip link set dev ' + self._ifname + ' ' + state], shell=True).decode()
- except subprocess.CalledProcessError as e:
- if self._debug():
- self._debug(e)
-
-
-
- def _debug(self, e=None):
- """
- export DEBUG=1 to see debug messages
- """
- if os.getenv('DEBUG') == '1':
- if e:
- print ("Exception raised:\ncommand: {0}\nerror code: {1}\nsubprocess output: {2}".format(e.cmd, e.returncode, e.output.decode()) )
- return True
- return False
-
- def get_mtu(self):
- try:
- ret = subprocess.check_output(['ip -j link list dev ' + self._ifname], shell=True).decode()
- a = json.loads(ret)[0]
- return a['mtu']
- except subprocess.CalledProcessError as e:
- if self._debug():
- self._debug(e)
- return None
-
- def get_macaddr(self):
- try:
- ret = subprocess.check_output(['ip -j -4 link show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode()
- j = json.loads(ret)
- return j[0]['address']
- except subprocess.CalledProcessError as e:
- if self._debug():
- self._debug(e)
- return None
-
- def get_alias(self):
- return open('/sys/class/net/{0}/ifalias'.format(self._ifname),'r').read()
-
- def del_alias(self):
- open('/sys/class/net/{0}/ifalias'.format(self._ifname),'w').write()
-
- def get_link_state(self):
- """
- returns either up/down or None if it can't find the state
- """
- try:
- ret = subprocess.check_output(['ip -j link show ' + self._ifname], shell=True).decode()
- s = json.loads(ret)
- return s[0]['operstate'].lower()
- except subprocess.CalledProcessError as e:
- if self._debug():
- self._debug(e)
- return None
-
- def remove_interface(self):
- try:
- ret = subprocess.check_output(['ip link del dev ' + self._ifname], shell=True).decode()
- return 0
- except subprocess.CalledProcessError as e:
- if self._debug():
- self._debug(e)
- return None
-
- def get_ipv4_addr(self):
- """
- reads all IPs assigned to an interface and returns it in a list,
- or None if no IP address is assigned to the interface
- """
- ips = []
- try:
- ret = subprocess.check_output(['ip -j -4 addr show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode()
- j = json.loads(ret)
- for i in j:
- if len(i) != 0:
- for addr in i['addr_info']:
- ips.append(addr['local'])
- return ips
- except subprocess.CalledProcessError as e:
- if self._debug():
- self._debug(e)
- return None
-
-
- def get_ipv6_addr(self):
- """
- reads all IPs assigned to an interface and returns it in a list,
- or None if no IP address is assigned to the interface
- """
- ips = []
- try:
- ret = subprocess.check_output(['ip -j -6 addr show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode()
- j = json.loads(ret)
- for i in j:
- if len(i) != 0:
- for addr in i['addr_info']:
- ips.append(addr['local'])
- return ips
- except subprocess.CalledProcessError as e:
- if self._debug():
- self._debug(e)
- return None
-
-
- def add_ipv4_addr(self, ipaddr=[]):
- """
- add addresses on the interface
- """
- for ip in ipaddr:
- try:
- ret = subprocess.check_output(['ip -4 address add ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode()
- except subprocess.CalledProcessError as e:
- if self._debug():
- self._debug(e)
- return None
- return True
-
-
- def del_ipv4_addr(self, ipaddr=[]):
- """
- delete addresses on the interface
- """
- for ip in ipaddr:
- try:
- ret = subprocess.check_output(['ip -4 address del ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode()
- except subprocess.CalledProcessError as e:
- if self._debug():
- self._debug(e)
- return None
- return True
-
-
- def add_ipv6_addr(self, ipaddr=[]):
- """
- add addresses on the interface
- """
- for ip in ipaddr:
- try:
- ret = subprocess.check_output(['ip -6 address add ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode()
- except subprocess.CalledProcessError as e:
- if self._debug():
- self._debug(e)
- return None
- return True
-
-
- def del_ipv6_addr(self, ipaddr=[]):
- """
- delete addresses on the interface
- """
- for ip in ipaddr:
- try:
- ret = subprocess.check_output(['ip -6 address del ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode()
- except subprocess.CalledProcessError as e:
- if self._debug():
- self._debug(e)
- return None
- return True
-
-
- #### replace dhcpv4/v6 with systemd.networkd?
- def set_dhcpv4(self):
- conf_file = dhclient_conf_dir + self._ifname + '.conf'
- pidfile = dhclient_conf_dir + self._ifname + '.pid'
- leasefile = dhclient_conf_dir + self._ifname + '.leases'
-
- a = [
- '# generated by interface_config.py',
- 'option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;',
- 'interface \"' + self._ifname + '\" {',
- '\tsend host-name \"' + socket.gethostname() +'\";',
- '\trequest subnet-mask, broadcast-address, routers, domain-name-servers, rfc3442-classless-static-routes, domain-name, interface-mtu;',
- '}'
- ]
-
- cnf = ""
- for ln in a:
- cnf +=str(ln + "\n")
- open(conf_file, 'w').write(cnf)
- if os.path.exists(dhclient_conf_dir + self._ifname + '.pid'):
- try:
- ret = subprocess.check_output(['/sbin/dhclient -4 -r -pf ' + pidfile], shell=True).decode()
- except subprocess.CalledProcessError as e:
- if self._debug():
- self._debug(e)
- try:
- ret = subprocess.check_output(['/sbin/dhclient -4 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self._ifname], shell=True).decode()
- return True
- except subprocess.CalledProcessError as e:
- if self._debug():
- self._debug(e)
- return None
-
- def del_dhcpv4(self):
- conf_file = dhclient_conf_dir + self._ifname + '.conf'
- pidfile = dhclient_conf_dir + self._ifname + '.pid'
- leasefile = dhclient_conf_dir + self._ifname + '.leases'
- if not os.path.exists(pidfile):
- return 1
- try:
- ret = subprocess.check_output(['/sbin/dhclient -4 -r -pf ' + pidfile], shell=True).decode()
- return True
- except subprocess.CalledProcessError as e:
- if self._debug():
- self._debug(e)
- return None
-
- def get_dhcpv4(self):
- pidfile = dhclient_conf_dir + self._ifname + '.pid'
- if not os.path.exists(pidfile):
- print ("no dhcp client running on interface {0}".format(self._ifname))
- return False
- else:
- pid = open(pidfile, 'r').read()
- print("dhclient running on {0} with pid {1}".format(self._ifname, pid))
- return True
-
-
- def set_dhcpv6(self):
- conf_file = dhclient_conf_dir + self._ifname + '.v6conf'
- pidfile = dhclient_conf_dir + self._ifname + '.v6pid'
- leasefile = dhclient_conf_dir + self._ifname + '.v6leases'
- a = [
- '# generated by interface_config.py',
- 'interface \"' + self._ifname + '\" {',
- '\trequest routers, domain-name-servers, domain-name;',
- '}'
- ]
- cnf = ""
- for ln in a:
- cnf +=str(ln + "\n")
- open(conf_file, 'w').write(cnf)
- subprocess.call(['sysctl', '-q', '-w', 'net.ipv6.conf.' + self._ifname + '.accept_ra=0'])
- if os.path.exists(pidfile):
- try:
- ret = subprocess.check_output(['/sbin/dhclient -6 -q -x -pf ' + pidfile], shell=True).decode()
- except subprocess.CalledProcessError as e:
- if self._debug():
- self._debug(e)
- try:
- ret = subprocess.check_output(['/sbin/dhclient -6 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self._ifname], shell=True).decode()
- return True
- except subprocess.CalledProcessError as e:
- if self._debug():
- self._debug(e)
- return None
-
- def del_dhcpv6(self):
- conf_file = dhclient_conf_dir + self._ifname + '.v6conf'
- pidfile = dhclient_conf_dir + self._ifname + '.v6pid'
- leasefile = dhclient_conf_dir + self._ifname + '.v6leases'
- if not os.path.exists(pidfile):
- return 1
- try:
- ret = subprocess.check_output(['/sbin/dhclient -6 -q -x -pf ' + pidfile], shell=True).decode()
- subprocess.call(['sysctl', '-q', '-w', 'net.ipv6.conf.' + self._ifname + '.accept_ra=1'])
- return True
- except subprocess.CalledProcessError as e:
- if self._debug():
- self._debug(e)
- return None
-
- def get_dhcpv6(self):
- pidfile = dhclient_conf_dir + self._ifname + '.v6pid'
- if not os.path.exists(pidfile):
- print ("no dhcpv6 client running on interface {0}".format(self._ifname))
- return False
- else:
- pid = open(pidfile, 'r').read()
- print("dhclientv6 running on {0} with pid {1}".format(self._ifname, pid))
- return True
-
-
-#### TODO: dhcpv6-pd via dhclient
-
diff --git a/python/vyos/interfaces.py b/python/vyos/interfaces.py
index 2e8ee4feb..d69ce9d04 100644
--- a/python/vyos/interfaces.py
+++ b/python/vyos/interfaces.py
@@ -43,3 +43,14 @@ def list_interfaces_of_type(typ):
else:
r = re.compile('^{0}\d+'.format(types_data[typ]))
return list(filter(lambda i: re.match(r, i), all_intfs))
+
+def get_type_of_interface(intf):
+ with open(intf_type_data_file, 'r') as f:
+ types_data = json.load(f)
+
+ for key,val in types_data.items():
+ r = re.compile('^{0}\d+'.format(val))
+ if re.match(r, intf):
+ return key
+
+ raise ValueError("No type found for interface name: {0}".format(intf))
diff --git a/python/vyos/util.py b/python/vyos/util.py
index 6ab606983..67a602f7a 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -15,9 +15,11 @@
import os
import re
+import getpass
import grp
import time
import subprocess
+import sys
import psutil
@@ -176,3 +178,24 @@ def wait_for_commit_lock():
while commit_in_progress():
time.sleep(1)
+def ask_yes_no(question, default=False) -> bool:
+ """Ask a yes/no question via input() and return their answer."""
+ default_msg = "[Y/n]" if default else "[y/N]"
+ while True:
+ sys.stdout.write("%s %s " % (question, default_msg))
+ c = input().lower()
+ if c == '':
+ return default
+ elif c in ("y", "ye", "yes"):
+ return True
+ elif c in ("n", "no"):
+ return False
+ else:
+ sys.stdout.write("Please respond with yes/y or no/n\n")
+
+
+def is_admin() -> bool:
+ """Look if current user is in sudo group"""
+ current_user = getpass.getuser()
+ (_, _, _, admin_group_members) = grp.getgrnam('sudo')
+ return current_user in admin_group_members
diff --git a/python/vyos/validate.py b/python/vyos/validate.py
index 97a401423..258f7f76a 100644
--- a/python/vyos/validate.py
+++ b/python/vyos/validate.py
@@ -16,6 +16,12 @@
import netifaces
import ipaddress
+def is_ip(addr):
+ """
+ Check addr if it is an IPv4 or IPv6 address
+ """
+ return is_ipv4(addr) or is_ipv6(addr)
+
def is_ipv4(addr):
"""
Check addr if it is an IPv4 address/network. Returns True/False
diff --git a/src/completion/list_disks.py b/src/completion/list_disks.py
new file mode 100755
index 000000000..ff1135e23
--- /dev/null
+++ b/src/completion/list_disks.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Completion script used by show disks to collect physical disk
+
+import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument("-e", "--exclude", type=str, help="Exclude specified device from the result list")
+args = parser.parse_args()
+
+disks = set()
+with open('/proc/partitions') as partitions_file:
+ for line in partitions_file:
+ fields = line.strip().split()
+ if len(fields) == 4 and fields[3].isalpha() and fields[3] != 'name':
+ disks.add(fields[3])
+
+if args.exclude:
+ disks.remove(args.exclude)
+
+for disk in disks:
+ print(disk)
diff --git a/src/completion/list_disks.sh b/src/completion/list_disks.sh
deleted file mode 100755
index f32e558fd..000000000
--- a/src/completion/list_disks.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/bin/bash
-
-# Completion script used by show disks to collect physical disk
-
-awk 'NR > 2 && $4 !~ /[0-9]$/ { print $4 }' </proc/partitions
diff --git a/src/completion/list_interfaces.py b/src/completion/list_interfaces.py
index 66432af19..5e444ef78 100755
--- a/src/completion/list_interfaces.py
+++ b/src/completion/list_interfaces.py
@@ -2,15 +2,14 @@
import sys
import argparse
-
import vyos.interfaces
-
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument("-t", "--type", type=str, help="List interfaces of specific type")
group.add_argument("-b", "--broadcast", action="store_true", help="List all broadcast interfaces")
group.add_argument("-br", "--bridgeable", action="store_true", help="List all bridgeable interfaces")
+group.add_argument("-bo", "--bondable", action="store_true", help="List all bondable interfaces")
args = parser.parse_args()
@@ -21,11 +20,13 @@ if args.type:
except ValueError as e:
print(e, file=sys.stderr)
print("")
+
elif args.broadcast:
eth = vyos.interfaces.list_interfaces_of_type("ethernet")
bridge = vyos.interfaces.list_interfaces_of_type("bridge")
bond = vyos.interfaces.list_interfaces_of_type("bonding")
interfaces = eth + bridge + bond
+
elif args.bridgeable:
eth = vyos.interfaces.list_interfaces_of_type("ethernet")
bond = vyos.interfaces.list_interfaces_of_type("bonding")
@@ -34,6 +35,15 @@ elif args.bridgeable:
vxlan = vyos.interfaces.list_interfaces_of_type("vxlan")
wireless = vyos.interfaces.list_interfaces_of_type("wireless")
interfaces = eth + bond + l2tpv3 + openvpn + vxlan + wireless
+
+elif args.bondable:
+ eth = vyos.interfaces.list_interfaces_of_type("ethernet")
+ # we need to filter out VLAN interfaces identified by a dot (.) in their name
+ for intf in eth:
+ if '.' in intf:
+ eth.remove(intf)
+ interfaces = eth
+
else:
interfaces = vyos.interfaces.list_interfaces()
diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py
index 3ca77adee..38f3cb4de 100755
--- a/src/conf_mode/dns_forwarding.py
+++ b/src/conf_mode/dns_forwarding.py
@@ -24,6 +24,7 @@ import jinja2
import netifaces
import vyos.util
+import vyos.hostsd_client
from vyos.config import Config
from vyos import ConfigError
@@ -44,7 +45,7 @@ config_tmpl = """
# Non-configurable defaults
daemon=yes
threads=1
-allow-from=0.0.0.0/0, ::/0
+allow-from={{ allow_from | join(',') }}
log-common-errors=yes
non-local-bind=yes
query-local-address=0.0.0.0
@@ -83,10 +84,10 @@ dnssec={{ dnssec }}
"""
default_config_data = {
+ 'allow_from': [],
'cache_size': 10000,
'export_hosts_file': 'yes',
'listen_on': [],
- 'interfaces': [],
'name_servers': [],
'negative_ttl': 3600,
'domains': [],
@@ -94,19 +95,6 @@ default_config_data = {
}
-# borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX!
-def get_resolvers(file):
- try:
- with open(file, 'r') as resolvconf:
- lines = [line.split('#', 1)[0].rstrip()
- for line in resolvconf.readlines()]
- resolvers = [line.split()[1]
- for line in lines if 'nameserver' in line]
- return resolvers
- except IOError:
- return []
-
-
def get_config(arguments):
dns = default_config_data
conf = Config()
@@ -121,6 +109,9 @@ def get_config(arguments):
conf.set_level('service dns forwarding')
+ if conf.exists('allow-from'):
+ dns['allow_from'] = conf.return_values('allow-from')
+
if conf.exists('cache-size'):
cache_size = conf.return_value('cache-size')
dns['cache_size'] = cache_size
@@ -164,64 +155,27 @@ def get_config(arguments):
if conf.exists('dnssec'):
dns['dnssec'] = conf.return_value('dnssec')
- ## 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'):
- print("WARNING: since VyOS 1.2.0, \"service dns forwarding listen-on\" is a limited compatibility option.")
- print("It will only make DNS forwarder listen on addresses assigned to the interface at the time of commit")
- print("which means it will NOT work properly with VRRP/clustering or addresses received from DHCP.")
- print("Please reconfigure your system with \"service dns forwarding listen-address\" instead.")
-
- interfaces = conf.return_values('listen-on')
-
- listen4 = []
- listen6 = []
- for interface in interfaces:
- try:
- addrs = netifaces.ifaddresses(interface)
- except ValueError:
- print(
- "WARNING: interface {0} does not exist".format(interface))
- continue
-
- if netifaces.AF_INET in addrs.keys():
- for ip4 in addrs[netifaces.AF_INET]:
- listen4.append(ip4['addr'])
-
- if netifaces.AF_INET6 in addrs.keys():
- for ip6 in addrs[netifaces.AF_INET6]:
- listen6.append(ip6['addr'])
-
- if (not listen4) and (not (listen6)):
- print(
- "WARNING: interface {0} has no configured addresses".format(interface))
-
- dns['listen_on'] = 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')
+ hc = vyos.hostsd_client.Client()
+
for interface in interfaces:
- dhcp_resolvers = get_resolvers(
- "/etc/resolv.conf.dhclient-new-{0}".format(interface))
+ dhcp_resolvers = hc.get_name_servers("dhcp-{0}".format(interface))
+ dhcpv6_resolvers = hc.get_name_servers("dhcpv6-{0}".format(interface))
+
if dhcp_resolvers:
dns['name_servers'] = dns['name_servers'] + dhcp_resolvers
+ if dhcpv6_resolvers:
+ dns['name_servers'] = dns['name_servers'] + dhcpv6_resolvers
return dns
-
def bracketize_ipv6_addrs(addrs):
"""Wraps each IPv6 addr in addrs in [], leaving IPv4 addrs untouched."""
return ['[{0}]'.format(a) if a.count(':') > 1 else a for a in addrs]
-
def verify(dns):
# bail out early - looks like removal from running config
if dns is None:
@@ -231,6 +185,10 @@ def verify(dns):
raise ConfigError(
"Error: DNS forwarding requires either a listen-address (preferred) or a listen-on option")
+ if not dns['allow_from']:
+ raise ConfigError(
+ "Error: DNS forwarding requires an allow-from network")
+
if dns['domains']:
for domain in dns['domains']:
if not domain['servers']:
@@ -239,7 +197,6 @@ def verify(dns):
return None
-
def generate(dns):
# bail out early - looks like removal from running config
if dns is None:
@@ -251,16 +208,14 @@ def generate(dns):
f.write(config_text)
return None
-
def apply(dns):
- if dns is not None:
- os.system("systemctl restart pdns-recursor")
- else:
+ if dns is None:
# DNS forwarding is removed in the commit
os.system("systemctl stop pdns-recursor")
if os.path.isfile(config_file):
os.unlink(config_file)
-
+ else:
+ os.system("systemctl restart pdns-recursor")
if __name__ == '__main__':
args = parser.parse_args()
diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py
index 2fad57db6..bb1ec9597 100755
--- a/src/conf_mode/host_name.py
+++ b/src/conf_mode/host_name.py
@@ -30,57 +30,12 @@ import argparse
import jinja2
import vyos.util
+import vyos.hostsd_client
from vyos.config import Config
from vyos import ConfigError
-parser = argparse.ArgumentParser()
-parser.add_argument("--dhclient", action="store_true",
- help="Started from dhclient-script")
-
-config_file_hosts = '/etc/hosts'
-config_file_resolv = '/etc/resolv.conf'
-
-config_tmpl_hosts = """
-### Autogenerated by host_name.py ###
-127.0.0.1 localhost
-127.0.1.1 {{ hostname }}{% if domain_name %}.{{ domain_name }} {{ hostname }}{% endif %}
-
-# The following lines are desirable for IPv6 capable hosts
-::1 localhost ip6-localhost ip6-loopback
-fe00::0 ip6-localnet
-ff00::0 ip6-mcastprefix
-ff02::1 ip6-allnodes
-ff02::2 ip6-allrouters
-
-# static hostname mappings
-{%- if static_host_mapping['hostnames'] %}
-{% for hn in static_host_mapping['hostnames'] -%}
-{{static_host_mapping['hostnames'][hn]['ipaddr']}}\t{{static_host_mapping['hostnames'][hn]['alias']}}\t{{hn}}
-{% endfor -%}
-{%- endif %}
-
-### modifications from other scripts should be added below
-
-"""
-
-config_tmpl_resolv = """
-### Autogenerated by host_name.py ###
-{% for ns in nameserver -%}
-nameserver {{ ns }}
-{% endfor -%}
-
-{%- if domain_name %}
-domain {{ domain_name }}
-{%- endif %}
-
-{%- if domain_search %}
-search {{ domain_search | join(" ") }}
-{%- endif %}
-
-"""
-
default_config_data = {
'hostname': 'vyos',
'domain_name': '',
@@ -89,32 +44,10 @@ default_config_data = {
'no_dhcp_ns': False
}
-# borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX!
-def get_resolvers(file):
- resolv = {}
- try:
- with open(file, 'r') as resolvconf:
- lines = [line.split('#', 1)[0].rstrip()
- for line in resolvconf.readlines()]
- resolvers = [line.split()[1]
- for line in lines if 'nameserver' in line]
- domains = [line.split()[1] for line in lines if 'search' in line]
- resolv['resolvers'] = resolvers
- resolv['domains'] = domains
- return resolv
- except IOError:
- return []
-
-
-def get_config(arguments):
+def get_config():
conf = Config()
hosts = copy.deepcopy(default_config_data)
- if arguments.dhclient:
- conf.exists = conf.exists_effective
- conf.return_value = conf.return_effective_value
- conf.return_values = conf.return_effective_values
-
if conf.exists("system host-name"):
hosts['hostname'] = conf.return_value("system host-name")
# This may happen if the config is not loaded yet,
@@ -136,19 +69,15 @@ def get_config(arguments):
hosts['no_dhcp_ns'] = conf.exists('system disable-dhcp-nameservers')
# system static-host-mapping
- hosts['static_host_mapping'] = {'hostnames': {}}
+ hosts['static_host_mapping'] = []
if conf.exists('system static-host-mapping host-name'):
for hn in conf.list_nodes('system static-host-mapping host-name'):
- hosts['static_host_mapping']['hostnames'][hn] = {
- 'ipaddr': conf.return_value('system static-host-mapping host-name ' + hn + ' inet'),
- 'alias': ''
- }
-
- if conf.exists('system static-host-mapping host-name ' + hn + ' alias'):
- a = conf.return_values(
- 'system static-host-mapping host-name ' + hn + ' alias')
- hosts['static_host_mapping']['hostnames'][hn]['alias'] = " ".join(a)
+ mapping = {}
+ mapping['host'] = hn
+ mapping['address'] = conf.return_value('system static-host-mapping host-name {0} inet'.format(hn))
+ mapping['aliases'] = conf.return_values('system static-host-mapping host-name {0} alias'.format(hn))
+ hosts['static_host_mapping'].append(mapping)
return hosts
@@ -180,83 +109,43 @@ def verify(config):
'The search list is currently limited to 256 characters')
# static mappings alias hostname
- if config['static_host_mapping']['hostnames']:
- for hn in config['static_host_mapping']['hostnames']:
- if not config['static_host_mapping']['hostnames'][hn]['ipaddr']:
- raise ConfigError('IP address required for ' + hn)
- for hn_alias in config['static_host_mapping']['hostnames'][hn]['alias'].split(' '):
- if not hostname_regex.match(hn_alias) and len(hn_alias) != 0:
- raise ConfigError('Invalid hostname alias ' + hn_alias)
+ if config['static_host_mapping']:
+ for m in config['static_host_mapping']:
+ if not m['address']:
+ raise ConfigError('IP address required for ' + m['host'])
+ for a in m['aliases']:
+ if not hostname_regex.match(a) and len(a) != 0:
+ raise ConfigError('Invalid alias \'{0}\' in mapping {1}'.format(a, m['host']))
return None
def generate(config):
+ pass
+
+def apply(config):
if config is None:
return None
- # If "system disable-dhcp-nameservers" is __configured__ all DNS resolvers
- # received via dhclient should not be added into the final 'resolv.conf'.
- #
- # We iterate over every resolver file and retrieve the received nameservers
- # for later adjustment of the system nameservers
- dhcp_ns = []
- dhcp_sd = []
- for file in glob.glob('/etc/resolv.conf.dhclient-new*'):
- for key, value in get_resolvers(file).items():
- ns = [r for r in value if key == 'resolvers']
- dhcp_ns.extend(ns)
- sd = [d for d in value if key == 'domains']
- dhcp_sd.extend(sd)
-
- if not config['no_dhcp_ns']:
- config['nameserver'] += dhcp_ns
- config['domain_search'] += dhcp_sd
-
- # Prune duplicate values
- # Not order preserving, but then when multiple DHCP clients are used,
- # there can't be guarantees about the order anyway
- dhcp_ns = list(set(dhcp_ns))
- dhcp_sd = list(set(dhcp_sd))
-
- # We have third party scripts altering /etc/hosts, too.
- # One example are the DHCP hostname update scripts thus we need to cache in
- # every modification first - so changing domain-name, domain-search or hostname
- # during runtime works
- old_hosts = ""
- with open(config_file_hosts, 'r') as f:
- # Skips text before the beginning of our marker.
- # NOTE: Marker __MUST__ match the one specified in config_tmpl_hosts
- for line in f:
- if line.strip() == '### modifications from other scripts should be added below':
- break
-
- for line in f:
- # This additional line.strip() filters empty lines
- if line.strip():
- old_hosts += line
-
- # Add an additional newline
- old_hosts += '\n'
-
- tmpl = jinja2.Template(config_tmpl_hosts)
- config_text = tmpl.render(config)
-
- with open(config_file_hosts, 'w') as f:
- f.write(config_text)
- f.write(old_hosts)
-
- tmpl = jinja2.Template(config_tmpl_resolv)
- config_text = tmpl.render(config)
- with open(config_file_resolv, 'w') as f:
- f.write(config_text)
+ ## Send the updated data to vyos-hostsd
- return None
+ # vyos-hostsd uses "tags" to identify data sources
+ tag = "static"
+ try:
+ client = vyos.hostsd_client.Client()
-def apply(config):
- if config is None:
- return None
+ client.set_host_name(config['hostname'], config['domain_name'], config['domain_search'])
+
+ client.delete_name_servers(tag)
+ client.add_name_servers(tag, config['nameserver'])
+
+ client.delete_hosts(tag)
+ client.add_hosts(tag, config['static_host_mapping'])
+ except vyos.hostsd_client.VyOSHostsdError as e:
+ raise ConfigError(str(e))
+
+ ## Actually update the hostname -- vyos-hostsd doesn't do that
# No domain name -- the Debian way.
hostname_new = config['hostname']
@@ -283,22 +172,9 @@ def apply(config):
if __name__ == '__main__':
- args = parser.parse_args()
-
- if args.dhclient:
- # There's a big chance it was triggered by a commit still in progress
- # so we need to wait until the new values are in the running config
- vyos.util.wait_for_commit_lock()
-
-
try:
- c = get_config(args)
- # If it's called from dhclient, then either:
- # a) verification was already done at commit time
- # b) it's run on an unconfigured system, e.g. by cloud-init
- # Therefore, verification is either redundant or useless
- if not args.dhclient:
- verify(c)
+ c = get_config()
+ verify(c)
generate(c)
apply(c)
except ConfigError as e:
diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py
index 1f91ac582..9c062f0aa 100755
--- a/src/conf_mode/http-api.py
+++ b/src/conf_mode/http-api.py
@@ -69,6 +69,9 @@ def generate(http_api):
if http_api is None:
return None
+ if not os.path.exists('/etc/vyos'):
+ os.mkdir('/etc/vyos')
+
with open(config_file, 'w') as f:
json.dump(http_api, f, indent=2)
diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py
index 289eacf69..f948063e9 100755
--- a/src/conf_mode/https.py
+++ b/src/conf_mode/https.py
@@ -40,12 +40,21 @@ server {
return 302 https://$server_name$request_uri;
}
+{% for addr, names in listen_addresses.items() %}
server {
# SSL configuration
#
+{% if addr == '*' %}
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
+{% else %}
+ listen {{ addr }}:443 ssl;
+{% endif %}
+
+{% for name in names %}
+ server_name {{ name }};
+{% endfor %}
{% if vyos_cert %}
include {{ vyos_cert.conf }};
@@ -57,9 +66,42 @@ server {
include snippets/snakeoil.conf;
{% endif %}
-{% for l_addr in listen_address %}
- server_name {{ l_addr }};
-{% endfor %}
+ # proxy settings for HTTP API, if enabled; 503, if not
+ location ~ /(retrieve|configure) {
+{% if api %}
+ proxy_pass http://localhost:{{ api.port }};
+ proxy_buffering off;
+{% else %}
+ return 503;
+{% endif %}
+ }
+
+ error_page 501 502 503 =200 @50*_json;
+
+ location @50*_json {
+ default_type application/json;
+ return 200 '{"error": "Start service in configuration mode: set service https api"}';
+ }
+
+}
+{% else %}
+server {
+ # SSL configuration
+ #
+ listen 443 ssl default_server;
+ listen [::]:443 ssl default_server;
+
+ server_name _;
+
+{% if vyos_cert %}
+ include {{ vyos_cert.conf }};
+{% else %}
+ #
+ # Self signed certs generated by the ssl-cert package
+ # Don't use them in a production server!
+ #
+ include snippets/snakeoil.conf;
+{% endif %}
# proxy settings for HTTP API, if enabled; 503, if not
location ~ /(retrieve|configure) {
@@ -79,6 +121,8 @@ server {
}
}
+
+{% endfor %}
"""
def get_config():
@@ -90,8 +134,13 @@ def get_config():
conf.set_level('service https')
if conf.exists('listen-address'):
- addrs = conf.return_values('listen-address')
- https['listen_address'] = addrs[:]
+ addrs = {}
+ for addr in conf.list_nodes('listen-address'):
+ addrs[addr] = ['_']
+ if conf.exists('listen-address {0} server-name'.format(addr)):
+ names = conf.return_values('listen-address {0} server-name'.format(addr))
+ addrs[addr] = names[:]
+ https['listen_addresses'] = addrs
if conf.exists('certificates'):
if conf.exists('certificates system-generated-certificate'):
diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py
new file mode 100755
index 000000000..dc0363fb7
--- /dev/null
+++ b/src/conf_mode/interface-bonding.py
@@ -0,0 +1,469 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+import os
+
+from copy import deepcopy
+from sys import exit
+from netifaces import interfaces
+
+from vyos.ifconfig import BondIf, EthernetIf
+from vyos.configdict import list_diff, vlan_to_dict
+from vyos.config import Config
+from vyos import ConfigError
+
+default_config_data = {
+ 'address': [],
+ 'address_remove': [],
+ 'arp_mon_intvl': 0,
+ 'arp_mon_tgt': [],
+ 'description': '',
+ 'deleted': False,
+ 'dhcp_client_id': '',
+ 'dhcp_hostname': '',
+ 'dhcpv6_prm_only': False,
+ 'dhcpv6_temporary': False,
+ 'disable': False,
+ 'disable_link_detect': 1,
+ 'hash_policy': 'layer2',
+ 'ip_arp_cache_tmo': 30,
+ 'ip_proxy_arp': 0,
+ 'ip_proxy_arp_pvlan': 0,
+ 'intf': '',
+ 'mac': '',
+ 'mode': '802.3ad',
+ 'member': [],
+ 'mtu': 1500,
+ 'primary': '',
+ 'vif_s': [],
+ 'vif_s_remove': [],
+ 'vif': [],
+ 'vif_remove': []
+}
+
+
+def get_bond_mode(mode):
+ if mode == 'round-robin':
+ return 'balance-rr'
+ elif mode == 'active-backup':
+ return 'active-backup'
+ elif mode == 'xor-hash':
+ return 'balance-xor'
+ elif mode == 'broadcast':
+ return 'broadcast'
+ elif mode == '802.3ad':
+ return '802.3ad'
+ elif mode == 'transmit-load-balance':
+ return 'balance-tlb'
+ elif mode == 'adaptive-load-balance':
+ return 'balance-alb'
+ else:
+ raise ConfigError('invalid bond mode "{}"'.format(mode))
+
+
+def apply_vlan_config(vlan, config):
+ """
+ Generic function to apply a VLAN configuration from a dictionary
+ to a VLAN interface
+ """
+
+ if type(vlan) != type(EthernetIf("lo")):
+ raise TypeError()
+
+ # update interface description used e.g. within SNMP
+ vlan.ifalias = config['description']
+ # ignore link state changes
+ vlan.link_detect = config['disable_link_detect']
+ # Maximum Transmission Unit (MTU)
+ vlan.mtu = config['mtu']
+ # Change VLAN interface MAC address
+ if config['mac']:
+ vlan.mac = config['mac']
+
+ # enable/disable VLAN interface
+ if config['disable']:
+ vlan.state = 'down'
+ else:
+ vlan.state = 'up'
+
+ # Configure interface address(es)
+ # - not longer required addresses get removed first
+ # - newly addresses will be added second
+ for addr in config['address_remove']:
+ vlan.del_addr(addr)
+ for addr in config['address']:
+ vlan.add_addr(addr)
+
+
+def get_config():
+ # initialize kernel module if not loaded
+ if not os.path.isfile('/sys/class/net/bonding_masters'):
+ import syslog
+ syslog.syslog(syslog.LOG_NOTICE, "loading bonding kernel module")
+ if os.system('modprobe bonding max_bonds=0 miimon=250') != 0:
+ syslog.syslog(syslog.LOG_NOTICE, "failed loading bonding kernel module")
+ raise ConfigError("failed loading bonding kernel module")
+
+ bond = deepcopy(default_config_data)
+ conf = Config()
+
+ # determine tagNode instance
+ try:
+ bond['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ except KeyError as E:
+ print("Interface not specified")
+
+ # check if bond has been removed
+ cfg_base = 'interfaces bonding ' + bond['intf']
+ if not conf.exists(cfg_base):
+ bond['deleted'] = True
+ return bond
+
+ # set new configuration level
+ conf.set_level(cfg_base)
+
+ # retrieve configured interface addresses
+ if conf.exists('address'):
+ bond['address'] = conf.return_values('address')
+
+ # get interface addresses (currently effective) - to determine which
+ # address is no longer valid and needs to be removed
+ eff_addr = conf.return_effective_values('address')
+ bond['address_remove'] = list_diff(eff_addr, bond['address'])
+
+ # ARP link monitoring frequency in milliseconds
+ if conf.exists('arp-monitor interval'):
+ bond['arp_mon_intvl'] = int(conf.return_value('arp-monitor interval'))
+
+ # IP address to use for ARP monitoring
+ if conf.exists('arp-monitor target'):
+ bond['arp_mon_tgt'] = conf.return_values('arp-monitor target')
+
+ # retrieve interface description
+ if conf.exists('description'):
+ bond['description'] = conf.return_value('description')
+ else:
+ bond['description'] = bond['intf']
+
+ # get DHCP client identifier
+ if conf.exists('dhcp-options client-id'):
+ bond['dhcp_client_id'] = conf.return_value('dhcp-options client-id')
+
+ # DHCP client host name (overrides the system host name)
+ if conf.exists('dhcp-options host-name'):
+ bond['dhcp_hostname'] = conf.return_value('dhcp-options host-name')
+
+ # DHCPv6 only acquire config parameters, no address
+ if conf.exists('dhcpv6-options parameters-only'):
+ bond['dhcpv6_prm_only'] = conf.return_value('dhcpv6-options parameters-only')
+
+ # DHCPv6 temporary IPv6 address
+ if conf.exists('dhcpv6-options temporary'):
+ bond['dhcpv6_temporary'] = conf.return_value('dhcpv6-options temporary')
+
+ # ignore link state changes
+ if conf.exists('disable-link-detect'):
+ bond['disable_link_detect'] = 2
+
+ # disable bond interface
+ if conf.exists('disable'):
+ bond['disable'] = True
+
+ # Bonding transmit hash policy
+ if conf.exists('hash-policy'):
+ bond['hash_policy'] = conf.return_value('hash-policy')
+
+ # ARP cache entry timeout in seconds
+ if conf.exists('ip arp-cache-timeout'):
+ bond['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout'))
+
+ # Enable proxy-arp on this interface
+ if conf.exists('ip enable-proxy-arp'):
+ bond['ip_proxy_arp'] = 1
+
+ # Enable private VLAN proxy ARP on this interface
+ if conf.exists('ip proxy-arp-pvlan'):
+ bond['ip_proxy_arp_pvlan'] = 1
+
+ # Media Access Control (MAC) address
+ if conf.exists('mac'):
+ bond['mac'] = conf.return_value('mac')
+
+ # Bonding mode
+ if conf.exists('mode'):
+ bond['mode'] = get_bond_mode(conf.return_value('mode'))
+
+ # Maximum Transmission Unit (MTU)
+ if conf.exists('mtu'):
+ bond['mtu'] = int(conf.return_value('mtu'))
+
+ # determine bond member interfaces (currently configured)
+ if conf.exists('member interface'):
+ bond['member'] = conf.return_values('member interface')
+
+ # Primary device interface
+ if conf.exists('primary'):
+ bond['primary'] = conf.return_value('primary')
+
+ # re-set configuration level and retrieve vif-s interfaces
+ conf.set_level(cfg_base)
+ # get vif-s interfaces (currently effective) - to determine which vif-s
+ # interface is no longer present and needs to be removed
+ eff_intf = conf.list_effective_nodes('vif-s')
+ act_intf = conf.list_nodes('vif-s')
+ bond['vif_s_remove'] = list_diff(eff_intf, act_intf)
+
+ if conf.exists('vif-s'):
+ for vif_s in conf.list_nodes('vif-s'):
+ # set config level to vif-s interface
+ conf.set_level(cfg_base + ' vif-s ' + vif_s)
+ bond['vif_s'].append(vlan_to_dict(conf))
+
+ # re-set configuration level and retrieve vif-s interfaces
+ conf.set_level(cfg_base)
+ # Determine vif interfaces (currently effective) - to determine which
+ # vif interface is no longer present and needs to be removed
+ eff_intf = conf.list_effective_nodes('vif')
+ act_intf = conf.list_nodes('vif')
+ bond['vif_remove'] = list_diff(eff_intf, act_intf)
+
+ if conf.exists('vif'):
+ for vif in conf.list_nodes('vif'):
+ # set config level to vif interface
+ conf.set_level(cfg_base + ' vif ' + vif)
+ bond['vif'].append(vlan_to_dict(conf))
+
+ return bond
+
+
+def verify(bond):
+ if len (bond['arp_mon_tgt']) > 16:
+ raise ConfigError('The maximum number of targets that can be specified is 16')
+
+ if bond['primary']:
+ if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
+ raise ConfigError('Mode dependency failed, primary not supported ' \
+ 'in this mode.'.format())
+
+ if bond['primary'] not in bond['member']:
+ raise ConfigError('Interface "{}" is not part of the bond' \
+ .format(bond['primary']))
+
+ for vif_s in bond['vif_s']:
+ for vif in bond['vif']:
+ if vif['id'] == vif_s['id']:
+ raise ConfigError('Can not use identical ID on vif and vif-s interface')
+
+
+ conf = Config()
+ for intf in bond['member']:
+ # a bonding member interface is only allowed to be assigned to one bond!
+ all_bonds = conf.list_nodes('interfaces bonding')
+ # We do not need to check our own bond
+ all_bonds.remove(bond['intf'])
+ for tmp in all_bonds:
+ if conf.exists('interfaces bonding ' + tmp + ' member interface ' + intf):
+ raise ConfigError('can not enslave interface {} which already ' \
+ 'belongs to {}'.format(intf, tmp))
+
+ # we can not add disabled slave interfaces to our bond
+ if conf.exists('interfaces ethernet ' + intf + ' disable'):
+ raise ConfigError('can not enslave disabled interface {}' \
+ .format(intf))
+
+ # can not add interfaces with an assigned address to a bond
+ if conf.exists('interfaces ethernet ' + intf + ' address'):
+ raise ConfigError('can not enslave interface {} which has an address ' \
+ 'assigned'.format(intf))
+
+ # bond members are not allowed to be bridge members, too
+ for tmp in conf.list_nodes('interfaces bridge'):
+ if conf.exists('interfaces bridge ' + tmp + ' member interface ' + intf):
+ raise ConfigError('can not enslave interface {} which belongs to ' \
+ 'bridge {}'.format(intf, tmp))
+
+ # bond members are not allowed to be vrrp members, too
+ for tmp in conf.list_nodes('high-availability vrrp group'):
+ if conf.exists('high-availability vrrp group ' + tmp + ' interface ' + intf):
+ raise ConfigError('can not enslave interface {} which belongs to ' \
+ 'VRRP group {}'.format(intf, tmp))
+
+ # bond members are not allowed to be underlaying psuedo-ethernet devices
+ for tmp in conf.list_nodes('interfaces pseudo-ethernet'):
+ if conf.exists('interfaces pseudo-ethernet ' + tmp + ' link ' + intf):
+ raise ConfigError('can not enslave interface {} which belongs to ' \
+ 'pseudo-ethernet {}'.format(intf, tmp))
+
+ # bond members are not allowed to be underlaying vxlan devices
+ for tmp in conf.list_nodes('interfaces vxlan'):
+ if conf.exists('interfaces vxlan ' + tmp + ' link ' + intf):
+ raise ConfigError('can not enslave interface {} which belongs to ' \
+ 'vxlan {}'.format(intf, tmp))
+
+
+ if bond['primary']:
+ if bond['primary'] not in bond['member']:
+ raise ConfigError('primary interface must be a member interface of {}' \
+ .format(bond['intf']))
+
+ if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
+ raise ConfigError('primary interface only works for mode active-backup, ' \
+ 'transmit-load-balance or adaptive-load-balance')
+
+ if bond['arp_mon_intvl'] > 0:
+ if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']:
+ raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \
+ 'transmit-load-balance or adaptive-load-balance')
+
+ return None
+
+
+def generate(bond):
+ return None
+
+
+def apply(bond):
+ b = BondIf(bond['intf'])
+
+ if bond['deleted']:
+ #
+ # delete bonding interface
+ b.remove()
+ else:
+ # Some parameters can not be changed when the bond is up.
+ # Always disable the bond prior changing anything
+ b.state = 'down'
+
+ # The bonding mode can not be changed when there are interfaces enslaved
+ # to this bond, thus we will free all interfaces from the bond first!
+ for intf in b.get_slaves():
+ b.del_port(intf)
+
+ # ARP link monitoring frequency
+ b.arp_interval = bond['arp_mon_intvl']
+ # reset miimon on arp-montior deletion
+ if bond['arp_mon_intvl'] == 0:
+ # reset miimon to default
+ b.bond_miimon = 250
+
+ # ARP monitor targets need to be synchronized between sysfs and CLI.
+ # Unfortunately an address can't be send twice to sysfs as this will
+ # result in the following exception: OSError: [Errno 22] Invalid argument.
+ #
+ # We remove ALL adresses prior adding new ones, this will remove addresses
+ # added manually by the user too - but as we are limited to 16 adresses
+ # from the kernel side this looks valid to me. We won't run into an error
+ # when a user added manual adresses which would result in having more
+ # then 16 adresses in total.
+ arp_tgt_addr = list(map(str, b.arp_ip_target.split()))
+ for addr in arp_tgt_addr:
+ b.arp_ip_target = '-' + addr
+
+ # Add configured ARP target addresses
+ for addr in bond['arp_mon_tgt']:
+ b.arp_ip_target = '+' + addr
+
+ # update interface description used e.g. within SNMP
+ b.ifalias = bond['description']
+
+ #
+ # missing DHCP/DHCPv6 options go here
+ #
+
+ # ignore link state changes
+ b.link_detect = bond['disable_link_detect']
+ # Bonding transmit hash policy
+ b.xmit_hash_policy = bond['hash_policy']
+ # configure ARP cache timeout in milliseconds
+ b.arp_cache_tmp = bond['ip_arp_cache_tmo']
+ # Enable proxy-arp on this interface
+ b.proxy_arp = bond['ip_proxy_arp']
+ # Enable private VLAN proxy ARP on this interface
+ b.proxy_arp_pvlan = bond['ip_proxy_arp_pvlan']
+
+ # Change interface MAC address
+ if bond['mac']:
+ b.mac = bond['mac']
+
+ # Bonding policy
+ b.mode = bond['mode']
+ # Maximum Transmission Unit (MTU)
+ b.mtu = bond['mtu']
+
+ # Primary device interface
+ if bond['primary']:
+ b.primary = bond['primary']
+
+ # Add (enslave) interfaces to bond
+ for intf in bond['member']:
+ b.add_port(intf)
+
+ # As the bond interface is always disabled first when changing
+ # parameters we will only re-enable the interface if it is not
+ # administratively disabled
+ if not bond['disable']:
+ b.state = 'up'
+
+ # Configure interface address(es)
+ # - not longer required addresses get removed first
+ # - newly addresses will be added second
+ for addr in bond['address_remove']:
+ b.del_addr(addr)
+ for addr in bond['address']:
+ b.add_addr(addr)
+
+ # remove no longer required service VLAN interfaces (vif-s)
+ for vif_s in bond['vif_s_remove']:
+ b.del_vlan(vif_s)
+
+ # create service VLAN interfaces (vif-s)
+ for vif_s in bond['vif_s']:
+ s_vlan = b.add_vlan(vif_s['id'], ethertype=vif_s['ethertype'])
+ apply_vlan_config(s_vlan, vif_s)
+
+ # remove no longer required client VLAN interfaces (vif-c)
+ # on lower service VLAN interface
+ for vif_c in vif_s['vif_c_remove']:
+ s_vlan.del_vlan(vif_c)
+
+ # create client VLAN interfaces (vif-c)
+ # on lower service VLAN interface
+ for vif_c in vif_s['vif_c']:
+ c_vlan = s_vlan.add_vlan(vif_c['id'])
+ apply_vlan_config(c_vlan, vif_c)
+
+ # remove no longer required VLAN interfaces (vif)
+ for vif in bond['vif_remove']:
+ b.del_vlan(vif)
+
+ # create VLAN interfaces (vif)
+ for vif in bond['vif']:
+ vlan = b.add_vlan(vif['id'])
+ apply_vlan_config(vlan, vif)
+
+ 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/interface-bridge.py b/src/conf_mode/interface-bridge.py
index 543349e7b..401182a0d 100755
--- a/src/conf_mode/interface-bridge.py
+++ b/src/conf_mode/interface-bridge.py
@@ -17,51 +17,39 @@
#
import os
-import sys
-import copy
-import subprocess
-import vyos.configinterface as VyIfconfig
+from copy import deepcopy
+from sys import exit
+from netifaces import interfaces
+from vyos.ifconfig import BridgeIf, Interface
+from vyos.configdict import list_diff
from vyos.config import Config
from vyos import ConfigError
default_config_data = {
'address': [],
'address_remove': [],
- 'aging': '300',
- 'arp_cache_timeout_ms': '30000',
+ 'aging': 300,
+ 'arp_cache_tmo': 30,
'description': '',
'deleted': False,
- 'dhcp_client_id': '',
- 'dhcp_hostname': '',
- 'dhcpv6_parameters_only': False,
- 'dhcpv6_temporary': False,
'disable': False,
- 'disable_link_detect': False,
- 'forwarding_delay': '15',
- 'hello_time': '2',
+ 'disable_link_detect': 1,
+ 'forwarding_delay': 14,
+ 'hello_time': 2,
'igmp_querier': 0,
'intf': '',
'mac' : '',
- 'max_age': '20',
+ 'max_age': 20,
'member': [],
'member_remove': [],
- 'priority': '32768',
- 'stp': 'off'
+ 'priority': 32768,
+ 'stp': 0
}
-def subprocess_cmd(command):
- process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True)
- proc_stdout = process.communicate()[0].strip()
- pass
-
-def diff(first, second):
- second = set(second)
- return [item for item in first if item not in second]
-
def get_config():
- bridge = copy.deepcopy(default_config_data)
+ bridge = deepcopy(default_config_data)
conf = Config()
# determine tagNode instance
@@ -82,45 +70,34 @@ def get_config():
if conf.exists('address'):
bridge['address'] = conf.return_values('address')
+ # Determine interface addresses (currently effective) - to determine which
+ # address is no longer valid and needs to be removed
+ eff_addr = conf.return_effective_values('address')
+ bridge['address_remove'] = list_diff(eff_addr, bridge['address'])
+
# retrieve aging - how long addresses are retained
if conf.exists('aging'):
- bridge['aging'] = conf.return_value('aging')
+ bridge['aging'] = int(conf.return_value('aging'))
# retrieve interface description
if conf.exists('description'):
bridge['description'] = conf.return_value('description')
- # DHCP client identifier
- if conf.exists('dhcp-options client-id'):
- bridge['dhcp_client_id'] = conf.return_value('dhcp-options client-id')
-
- # DHCP client hostname
- if conf.exists('dhcp-options host-name'):
- bridge['dhcp_hostname'] = conf.return_value('dhcp-options host-name')
-
- # DHCPv6 acquire only config parameters, no address
- if conf.exists('dhcpv6-options parameters-only'):
- bridge['dhcpv6_parameters_only'] = True
-
- # DHCPv6 IPv6 "temporary" address
- if conf.exists('dhcpv6-options temporary'):
- bridge['dhcpv6_temporary'] = True
-
# Disable this bridge interface
if conf.exists('disable'):
bridge['disable'] = True
# Ignore link state changes
if conf.exists('disable-link-detect'):
- bridge['disable_link_detect'] = True
+ bridge['disable_link_detect'] = 2
# Forwarding delay
if conf.exists('forwarding-delay'):
- bridge['forwarding_delay'] = conf.return_value('forwarding-delay')
+ bridge['forwarding_delay'] = int(conf.return_value('forwarding-delay'))
# Hello packet advertisment interval
if conf.exists('hello-time'):
- bridge['hello_time'] = conf.return_value('hello-time')
+ bridge['hello_time'] = int(conf.return_value('hello-time'))
# Enable Internet Group Management Protocol (IGMP) querier
if conf.exists('igmp querier'):
@@ -128,8 +105,7 @@ def get_config():
# ARP cache entry timeout in seconds
if conf.exists('ip arp-cache-timeout'):
- tmp = 1000 * int(conf.return_value('ip arp-cache-timeout'))
- bridge['arp_cache_timeout_ms'] = str(tmp)
+ bridge['arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout'))
# Media Access Control (MAC) address
if conf.exists('mac'):
@@ -137,21 +113,24 @@ def get_config():
# Interval at which neighbor bridges are removed
if conf.exists('max-age'):
- bridge['max_age'] = conf.return_value('max-age')
+ bridge['max_age'] = int(conf.return_value('max-age'))
# Determine bridge member interface (currently configured)
for intf in conf.list_nodes('member interface'):
+ # cost and priority initialized with linux defaults
+ # by reading /sys/devices/virtual/net/br0/brif/eth2/{path_cost,priority}
+ # after adding interface to bridge after reboot
iface = {
'name': intf,
- 'cost': '',
- 'priority': ''
+ 'cost': 100,
+ 'priority': 32
}
if conf.exists('member interface {} cost'.format(intf)):
- iface['cost'] = conf.return_value('member interface {} cost'.format(intf))
+ iface['cost'] = int(conf.return_value('member interface {} cost'.format(intf)))
if conf.exists('member interface {} priority'.format(intf)):
- iface['priority'] = conf.return_value('member interface {} priority'.format(intf))
+ iface['priority'] = int(conf.return_value('member interface {} priority'.format(intf)))
bridge['member'].append(iface)
@@ -159,28 +138,19 @@ def get_config():
# interfaces is no longer assigend to the bridge and thus can be removed
eff_intf = conf.list_effective_nodes('member interface')
act_intf = conf.list_nodes('member interface')
- bridge['member_remove'] = diff(eff_intf, act_intf)
-
- # Determine interface addresses (currently effective) - to determine which
- # address is no longer valid and needs to be removed from the bridge
- eff_addr = conf.return_effective_values('address')
- act_addr = conf.return_values('address')
- bridge['address_remove'] = diff(eff_addr, act_addr)
+ bridge['member_remove'] = list_diff(eff_intf, act_intf)
# Priority for this bridge
if conf.exists('priority'):
- bridge['priority'] = conf.return_value('priority')
+ bridge['priority'] = int(conf.return_value('priority'))
# Enable spanning tree protocol
if conf.exists('stp'):
- bridge['stp'] = 'on'
+ bridge['stp'] = 1
return bridge
def verify(bridge):
- if bridge is None:
- return None
-
conf = Config()
for br in conf.list_nodes('interfaces bridge'):
# it makes no sense to verify ourself in this case
@@ -190,108 +160,88 @@ def verify(bridge):
for intf in bridge['member']:
tmp = conf.list_nodes('interfaces bridge {} member interface'.format(br))
if intf['name'] in tmp:
- raise ConfigError('{} can be assigned to any one bridge only'.format(intf['name']))
+ raise ConfigError('Interface "{}" belongs to bridge "{}" and can not be enslaved.'.format(intf['name'], bridge['intf']))
+
+ # the interface must exist prior adding it to a bridge
+ for intf in bridge['member']:
+ if intf['name'] not in interfaces():
+ raise ConfigError('Can not add non existing interface "{}" to bridge "{}"'.format(intf['name'], bridge['intf']))
+
+ # bridge members are not allowed to be bond members, too
+ for intf in bridge['member']:
+ for bond in conf.list_nodes('interfaces bonding'):
+ if conf.exists('interfaces bonding ' + bond + ' member interface'):
+ if intf['name'] in conf.return_values('interfaces bonding ' + bond + ' member interface'):
+ raise ConfigError('Interface {} belongs to bond {}, can not add it to {}'.format(intf['name'], bond, bridge['intf']))
return None
def generate(bridge):
- if bridge is None:
- return None
-
return None
def apply(bridge):
- if bridge is None:
- return None
+ br = BridgeIf(bridge['intf'])
- cmd = ''
if bridge['deleted']:
- # bridges need to be shutdown first
- cmd += 'ip link set dev "{}" down'.format(bridge['intf'])
- cmd += ' && '
- # delete bridge
- cmd += 'brctl delbr "{}"'.format(bridge['intf'])
- subprocess_cmd(cmd)
-
+ # delete bridge interface
+ # DHCP is stopped inside remove()
+ br.remove()
else:
- # create bridge if it does not exist
- if not os.path.exists("/sys/class/net/" + bridge['intf']):
- # create bridge interface
- cmd += 'brctl addbr "{}"'.format(bridge['intf'])
- cmd += ' && '
- # activate "UP" the interface
- cmd += 'ip link set dev "{}" up'.format(bridge['intf'])
- cmd += ' && '
-
+ # enable interface
+ br.state = 'up'
# set ageing time
- cmd += 'brctl setageing "{}" "{}"'.format(bridge['intf'], bridge['aging'])
- cmd += ' && '
-
+ br.ageing_time = bridge['aging']
# set bridge forward delay
- cmd += 'brctl setfd "{}" "{}"'.format(bridge['intf'], bridge['forwarding_delay'])
- cmd += ' && '
-
+ br.forward_delay = bridge['forwarding_delay']
# set hello time
- cmd += 'brctl sethello "{}" "{}"'.format(bridge['intf'], bridge['hello_time'])
- cmd += ' && '
-
+ br.hello_time = bridge['hello_time']
# set max message age
- cmd += 'brctl setmaxage "{}" "{}"'.format(bridge['intf'], bridge['max_age'])
- cmd += ' && '
-
+ br.max_age = bridge['max_age']
# set bridge priority
- cmd += 'brctl setbridgeprio "{}" "{}"'.format(bridge['intf'], bridge['priority'])
- cmd += ' && '
-
+ br.priority = bridge['priority']
# turn stp on/off
- cmd += 'brctl stp "{}" "{}"'.format(bridge['intf'], bridge['stp'])
-
- for intf in bridge['member_remove']:
- # remove interface from bridge
- cmd += ' && '
- cmd += 'brctl delif "{}" "{}"'.format(bridge['intf'], intf)
-
- for intf in bridge['member']:
- # add interface to bridge
- # but only if it is not yet member of this bridge
- if not os.path.exists('/sys/devices/virtual/net/' + bridge['intf'] + '/brif/' + intf['name']):
- cmd += ' && '
- cmd += 'brctl addif "{}" "{}"'.format(bridge['intf'], intf['name'])
-
- # set bridge port cost
- if intf['cost']:
- cmd += ' && '
- cmd += 'brctl setpathcost "{}" "{}" "{}"'.format(bridge['intf'], intf['name'], intf['cost'])
-
- # set bridge port priority
- if intf['priority']:
- cmd += ' && '
- cmd += 'brctl setportprio "{}" "{}" "{}"'.format(bridge['intf'], intf['name'], intf['priority'])
-
- subprocess_cmd(cmd)
+ br.stp_state = bridge['stp']
+ # enable or disable IGMP querier
+ br.multicast_querier = bridge['igmp_querier']
+ # update interface description used e.g. within SNMP
+ br.ifalias = bridge['description']
# Change interface MAC address
if bridge['mac']:
- VyIfconfig.set_mac_address(bridge['intf'], bridge['mac'])
-
- # update interface description used e.g. within SNMP
- VyIfconfig.set_description(bridge['intf'], bridge['description'])
+ br.mac = bridge['mac']
- # Ignore link state changes?
- VyIfconfig.set_link_detect(bridge['intf'], bridge['disable_link_detect'])
+ # remove interface from bridge
+ for intf in bridge['member_remove']:
+ br.del_port( intf['name'] )
- # enable or disable IGMP querier
- VyIfconfig.set_multicast_querier(bridge['intf'], bridge['igmp_querier'])
+ # add interfaces to bridge
+ for member in bridge['member']:
+ br.add_port(member['name'])
- # ARP cache entry timeout in seconds
- VyIfconfig.set_arp_cache_timeout(bridge['intf'], bridge['arp_cache_timeout_ms'])
+ # up/down interface
+ if bridge['disable']:
+ br.state = 'down'
# Configure interface address(es)
+ # - not longer required addresses get removed first
+ # - newly addresses will be added second
for addr in bridge['address_remove']:
- VyIfconfig.remove_interface_address(bridge['intf'], addr)
-
+ br.del_addr(addr)
for addr in bridge['address']:
- VyIfconfig.add_interface_address(bridge['intf'], addr)
+ br.add_addr(addr)
+
+ # configure additional bridge member options
+ for member in bridge['member']:
+ # set bridge port cost
+ br.set_cost(member['name'], member['cost'])
+ # set bridge port priority
+ br.set_priority(member['name'], member['priority'])
+
+ i = Interface(member['name'])
+ # configure ARP cache timeout
+ i.arp_cache_tmo = bridge['arp_cache_tmo']
+ # ignore link state changes
+ i.link_detect = bridge['disable_link_detect']
return None
@@ -303,4 +253,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/interface-dummy.py b/src/conf_mode/interface-dummy.py
new file mode 100755
index 000000000..614fe08db
--- /dev/null
+++ b/src/conf_mode/interface-dummy.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+from os import environ
+from copy import deepcopy
+from sys import exit
+
+from vyos.ifconfig import DummyIf
+from vyos.configdict import list_diff
+from vyos.config import Config
+from vyos import ConfigError
+
+default_config_data = {
+ 'address': [],
+ 'address_remove': [],
+ 'deleted': False,
+ 'description': '',
+ 'disable': False,
+ 'intf': ''
+}
+
+def get_config():
+ dummy = deepcopy(default_config_data)
+ conf = Config()
+
+ # determine tagNode instance
+ try:
+ dummy['intf'] = environ['VYOS_TAGNODE_VALUE']
+ except KeyError as E:
+ print("Interface not specified")
+
+ # Check if interface has been removed
+ if not conf.exists('interfaces dummy ' + dummy['intf']):
+ dummy['deleted'] = True
+ return dummy
+
+ # set new configuration level
+ conf.set_level('interfaces dummy ' + dummy['intf'])
+
+ # retrieve configured interface addresses
+ if conf.exists('address'):
+ dummy['address'] = conf.return_values('address')
+
+ # retrieve interface description
+ if conf.exists('description'):
+ dummy['description'] = conf.return_value('description')
+
+ # Disable this interface
+ if conf.exists('disable'):
+ dummy['disable'] = True
+
+ # Determine interface addresses (currently effective) - to determine which
+ # address is no longer valid and needs to be removed from the interface
+ eff_addr = conf.return_effective_values('address')
+ act_addr = conf.return_values('address')
+ dummy['address_remove'] = list_diff(eff_addr, act_addr)
+
+ return dummy
+
+def verify(dummy):
+ return None
+
+def generate(dummy):
+ return None
+
+def apply(dummy):
+ du = DummyIf(dummy['intf'])
+
+ # Remove dummy interface
+ if dummy['deleted']:
+ du.remove()
+ else:
+ # enable interface
+ du.state = 'up'
+ # update interface description used e.g. within SNMP
+ du.ifalias = dummy['description']
+
+ # Configure interface address(es)
+ # - not longer required addresses get removed first
+ # - newly addresses will be added second
+ for addr in dummy['address_remove']:
+ du.del_addr(addr)
+ for addr in dummy['address']:
+ du.add_addr(addr)
+
+ # disable interface on demand
+ if dummy['disable']:
+ du.state = 'down'
+
+ 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/interface-loopback.py b/src/conf_mode/interface-loopback.py
new file mode 100755
index 000000000..a1a807868
--- /dev/null
+++ b/src/conf_mode/interface-loopback.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+from os import environ
+from sys import exit
+from copy import deepcopy
+
+from vyos.ifconfig import LoopbackIf
+from vyos.configdict import list_diff
+from vyos.config import Config
+from vyos import ConfigError
+
+default_config_data = {
+ 'address': [],
+ 'address_remove': [],
+ 'deleted': False,
+ 'description': '',
+}
+
+
+def get_config():
+ loopback = deepcopy(default_config_data)
+ conf = Config()
+
+ # determine tagNode instance
+ try:
+ loopback['intf'] = environ['VYOS_TAGNODE_VALUE']
+ except KeyError as E:
+ print("Interface not specified")
+
+ # Check if interface has been removed
+ if not conf.exists('interfaces loopback ' + loopback['intf']):
+ loopback['deleted'] = True
+
+ # set new configuration level
+ conf.set_level('interfaces loopback ' + loopback['intf'])
+
+ # retrieve configured interface addresses
+ if conf.exists('address'):
+ loopback['address'] = conf.return_values('address')
+
+ # retrieve interface description
+ if conf.exists('description'):
+ loopback['description'] = conf.return_value('description')
+
+ # Determine interface addresses (currently effective) - to determine which
+ # address is no longer valid and needs to be removed from the interface
+ eff_addr = conf.return_effective_values('address')
+ act_addr = conf.return_values('address')
+ loopback['address_remove'] = list_diff(eff_addr, act_addr)
+
+ return loopback
+
+def verify(loopback):
+ return None
+
+def generate(loopback):
+ return None
+
+def apply(loopback):
+ lo = LoopbackIf(loopback['intf'])
+ if not loopback['deleted']:
+ # update interface description used e.g. within SNMP
+ # update interface description used e.g. within SNMP
+ lo.ifalias = loopback['description']
+
+ # Configure interface address(es)
+ # - not longer required addresses get removed first
+ # - newly addresses will be added second
+ for addr in loopback['address']:
+ lo.add_addr(addr)
+
+ # remove interface address(es)
+ for addr in loopback['address_remove']:
+ lo.del_addr(addr)
+
+ 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/interface-openvpn.py b/src/conf_mode/interface-openvpn.py
index 4e5915d4e..548c78535 100755
--- a/src/conf_mode/interface-openvpn.py
+++ b/src/conf_mode/interface-openvpn.py
@@ -18,24 +18,25 @@
import os
import re
-import pwd
-import grp
import sys
import stat
-import copy
import jinja2
-import psutil
-from ipaddress import ip_address,ip_network,IPv4Interface
-from signal import SIGUSR1
+from copy import deepcopy
+from grp import getgrnam
+from ipaddress import ip_address,ip_network,IPv4Interface
+from netifaces import interfaces
+from psutil import pid_exists
+from pwd import getpwnam
from subprocess import Popen, PIPE
+from time import sleep
from vyos.config import Config
from vyos import ConfigError
from vyos.validate import is_addr_assigned
-user = 'nobody'
-group = 'nogroup'
+user = 'openvpn'
+group = 'openvpn'
# Please be careful if you edit the template.
config_tmpl = """
@@ -58,6 +59,7 @@ dev {{ intf }}
user {{ uid }}
group {{ gid }}
persist-key
+iproute /usr/libexec/vyos/system/unpriv-ip
proto {% if 'tcp-active' in protocol -%}tcp-client{% elif 'tcp-passive' in protocol -%}tcp-server{% else %}udp{% endif %}
@@ -301,8 +303,8 @@ def openvpn_mkdir(directory):
# fix permissions - corresponds to mode 755
os.chmod(directory, stat.S_IRWXU|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH)
- uid = pwd.getpwnam(user).pw_uid
- gid = grp.getgrnam(group).gr_gid
+ uid = getpwnam(user).pw_uid
+ gid = getgrnam(group).gr_gid
os.chown(directory, uid, gid)
def fixup_permission(filename, permission=stat.S_IRUSR):
@@ -314,8 +316,8 @@ def fixup_permission(filename, permission=stat.S_IRUSR):
os.chmod(filename, permission)
# make file owned by root / vyattacfg
- uid = pwd.getpwnam('root').pw_uid
- gid = grp.getgrnam('vyattacfg').gr_gid
+ uid = getpwnam('root').pw_uid
+ gid = getgrnam('vyattacfg').gr_gid
os.chown(filename, uid, gid)
def checkCertHeader(header, filename):
@@ -334,7 +336,7 @@ def checkCertHeader(header, filename):
return False
def get_config():
- openvpn = copy.deepcopy(default_config_data)
+ openvpn = deepcopy(default_config_data)
conf = Config()
# determine tagNode instance
@@ -792,8 +794,8 @@ def generate(openvpn):
fixup_permission(auth_file)
# get numeric uid/gid
- uid = pwd.getpwnam(user).pw_uid
- gid = grp.getgrnam(group).gr_gid
+ uid = getpwnam(user).pw_uid
+ gid = getgrnam(group).gr_gid
# Generate client specific configuration
for client in openvpn['client']:
@@ -806,6 +808,11 @@ def generate(openvpn):
tmpl = jinja2.Template(config_tmpl)
config_text = tmpl.render(openvpn)
+
+ # we need to support quoting of raw parameters from OpenVPN CLI
+ # see https://phabricator.vyos.net/T1632
+ config_text = config_text.replace("&quot;",'"')
+
with open(get_config_name(interface), 'w') as f:
f.write(config_text)
os.chown(get_config_name(interface), uid, gid)
@@ -813,47 +820,46 @@ def generate(openvpn):
return None
def apply(openvpn):
- interface = openvpn['intf']
-
pid = 0
- pidfile = '/var/run/openvpn/{}.pid'.format(interface)
+ pidfile = '/var/run/openvpn/{}.pid'.format(openvpn['intf'])
if os.path.isfile(pidfile):
pid = 0
with open(pidfile, 'r') as f:
pid = int(f.read())
- # If tunnel interface has been deleted - stop service
- if openvpn['deleted'] or openvpn['disable']:
- directory = os.path.dirname(get_config_name(interface))
+ # Always stop OpenVPN service. We can not send a SIGUSR1 for restart of the
+ # service as the configuration is not re-read. Stop daemon only if it's
+ # running - it could have died or killed by someone evil
+ if pid_exists(pid):
+ cmd = 'start-stop-daemon --stop --quiet'
+ cmd += ' --pidfile ' + pidfile
+ subprocess_cmd(cmd)
- # we only need to stop the demon if it's running
- # daemon could have died or killed by someone
- if psutil.pid_exists(pid):
- cmd = 'start-stop-daemon --stop --quiet'
- cmd += ' --pidfile ' + pidfile
- subprocess_cmd(cmd)
-
- # cleanup old PID file
- if os.path.isfile(pidfile):
- os.remove(pidfile)
+ # cleanup old PID file
+ if os.path.isfile(pidfile):
+ os.remove(pidfile)
+ # Do some cleanup when OpenVPN is disabled/deleted
+ if openvpn['deleted'] or openvpn['disable']:
# cleanup old configuration file
- if os.path.isfile(get_config_name(interface)):
- os.remove(get_config_name(interface))
+ if os.path.isfile(get_config_name(openvpn['intf'])):
+ os.remove(get_config_name(openvpn['intf']))
# cleanup client config dir
- if os.path.isdir(directory + '/ccd/' + interface):
+ directory = os.path.dirname(get_config_name(openvpn['intf']))
+ if os.path.isdir(directory + '/ccd/' + openvpn['intf']):
try:
- os.remove(directory + '/ccd/' + interface + '/*')
+ os.remove(directory + '/ccd/' + openvpn['intf'] + '/*')
except:
pass
return None
- # Send SIGUSR1 to the process instead of creating a new process
- if psutil.pid_exists(pid):
- os.kill(pid, SIGUSR1)
- return None
+ # On configuration change we need to wait for the 'old' interface to
+ # vanish from the Kernel, if it is not gone, OpenVPN will report:
+ # ERROR: Cannot ioctl TUNSETIFF vtun10: Device or resource busy (errno=16)
+ while openvpn['intf'] in interfaces():
+ sleep(0.250) # 250ms
# No matching OpenVPN process running - maybe it got killed or none
# existed - nevertheless, spawn new OpenVPN process
@@ -862,13 +868,13 @@ def apply(openvpn):
cmd += ' --exec /usr/sbin/openvpn'
# now pass arguments to openvpn binary
cmd += ' --'
- cmd += ' --config ' + get_config_name(interface)
+ cmd += ' --config ' + get_config_name(openvpn['intf'])
# execute assembled command
subprocess_cmd(cmd)
-
return None
+
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/interface-vxlan.py b/src/conf_mode/interface-vxlan.py
new file mode 100755
index 000000000..59022238e
--- /dev/null
+++ b/src/conf_mode/interface-vxlan.py
@@ -0,0 +1,208 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+from os import environ
+from sys import exit
+from copy import deepcopy
+
+from vyos.configdict import list_diff
+from vyos.config import Config
+from vyos.ifconfig import VXLANIf, Interface
+from vyos.interfaces import get_type_of_interface
+from vyos import ConfigError
+from netifaces import interfaces
+
+default_config_data = {
+ 'address': [],
+ 'address_remove': [],
+ 'deleted': False,
+ 'description': '',
+ 'disable': False,
+ 'group': '',
+ 'intf': '',
+ 'ip_arp_cache_tmo': 30,
+ 'ip_proxy_arp': 0,
+ 'link': '',
+ 'mtu': 1450,
+ 'remote': '',
+ 'remote_port': 8472 # The Linux implementation of VXLAN pre-dates
+ # the IANA's selection of a standard destination port
+}
+
+
+def get_config():
+ vxlan = deepcopy(default_config_data)
+ conf = Config()
+
+ # determine tagNode instance
+ try:
+ vxlan['intf'] = environ['VYOS_TAGNODE_VALUE']
+ except KeyError as E:
+ print("Interface not specified")
+
+ # Check if interface has been removed
+ if not conf.exists('interfaces vxlan ' + vxlan['intf']):
+ vxlan['deleted'] = True
+ return vxlan
+
+ # set new configuration level
+ conf.set_level('interfaces vxlan ' + vxlan['intf'])
+
+ # retrieve configured interface addresses
+ if conf.exists('address'):
+ vxlan['address'] = conf.return_values('address')
+
+ # Determine interface addresses (currently effective) - to determine which
+ # address is no longer valid and needs to be removed from the interface
+ eff_addr = conf.return_effective_values('address')
+ act_addr = conf.return_values('address')
+ vxlan['address_remove'] = list_diff(eff_addr, act_addr)
+
+ # retrieve interface description
+ if conf.exists('description'):
+ vxlan['description'] = conf.return_value('description')
+
+ # Disable this interface
+ if conf.exists('disable'):
+ vxlan['disable'] = True
+
+ # VXLAN multicast grou
+ if conf.exists('group'):
+ vxlan['group'] = conf.return_value('group')
+
+ # ARP cache entry timeout in seconds
+ if conf.exists('ip arp-cache-timeout'):
+ vxlan['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout'))
+
+ # Enable proxy-arp on this interface
+ if conf.exists('ip enable-proxy-arp'):
+ vxlan['ip_proxy_arp'] = 1
+
+ # VXLAN underlay interface
+ if conf.exists('link'):
+ vxlan['link'] = conf.return_value('link')
+
+ # Maximum Transmission Unit (MTU)
+ if conf.exists('mtu'):
+ vxlan['mtu'] = int(conf.return_value('mtu'))
+
+ # Remote address of VXLAN tunnel
+ if conf.exists('remote'):
+ vxlan['remote'] = conf.return_value('remote')
+
+ # Remote port of VXLAN tunnel
+ if conf.exists('port'):
+ vxlan['remote_port'] = int(conf.return_value('port'))
+
+ # Virtual Network Identifier
+ if conf.exists('vni'):
+ vxlan['vni'] = conf.return_value('vni')
+
+ return vxlan
+
+
+def verify(vxlan):
+ if vxlan['deleted']:
+ # bail out early
+ return None
+
+ if vxlan['mtu'] < 1500:
+ print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU')
+
+ if vxlan['group'] and not vxlan['link']:
+ raise ConfigError('Multicast VXLAN requires an underlaying interface ')
+
+ if not (vxlan['group'] or vxlan['remote']):
+ raise ConfigError('Group or remote must be configured')
+
+ if not vxlan['vni']:
+ raise ConfigError('Must configure VNI for VXLAN')
+
+ if vxlan['link']:
+ # VXLAN adds a 50 byte overhead - we need to check the underlaying MTU
+ # if our configured MTU is at least 50 bytes less
+ underlay_mtu = int(Interface(vxlan['link']).mtu)
+ if underlay_mtu < (vxlan['mtu'] + 50):
+ raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \
+ 'MTU is to small ({})'.format(underlay_mtu))
+
+ return None
+
+
+def generate(vxlan):
+ return None
+
+
+def apply(vxlan):
+ # Check if the VXLAN interface already exists
+ if vxlan['intf'] in interfaces():
+ v = VXLANIf(vxlan['intf'])
+ # VXLAN is super picky and the tunnel always needs to be recreated,
+ # thus we can simply always delete it first.
+ v.remove()
+
+ if not vxlan['deleted']:
+ # VXLAN interface needs to be created on-block
+ # instead of passing a ton of arguments, I just use a dict
+ # that is managed by vyos.ifconfig
+ conf = deepcopy(VXLANIf.get_config())
+
+ # Assign VXLAN instance configuration parameters to config dict
+ conf['vni'] = vxlan['vni']
+ conf['group'] = vxlan['group']
+ conf['dev'] = vxlan['link']
+ conf['remote'] = vxlan['remote']
+ conf['port'] = vxlan['remote_port']
+
+ # Finally create the new interface
+ v = VXLANIf(vxlan['intf'], config=conf)
+ # update interface description used e.g. by SNMP
+ v.ifalias = vxlan['description']
+ # Maximum Transfer Unit (MTU)
+ v.mtu = vxlan['mtu']
+
+ # configure ARP cache timeout in milliseconds
+ v.arp_cache_tmp = vxlan['ip_arp_cache_tmo']
+ # Enable proxy-arp on this interface
+ v.proxy_arp = vxlan['ip_proxy_arp']
+
+ # Configure interface address(es)
+ # - not longer required addresses get removed first
+ # - newly addresses will be added second
+ for addr in vxlan['address_remove']:
+ v.del_addr(addr)
+ for addr in vxlan['address']:
+ v.add_addr(addr)
+
+ # As the bond interface is always disabled first when changing
+ # parameters we will only re-enable the interface if it is not
+ # administratively disabled
+ if not vxlan['disable']:
+ v.state='up'
+
+ 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/interface-wireguard.py b/src/conf_mode/interface-wireguard.py
index 8234fad0b..d51a7a08d 100755
--- a/src/conf_mode/interface-wireguard.py
+++ b/src/conf_mode/interface-wireguard.py
@@ -24,350 +24,243 @@ import subprocess
from vyos.config import Config
from vyos import ConfigError
+from vyos.ifconfig import WireGuardIf
-dir = r'/config/auth/wireguard'
-pk = dir + '/private.key'
-pub = dir + '/public.key'
-psk_file = r'/tmp/psk'
+ifname = str(os.environ['VYOS_TAGNODE_VALUE'])
+intfc = WireGuardIf(ifname)
+
+kdir = r'/config/auth/wireguard'
def check_kmod():
- if not os.path.exists('/sys/module/wireguard'):
- sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod")
- if os.system('sudo modprobe wireguard') != 0:
- sl.syslog(sl.LOG_NOTICE, "modprobe wireguard failed")
- raise ConfigError("modprobe wireguard failed")
+ if not os.path.exists('/sys/module/wireguard'):
+ sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod")
+ if os.system('sudo modprobe wireguard') != 0:
+ sl.syslog(sl.LOG_NOTICE, "modprobe wireguard failed")
+ raise ConfigError("modprobe wireguard failed")
+
def get_config():
- c = Config()
- if not c.exists('interfaces wireguard'):
- return None
-
- c.set_level('interfaces')
- intfcs = c.list_nodes('wireguard')
- intfcs_eff = c.list_effective_nodes('wireguard')
- new_lst = list(set(intfcs) - set(intfcs_eff))
- del_lst = list(set(intfcs_eff) - set(intfcs))
-
- config_data = {
- 'interfaces' : {}
- }
- ### setting defaults and determine status of the config
- for intfc in intfcs:
- cnf = 'wireguard ' + intfc
- # default data struct
- config_data['interfaces'].update(
- {
- intfc : {
- 'addr' : '',
- 'descr' : intfc, ## snmp ifAlias
- 'lport' : '',
- 'status' : 'exists',
- 'state' : 'enabled',
- 'fwmark' : 0x00,
- 'mtu' : '1420',
- 'peer' : {}
- }
+ c = Config()
+ if not c.exists('interfaces wireguard'):
+ return None
+
+ config_data = {
+ ifname: {
+ 'addr': '',
+ 'descr': ifname,
+ 'lport': None,
+ 'status': 'exists',
+ 'state': 'enabled',
+ 'fwmark': 0x00,
+ 'mtu': 1420,
+ 'peer': {},
+ 'pk' : '{}/default/private.key'.format(kdir)
}
- )
-
- ### determine status either delete or create
- for i in new_lst:
- config_data['interfaces'][i]['status'] = 'create'
-
- for i in del_lst:
- config_data['interfaces'].update(
- {
- i : {
- 'status': 'delete'
- }
- }
- )
-
- ### based on the status, setup conf values
- for intfc in intfcs:
- cnf = 'wireguard ' + intfc
- if config_data['interfaces'][intfc]['status'] != 'delete':
- ### addresses
- if c.exists(cnf + ' address'):
- config_data['interfaces'][intfc]['addr'] = c.return_values(cnf + ' address')
- ### interface up/down
- if c.exists(cnf + ' disable'):
- config_data['interfaces'][intfc]['state'] = 'disable'
- ### listen port
- if c.exists(cnf + ' port'):
- config_data['interfaces'][intfc]['lport'] = c.return_value(cnf + ' port')
- ### fwmark
- if c.exists(cnf + ' fwmark'):
- config_data['interfaces'][intfc]['fwmark'] = c.return_value(cnf + ' fwmark')
- ### description
- if c.exists(cnf + ' description'):
- config_data['interfaces'][intfc]['descr'] = c.return_value(cnf + ' description')
- ### mtu
- if c.exists(cnf + ' mtu'):
- config_data['interfaces'][intfc]['mtu'] = c.return_value(cnf + ' mtu')
- ### peers
- if c.exists(cnf + ' peer'):
- for p in c.list_nodes(cnf + ' peer'):
- if not c.exists(cnf + ' peer ' + p + ' disable'):
- config_data['interfaces'][intfc]['peer'].update(
- {
- p : {
- 'allowed-ips' : [],
- 'endpoint' : '',
- 'pubkey' : ''
- }
- }
- )
- if c.exists(cnf + ' peer ' + p + ' pubkey'):
- config_data['interfaces'][intfc]['peer'][p]['pubkey'] = c.return_value(cnf + ' peer ' + p + ' pubkey')
- if c.exists(cnf + ' peer ' + p + ' allowed-ips'):
- config_data['interfaces'][intfc]['peer'][p]['allowed-ips'] = c.return_values(cnf + ' peer ' + p + ' allowed-ips')
- if c.exists(cnf + ' peer ' + p + ' endpoint'):
- config_data['interfaces'][intfc]['peer'][p]['endpoint'] = c.return_value(cnf + ' peer ' + p + ' endpoint')
- if c.exists(cnf + ' peer ' + p + ' persistent-keepalive'):
- config_data['interfaces'][intfc]['peer'][p]['persistent-keepalive'] = c.return_value(cnf + ' peer ' + p + ' persistent-keepalive')
- if c.exists(cnf + ' peer ' + p + ' preshared-key'):
- config_data['interfaces'][intfc]['peer'][p]['psk'] = c.return_value(cnf + ' peer ' + p + ' preshared-key')
-
- return config_data
+ }
+
+ c.set_level('interfaces wireguard')
+ if not c.exists_effective(ifname):
+ config_data[ifname]['status'] = 'create'
+
+ if not c.exists(ifname) and c.exists_effective(ifname):
+ config_data[ifname]['status'] = 'delete'
+
+ if config_data[ifname]['status'] != 'delete':
+ if c.exists(ifname + ' address'):
+ config_data[ifname]['addr'] = c.return_values(ifname + ' address')
+ if c.exists(ifname + ' disable'):
+ config_data[ifname]['state'] = 'disable'
+ if c.exists(ifname + ' port'):
+ config_data[ifname]['lport'] = c.return_value(ifname + ' port')
+ if c.exists(ifname + ' fwmark'):
+ config_data[ifname]['fwmark'] = c.return_value(ifname + ' fwmark')
+ if c.exists(ifname + ' description'):
+ config_data[ifname]['descr'] = c.return_value(
+ ifname + ' description')
+ if c.exists(ifname + ' mtu'):
+ config_data[ifname]['mtu'] = c.return_value(ifname + ' mtu')
+ if c.exists(ifname + ' private-key'):
+ config_data[ifname]['pk'] = "{0}/{1}/private.key".format(kdir,c.return_value(ifname + ' private-key'))
+ if c.exists(ifname + ' peer'):
+ for p in c.list_nodes(ifname + ' peer'):
+ if not c.exists(ifname + ' peer ' + p + ' disable'):
+ config_data[ifname]['peer'].update(
+ {
+ p: {
+ 'allowed-ips': [],
+ 'endpoint': '',
+ 'pubkey': ''
+ }
+ }
+ )
+ if c.exists(ifname + ' peer ' + p + ' pubkey'):
+ config_data[ifname]['peer'][p]['pubkey'] = c.return_value(
+ ifname + ' peer ' + p + ' pubkey')
+ if c.exists(ifname + ' peer ' + p + ' allowed-ips'):
+ config_data[ifname]['peer'][p]['allowed-ips'] = c.return_values(
+ ifname + ' peer ' + p + ' allowed-ips')
+ if c.exists(ifname + ' peer ' + p + ' endpoint'):
+ config_data[ifname]['peer'][p]['endpoint'] = c.return_value(
+ ifname + ' peer ' + p + ' endpoint')
+ if c.exists(ifname + ' peer ' + p + ' persistent-keepalive'):
+ config_data[ifname]['peer'][p]['persistent-keepalive'] = c.return_value(
+ ifname + ' peer ' + p + ' persistent-keepalive')
+ if c.exists(ifname + ' peer ' + p + ' preshared-key'):
+ config_data[ifname]['peer'][p]['psk'] = c.return_value(
+ ifname + ' peer ' + p + ' preshared-key')
+
+ return config_data
def verify(c):
- if not c:
- return None
+ if not c:
+ return None
- for i in c['interfaces']:
- if c['interfaces'][i]['status'] != 'delete':
- if not c['interfaces'][i]['addr']:
- raise ConfigError("address required for interface " + i)
- if not c['interfaces'][i]['peer']:
- raise ConfigError("peer required on interface " + i)
+ if not os.path.exists(c[ifname]['pk']):
+ raise ConfigError(
+ "No keys found, generate them by executing: \'run generate wireguard [keypair|named-keypairs]\'")
- for p in c['interfaces'][i]['peer']:
- if not c['interfaces'][i]['peer'][p]['allowed-ips']:
- raise ConfigError("allowed-ips required on interface " + i + " for peer " + p)
- if not c['interfaces'][i]['peer'][p]['pubkey']:
- raise ConfigError("pubkey from your peer is mandatory on " + i + " for peer " + p)
+ if c[ifname]['status'] != 'delete':
+ if not c[ifname]['addr']:
+ raise ConfigError("ERROR: IP address required")
+ if not c[ifname]['peer']:
+ raise ConfigError("ERROR: peer required")
+ for p in c[ifname]['peer']:
+ if not c[ifname]['peer'][p]['allowed-ips']:
+ raise ConfigError("ERROR: allowed-ips required for peer " + p)
+ if not c[ifname]['peer'][p]['pubkey']:
+ raise ConfigError("peer pubkey required for peer " + p)
def apply(c):
- ### no wg config left, delete all wireguard devices on the os
- if not c:
- net_devs = os.listdir('/sys/class/net/')
- for dev in net_devs:
- if os.path.isdir('/sys/class/net/' + dev):
- buf = open('/sys/class/net/' + dev + '/uevent', 'r').read()
- if re.search("DEVTYPE=wireguard", buf, re.I|re.M):
- wg_intf = re.sub("INTERFACE=", "", re.search("INTERFACE=.*", buf, re.I|re.M).group(0))
- sl.syslog(sl.LOG_NOTICE, "removing interface " + wg_intf)
- subprocess.call(['ip l d dev ' + wg_intf + ' >/dev/null'], shell=True)
- return None
-
- ###
- ## find the diffs between effective config an new config
- ###
- c_eff = Config()
- c_eff.set_level('interfaces wireguard')
-
- ### link status up/down aka interface disable
-
- for intf in c['interfaces']:
- if not c['interfaces'][intf]['status'] == 'delete':
- if c['interfaces'][intf]['state'] == 'disable':
- sl.syslog(sl.LOG_NOTICE, "disable interface " + intf)
- subprocess.call(['ip l s dev ' + intf + ' down ' + ' &>/dev/null'], shell=True)
- else:
- sl.syslog(sl.LOG_NOTICE, "enable interface " + intf)
- subprocess.call(['ip l s dev ' + intf + ' up ' + ' &>/dev/null'], shell=True)
-
- ### deletion of a specific interface
- for intf in c['interfaces']:
- if c['interfaces'][intf]['status'] == 'delete':
- sl.syslog(sl.LOG_NOTICE, "removing interface " + intf)
- subprocess.call(['ip l d dev ' + intf + ' &>/dev/null'], shell=True)
-
- ### peer deletion
- peer_eff = c_eff.list_effective_nodes( intf + ' peer')
+ # no wg config left, delete all wireguard devices, if any
+ if not c:
+ net_devs = os.listdir('/sys/class/net/')
+ for dev in net_devs:
+ if os.path.isdir('/sys/class/net/' + dev):
+ buf = open('/sys/class/net/' + dev + '/uevent', 'r').read()
+ if re.search("DEVTYPE=wireguard", buf, re.I | re.M):
+ wg_intf = re.sub("INTERFACE=", "", re.search(
+ "INTERFACE=.*", buf, re.I | re.M).group(0))
+ sl.syslog(sl.LOG_NOTICE, "removing interface " + wg_intf)
+ subprocess.call(
+ ['ip l d dev ' + wg_intf + ' >/dev/null'], shell=True)
+ return None
+
+ # interface removal
+ if c[ifname]['status'] == 'delete':
+ sl.syslog(sl.LOG_NOTICE, "removing interface " + ifname)
+ intfc.remove()
+ return None
+
+ c_eff = Config()
+ c_eff.set_level('interfaces wireguard')
+
+ # interface state
+ if c[ifname]['state'] == 'disable':
+ sl.syslog(sl.LOG_NOTICE, "disable interface " + ifname)
+ intfc.state = 'down'
+ else:
+ if not intfc.state == 'up':
+ sl.syslog(sl.LOG_NOTICE, "enable interface " + ifname)
+ intfc.state = 'up'
+
+ # IP address
+ if not c_eff.exists_effective(ifname + ' address'):
+ for ip in c[ifname]['addr']:
+ intfc.add_addr(ip)
+ else:
+ addr_eff = c_eff.return_effective_values(ifname + ' address')
+ addr_rem = list(set(addr_eff) - set(c[ifname]['addr']))
+ addr_add = list(set(c[ifname]['addr']) - set(addr_eff))
+
+ if len(addr_rem) != 0:
+ for ip in addr_rem:
+ sl.syslog(
+ sl.LOG_NOTICE, "remove IP address {0} from {1}".format(ip, ifname))
+ intfc.del_addr(ip)
+
+ if len(addr_add) != 0:
+ for ip in addr_add:
+ sl.syslog(
+ sl.LOG_NOTICE, "add IP address {0} to {1}".format(ip, ifname))
+ intfc.add_addr(ip)
+
+ # interface MTU
+ if c[ifname]['mtu'] != 1420:
+ intfc.mtu = int(c[ifname]['mtu'])
+ else:
+ # default is set to 1420 in config_data
+ intfc.mtu = int(c[ifname]['mtu'])
+
+ # ifalias for snmp from description
+ descr_eff = c_eff.return_effective_value(ifname + ' description')
+ if descr_eff != c[ifname]['descr']:
+ intfc.ifalias = str(c[ifname]['descr'])
+
+ # peer deletion
+ peer_eff = c_eff.list_effective_nodes(ifname + ' peer')
peer_cnf = []
+
try:
- for p in c['interfaces'][intf]['peer']:
- peer_cnf.append(p)
+ for p in c[ifname]['peer']:
+ peer_cnf.append(p)
except KeyError:
- pass
+ pass
peer_rem = list(set(peer_eff) - set(peer_cnf))
for p in peer_rem:
- pkey = c_eff.return_effective_value( intf + ' peer ' + p +' pubkey')
- remove_peer(intf, pkey)
+ pkey = c_eff.return_effective_value(ifname + ' peer ' + p + ' pubkey')
+ intfc.remove_peer(pkey)
- ### peer pubkey update
- ### wg identifies peers by its pubky, so we have to remove the peer first
- ### it will recreated it then below with the new key from the cli config
+ # peer key update
for p in peer_eff:
- if p in peer_cnf:
- ekey = c_eff.return_effective_value( intf + ' peer ' + p +' pubkey')
- nkey = c['interfaces'][intf]['peer'][p]['pubkey']
- if nkey != ekey:
- sl.syslog(sl.LOG_NOTICE, "peer " + p + ' changed pubkey from ' + ekey + 'to key ' + nkey + ' on interface ' + intf)
- remove_peer(intf, ekey)
-
- ### new config
- if c['interfaces'][intf]['status'] == 'create':
- if not os.path.exists(pk):
- raise ConfigError("No keys found, generate them by executing: \'run generate wireguard keypair\'")
-
- subprocess.call(['ip l a dev ' + intf + ' type wireguard 2>/dev/null'], shell=True)
- for addr in c['interfaces'][intf]['addr']:
- add_addr(intf, addr)
-
- subprocess.call(['ip l set up dev ' + intf + ' mtu ' + c['interfaces'][intf]['mtu'] + ' &>/dev/null'], shell=True)
- configure_interface(c, intf)
-
- ### config updates
- if c['interfaces'][intf]['status'] == 'exists':
- ### IP address change
- addr_eff = c_eff.return_effective_values(intf + ' address')
- addr_rem = list(set(addr_eff) - set(c['interfaces'][intf]['addr']))
- addr_add = list(set(c['interfaces'][intf]['addr']) - set(addr_eff))
-
- if len(addr_rem) != 0:
- for addr in addr_rem:
- del_addr(intf, addr)
-
- if len(addr_add) != 0:
- for addr in addr_add:
- add_addr(intf, addr)
-
- ## mtu update
- mtu = c['interfaces'][intf]['mtu']
- if mtu != 1420:
- sl.syslog(sl.LOG_NOTICE, "setting mtu to " + mtu + " on " + intf)
- subprocess.call(['ip l set mtu ' + mtu + ' dev ' + intf + ' &>/dev/null'], shell=True)
-
-
- ### persistent-keepalive
- for p in c['interfaces'][intf]['peer']:
- val_eff = ""
- val = ""
-
- try:
- val = c['interfaces'][intf]['peer'][p]['persistent-keepalive']
- except KeyError:
- pass
-
- if c_eff.exists_effective(intf + ' peer ' + p + ' persistent-keepalive'):
- val_eff = c_eff.return_effective_value(intf + ' peer ' + p + ' persistent-keepalive')
-
- ### disable keepalive
- if val_eff and not val:
- c['interfaces'][intf]['peer'][p]['persistent-keepalive'] = 0
-
- ### set new keepalive value
- if not val_eff and val:
- c['interfaces'][intf]['peer'][p]['persistent-keepalive'] = val
-
- ## wg command call
- configure_interface(c, intf)
-
- ### ifalias for snmp from description
- if c['interfaces'][intf]['status'] != 'delete':
- descr_eff = c_eff.return_effective_value(intf + ' description')
- cnf_descr = c['interfaces'][intf]['descr']
- if descr_eff != cnf_descr:
- with open('/sys/class/net/' + str(intf) + '/ifalias', 'w') as fh:
- fh.write(str(cnf_descr))
-
-def configure_interface(c, intf):
- for p in c['interfaces'][intf]['peer']:
- ## config init for wg call
- wg_config = {
- 'interface' : intf,
- 'port' : 0,
- 'private-key' : pk,
- 'pubkey' : '',
- 'psk' : '/dev/null',
- 'allowed-ips' : [],
- 'fwmark' : 0x00,
- 'endpoint' : None,
- 'keepalive' : 0
- }
-
- ## mandatory settings
- wg_config['pubkey'] = c['interfaces'][intf]['peer'][p]['pubkey']
- wg_config['allowed-ips'] = c['interfaces'][intf]['peer'][p]['allowed-ips']
-
- ## optional settings
- # listen-port
- if c['interfaces'][intf]['lport']:
- wg_config['port'] = c['interfaces'][intf]['lport']
-
- ## fwmark
- if c['interfaces'][intf]['fwmark']:
- wg_config['fwmark'] = c['interfaces'][intf]['fwmark']
-
- ## endpoint
- if c['interfaces'][intf]['peer'][p]['endpoint']:
- wg_config['endpoint'] = c['interfaces'][intf]['peer'][p]['endpoint']
-
- ## persistent-keepalive
- if 'persistent-keepalive' in c['interfaces'][intf]['peer'][p]:
- wg_config['keepalive'] = c['interfaces'][intf]['peer'][p]['persistent-keepalive']
-
- ## preshared-key - is only read from a file, it's called via sudo redirection doesn't work either
- if 'psk' in c['interfaces'][intf]['peer'][p]:
- old_umask = os.umask(0o077)
- open(psk_file, 'w').write(str(c['interfaces'][intf]['peer'][p]['psk']))
- os.umask(old_umask)
- wg_config['psk'] = psk_file
-
- ### assemble wg command
- cmd = "sudo wg set " + intf
- cmd += " listen-port " + str(wg_config['port'])
- cmd += " fwmark " + str(wg_config['fwmark'])
- cmd += " private-key " + wg_config['private-key']
- cmd += " peer " + wg_config['pubkey']
- cmd += " preshared-key " + wg_config['psk']
- cmd += " allowed-ips "
- for ap in wg_config['allowed-ips']:
- if ap != wg_config['allowed-ips'][-1]:
- cmd += ap + ","
- else:
- cmd += ap
-
- if wg_config['endpoint']:
- cmd += " endpoint " + wg_config['endpoint']
-
- if wg_config['keepalive'] != 0:
- cmd += " persistent-keepalive " + wg_config['keepalive']
- else:
- cmd += " persistent-keepalive 0"
-
- sl.syslog(sl.LOG_NOTICE, cmd)
- #print (cmd)
- subprocess.call([cmd], shell=True)
- """ remove psk_file """
- if os.path.exists(psk_file):
- os.remove(psk_file)
-
-def add_addr(intf, addr):
- # see https://phabricator.vyos.net/T949
- ret = subprocess.call(['ip a a dev ' + intf + ' ' + addr + ' &>/dev/null'], shell=True)
- sl.syslog(sl.LOG_NOTICE, "ip a a dev " + intf + " " + addr)
-
-def del_addr(intf, addr):
- ret = subprocess.call(['ip a d dev ' + intf + ' ' + addr + ' &>/dev/null'], shell=True)
- sl.syslog(sl.LOG_NOTICE, "ip a d dev " + intf + " " + addr)
-
-def remove_peer(intf, peer_key):
- cmd = 'sudo wg set ' + str(intf) + ' peer ' + peer_key + ' remove &>/dev/null'
- ret = subprocess.call([cmd], shell=True)
- sl.syslog(sl.LOG_NOTICE, "peer " + peer_key + " removed from " + intf)
+ if p in peer_cnf:
+ ekey = c_eff.return_effective_value(
+ ifname + ' peer ' + p + ' pubkey')
+ nkey = c[ifname]['peer'][p]['pubkey']
+ if nkey != ekey:
+ sl.syslog(
+ sl.LOG_NOTICE, "peer {0} pubkey changed from {1} to {2} on interface {3}".format(p, ekey, nkey, ifname))
+ intfc.remove_peer(ekey)
+
+ intfc.config['private-key'] = c[ifname]['pk']
+ for p in c[ifname]['peer']:
+ intfc.config['pubkey'] = str(c[ifname]['peer'][p]['pubkey'])
+ intfc.config['allowed-ips'] = (c[ifname]['peer'][p]['allowed-ips'])
+
+ # listen-port
+ if c[ifname]['lport']:
+ intfc.config['port'] = c[ifname]['lport']
+
+ # fwmark
+ if c[ifname]['fwmark']:
+ intfc.config['fwmark'] = c[ifname]['fwmark']
+
+ # endpoint
+ if c[ifname]['peer'][p]['endpoint']:
+ intfc.config['endpoint'] = c[ifname]['peer'][p]['endpoint']
+
+ # persistent-keepalive
+ if 'persistent-keepalive' in c[ifname]['peer'][p]:
+ intfc.config['keepalive'] = c[ifname][
+ 'peer'][p]['persistent-keepalive']
+
+ # preshared-key - needs to be read from a file
+ if 'psk' in c[ifname]['peer'][p]:
+ psk_file = '/config/auth/wireguard/psk'
+ old_umask = os.umask(0o077)
+ open(psk_file, 'w').write(str(c[ifname]['peer'][p]['psk']))
+ os.umask(old_umask)
+ intfc.config['psk'] = psk_file
+
+ intfc.update()
if __name__ == '__main__':
- try:
- check_kmod()
- c = get_config()
- verify(c)
- apply(c)
- except ConfigError as e:
- print(e)
- sys.exit(1)
+ try:
+ check_kmod()
+ c = get_config()
+ verify(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py
index 8d25e7abd..156bb2edd 100755
--- a/src/conf_mode/ipsec-settings.py
+++ b/src/conf_mode/ipsec-settings.py
@@ -62,7 +62,7 @@ conn {{ra_conn_name}}
left={{outside_addr}}
leftsubnet=%dynamic[/1701]
rightsubnet=%dynamic
- mark=%unique
+ mark_in=%unique
auto=add
ike=aes256-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1024!
dpddelay=15
diff --git a/src/conf_mode/syslog.py b/src/conf_mode/syslog.py
index 7b79c701b..c4f3d2c9c 100755
--- a/src/conf_mode/syslog.py
+++ b/src/conf_mode/syslog.py
@@ -24,16 +24,16 @@ import jinja2
from vyos.config import Config
from vyos import ConfigError
-########### config templates
+# config templates
-#### /etc/rsyslog.d/vyos-rsyslog.conf ###
+# /etc/rsyslog.d/vyos-rsyslog.conf ###
configs = '''
## generated by syslog.py ##
## file based logging
{% if files['global']['marker'] -%}
$ModLoad immark
{% if files['global']['marker-interval'] %}
-$MarkMessagePeriod {{files['global']['marker-interval']}}
+$MarkMessagePeriod {{files['global']['marker-interval']}}
{% endif %}
{% endif -%}
{% if files['global']['preserver_fqdn'] -%}
@@ -80,217 +80,241 @@ logrotate_configs = '''
}
{% endfor %}
'''
-############# config templates end
+# config templates end
+
def get_config():
- c = Config()
- if not c.exists('system syslog'):
- return None
- c.set_level('system syslog')
-
- config_data = {
- 'files' : {},
- 'console' : {},
- 'hosts' : {},
- 'user' : {}
- }
-
- #####
- # /etc/rsyslog.d/vyos-rsyslog.conf
- # 'set system syslog global'
- #####
- config_data['files'].update(
- {
- 'global' : {
- 'log-file' : '/var/log/messages',
- 'max-size' : 262144,
- 'action-on-max-size' : '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog',
- 'selectors' : '*.notice;local7.debug',
- 'max-files' : '5',
- 'preserver_fqdn' : False
- }
- }
- )
-
- if c.exists('global marker'):
- config_data['files']['global']['marker'] = True
- if c.exists('global marker interval'):
- config_data['files']['global']['marker-interval'] = c.return_value('global marker interval')
- if c.exists('global facility'):
- config_data['files']['global']['selectors'] = generate_selectors(c, 'global facility')
- if c.exists('global archive size'):
- config_data['files']['global']['max-size'] = int(c.return_value('global archive size'))* 1024
- if c.exists('global archive file'):
- config_data['files']['global']['max-files'] = c.return_value('global archive file')
- if c.exists('global preserve-fqdn'):
- config_data['files']['global']['preserver_fqdn'] = True
-
- ###
- # set system syslog file
- ###
-
- if c.exists('file'):
- filenames = c.list_nodes('file')
- for filename in filenames:
- config_data['files'].update(
- {
- filename : {
- 'log-file' : '/var/log/user/' + filename,
- 'max-files' : '5',
- 'action-on-max-size' : '/usr/sbin/logrotate /etc/logrotate.d/' + filename,
- 'selectors' : '*.err',
- 'max-size' : 262144
- }
- }
- )
-
- if c.exists('file ' + filename + ' facility'):
- config_data['files'][filename]['selectors'] = generate_selectors(c, 'file ' + filename + ' facility')
- if c.exists('file ' + filename + ' archive size'):
- config_data['files'][filename]['max-size'] = int(c.return_value('file ' + filename + ' archive size'))* 1024
- if c.exists('file ' + filename + ' archive files'):
- config_data['files'][filename]['max-files'] = c.return_value('file ' + filename + ' archive files')
-
- ## set system syslog console
- if c.exists('console'):
- config_data['console'] = {
- '/dev/console' : {
- 'selectors' : '*.err'
- }
+ c = Config()
+ if not c.exists('system syslog'):
+ return None
+ c.set_level('system syslog')
+
+ config_data = {
+ 'files': {},
+ 'console': {},
+ 'hosts': {},
+ 'user': {}
}
-
- for f in c.list_nodes('console facility'):
- if c.exists('console facility ' + f + ' level'):
- config_data['console'] = {
- '/dev/console' : {
- 'selectors' : generate_selectors(c, 'console facility')
- }
- }
-
- ## set system syslog host
- if c.exists('host'):
- proto = 'udp'
- rhosts = c.list_nodes('host')
- for rhost in rhosts:
- for fac in c.list_nodes('host ' + rhost + ' facility'):
- if c.exists('host ' + rhost + ' facility ' + fac + ' protocol'):
- proto = c.return_value('host ' + rhost + ' facility ' + fac + ' protocol')
-
- config_data['hosts'].update(
+
+ #
+ # /etc/rsyslog.d/vyos-rsyslog.conf
+ # 'set system syslog global'
+ #
+ config_data['files'].update(
{
- rhost : {
- 'selectors' : generate_selectors(c, 'host ' + rhost + ' facility'),
- 'proto' : proto
- }
+ 'global': {
+ 'log-file': '/var/log/messages',
+ 'max-size': 262144,
+ 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog',
+ 'selectors': '*.notice;local7.debug',
+ 'max-files': '5',
+ 'preserver_fqdn': False
+ }
}
- )
+ )
- ## set system syslog user
- if c.exists('user'):
- usrs = c.list_nodes('user')
- for usr in usrs:
- config_data['user'].update(
- {
- usr : {
- 'selectors' : generate_selectors(c, 'user ' + usr + ' facility')
- }
+ if c.exists('global marker'):
+ config_data['files']['global']['marker'] = True
+ if c.exists('global marker interval'):
+ config_data['files']['global'][
+ 'marker-interval'] = c.return_value('global marker interval')
+ if c.exists('global facility'):
+ config_data['files']['global'][
+ 'selectors'] = generate_selectors(c, 'global facility')
+ if c.exists('global archive size'):
+ config_data['files']['global']['max-size'] = int(
+ c.return_value('global archive size')) * 1024
+ if c.exists('global archive file'):
+ config_data['files']['global'][
+ 'max-files'] = c.return_value('global archive file')
+ if c.exists('global preserve-fqdn'):
+ config_data['files']['global']['preserver_fqdn'] = True
+
+ #
+ # set system syslog file
+ #
+
+ if c.exists('file'):
+ filenames = c.list_nodes('file')
+ for filename in filenames:
+ config_data['files'].update(
+ {
+ filename: {
+ 'log-file': '/var/log/user/' + filename,
+ 'max-files': '5',
+ 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/' + filename,
+ 'selectors': '*.err',
+ 'max-size': 262144
+ }
+ }
+ )
+
+ if c.exists('file ' + filename + ' facility'):
+ config_data['files'][filename]['selectors'] = generate_selectors(
+ c, 'file ' + filename + ' facility')
+ if c.exists('file ' + filename + ' archive size'):
+ config_data['files'][filename]['max-size'] = int(
+ c.return_value('file ' + filename + ' archive size')) * 1024
+ if c.exists('file ' + filename + ' archive files'):
+ config_data['files'][filename]['max-files'] = c.return_value(
+ 'file ' + filename + ' archive files')
+
+ # set system syslog console
+ if c.exists('console'):
+ config_data['console'] = {
+ '/dev/console': {
+ 'selectors': '*.err'
+ }
}
- )
-
- return config_data
+
+ for f in c.list_nodes('console facility'):
+ if c.exists('console facility ' + f + ' level'):
+ config_data['console'] = {
+ '/dev/console': {
+ 'selectors': generate_selectors(c, 'console facility')
+ }
+ }
+
+ # set system syslog host
+ if c.exists('host'):
+ proto = 'udp'
+ rhosts = c.list_nodes('host')
+ for rhost in rhosts:
+ for fac in c.list_nodes('host ' + rhost + ' facility'):
+ if c.exists('host ' + rhost + ' facility ' + fac + ' protocol'):
+ proto = c.return_value(
+ 'host ' + rhost + ' facility ' + fac + ' protocol')
+
+ config_data['hosts'].update(
+ {
+ rhost: {
+ 'selectors': generate_selectors(c, 'host ' + rhost + ' facility'),
+ 'proto': proto
+ }
+ }
+ )
+
+ # set system syslog user
+ if c.exists('user'):
+ usrs = c.list_nodes('user')
+ for usr in usrs:
+ config_data['user'].update(
+ {
+ usr: {
+ 'selectors': generate_selectors(c, 'user ' + usr + ' facility')
+ }
+ }
+ )
+
+ return config_data
+
def generate_selectors(c, config_node):
-## protocols and security are being mapped here
-## for backward compatibility with old configs
-## security and protocol mappings can be removed later
- if c.is_tag(config_node):
- nodes = c.list_nodes(config_node)
- selectors = ""
- for node in nodes:
- lvl = c.return_value( config_node + ' ' + node + ' level')
- if lvl == None:
- lvl = "err"
- if lvl == 'all':
- lvl = '*'
- if node == 'all' and node != nodes[-1]:
- selectors += "*." + lvl + ";"
- elif node == 'all':
- selectors += "*." + lvl
- elif node != nodes[-1]:
- if node == 'protocols':
- node = 'local7'
- if node == 'security':
- node = 'auth'
- selectors += node + "." + lvl + ";"
- else:
- if node == 'protocols':
- node = 'local7'
- if node == 'security':
- node = 'auth'
- selectors += node + "." + lvl
- return selectors
+# protocols and security are being mapped here
+# for backward compatibility with old configs
+# security and protocol mappings can be removed later
+ if c.is_tag(config_node):
+ nodes = c.list_nodes(config_node)
+ selectors = ""
+ for node in nodes:
+ lvl = c.return_value(config_node + ' ' + node + ' level')
+ if lvl == None:
+ lvl = "err"
+ if lvl == 'all':
+ lvl = '*'
+ if node == 'all' and node != nodes[-1]:
+ selectors += "*." + lvl + ";"
+ elif node == 'all':
+ selectors += "*." + lvl
+ elif node != nodes[-1]:
+ if node == 'protocols':
+ node = 'local7'
+ if node == 'security':
+ node = 'auth'
+ selectors += node + "." + lvl + ";"
+ else:
+ if node == 'protocols':
+ node = 'local7'
+ if node == 'security':
+ node = 'auth'
+ selectors += node + "." + lvl
+ return selectors
+
def generate(c):
- if c == None:
- return None
+ if c == None:
+ return None
- tmpl = jinja2.Template(configs, trim_blocks=True)
- config_text = tmpl.render(c)
- with open('/etc/rsyslog.d/vyos-rsyslog.conf', 'w') as f:
- f.write(config_text)
+ tmpl = jinja2.Template(configs, trim_blocks=True)
+ config_text = tmpl.render(c)
+ with open('/etc/rsyslog.d/vyos-rsyslog.conf', 'w') as f:
+ f.write(config_text)
+
+ # eventually write for each file its own logrotate file, since size is
+ # defined it shouldn't matter
+ tmpl = jinja2.Template(logrotate_configs, trim_blocks=True)
+ config_text = tmpl.render(c)
+ with open('/etc/logrotate.d/vyos-rsyslog', 'w') as f:
+ f.write(config_text)
- ## eventually write for each file its own logrotate file, since size is defined it shouldn't matter
- tmpl = jinja2.Template(logrotate_configs, trim_blocks=True)
- config_text = tmpl.render(c)
- with open('/etc/logrotate.d/vyos-rsyslog', 'w') as f:
- f.write(config_text)
def verify(c):
- if c == None:
- return None
- #
- # /etc/rsyslog.conf is generated somewhere and copied over the original (exists in /opt/vyatta/etc/rsyslog.conf)
- # it interferes with the global logging, to make sure we are using a single base, template is enforced here
- #
- if not os.path.islink('/etc/rsyslog.conf'):
- os.remove('/etc/rsyslog.conf')
- os.symlink('/usr/share/vyos/templates/rsyslog/rsyslog.conf', '/etc/rsyslog.conf')
-
- # /var/log/vyos-rsyslog were the old files, we may want to clean those up, but currently there
- # is a chance that someone still needs it, so I don't automatically remove them
-
- if c == None:
- return None
-
- fac = ['*','auth','authpriv','cron','daemon','kern','lpr','mail','mark','news','protocols','security',\
- 'syslog','user','uucp','local0','local1','local2','local3','local4','local5','local6','local7']
- lvl = ['emerg','alert','crit','err','warning','notice','info','debug','*']
-
- for conf in c:
- if c[conf]:
- for item in c[conf]:
- for s in c[conf][item]['selectors'].split(";"):
- f = re.sub("\..*$","",s)
- if f not in fac:
- print (c[conf])
- raise ConfigError('Invalid facility ' + s + ' set in '+ conf + ' ' + item)
- l = re.sub("^.+\.","",s)
- if l not in lvl:
- raise ConfigError('Invalid logging level ' + s + ' set in '+ conf + ' ' + item)
+ if c == None:
+ return None
+ #
+ # /etc/rsyslog.conf is generated somewhere and copied over the original (exists in /opt/vyatta/etc/rsyslog.conf)
+ # it interferes with the global logging, to make sure we are using a single base, template is enforced here
+ #
+ if not os.path.islink('/etc/rsyslog.conf'):
+ os.remove('/etc/rsyslog.conf')
+ os.symlink(
+ '/usr/share/vyos/templates/rsyslog/rsyslog.conf', '/etc/rsyslog.conf')
+
+ # /var/log/vyos-rsyslog were the old files, we may want to clean those up, but currently there
+ # is a chance that someone still needs it, so I don't automatically remove
+ # them
+
+ if c == None:
+ return None
+
+ fac = [
+ '*', 'auth', 'authpriv', 'cron', 'daemon', 'kern', 'lpr', 'mail', 'mark', 'news', 'protocols', 'security',
+ 'syslog', 'user', 'uucp', 'local0', 'local1', 'local2', 'local3', 'local4', 'local5', 'local6', 'local7']
+ lvl = ['emerg', 'alert', 'crit', 'err',
+ 'warning', 'notice', 'info', 'debug', '*']
+
+ for conf in c:
+ if c[conf]:
+ for item in c[conf]:
+ for s in c[conf][item]['selectors'].split(";"):
+ f = re.sub("\..*$", "", s)
+ if f not in fac:
+ print (c[conf])
+ raise ConfigError(
+ 'Invalid facility ' + s + ' set in ' + conf + ' ' + item)
+ l = re.sub("^.+\.", "", s)
+ if l not in lvl:
+ raise ConfigError(
+ 'Invalid logging level ' + s + ' set in ' + conf + ' ' + item)
+
def apply(c):
- if not os.path.exists('/var/run/rsyslogd.pid'):
- os.system("sudo systemctl start rsyslog >/dev/null")
- else:
- os.system("sudo systemctl restart rsyslog >/dev/null")
+ if not c and os.path.exists('/var/run/rsyslogd.pid'):
+ os.system("sudo systemctl stop syslog.socket")
+ os.system("sudo systemctl stop rsyslog")
+ else:
+ if not os.path.exists('/var/run/rsyslogd.pid'):
+ os.system("sudo systemctl start rsyslog >/dev/null")
+ else:
+ os.system("sudo systemctl restart rsyslog >/dev/null")
if __name__ == '__main__':
- try:
- c = get_config()
- verify(c)
- generate(c)
- apply(c)
- except ConfigError as e:
- print(e)
- sys.exit(1)
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/helpers/vyos-boot-config-loader.py b/src/helpers/vyos-boot-config-loader.py
new file mode 100755
index 000000000..06c95765f
--- /dev/null
+++ b/src/helpers/vyos-boot-config-loader.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+import os
+import sys
+import subprocess
+import traceback
+
+from vyos.configsession import ConfigSession, ConfigSessionError
+from vyos.configtree import ConfigTree
+
+STATUS_FILE = '/tmp/vyos-config-status'
+TRACE_FILE = '/tmp/boot-config-trace'
+
+session = ConfigSession(os.getpid(), 'vyos-boot-config-loader')
+env = session.get_session_env()
+
+default_file_name = env['vyatta_sysconfdir'] + '/config.boot.default'
+
+if len(sys.argv) < 1:
+ print("Must be called with argument.")
+ sys.exit(1)
+else:
+ file_name = sys.argv[1]
+
+def write_config_status(status):
+ with open(STATUS_FILE, 'w') as f:
+ f.write('{0}\n'.format(status))
+
+def trace_to_file(trace_file_name):
+ with open(trace_file_name, 'w') as trace_file:
+ traceback.print_exc(file=trace_file)
+
+def failsafe():
+ try:
+ with open(default_file_name, 'r') as f:
+ config_file = f.read()
+ except Exception as e:
+ print("Catastrophic: no default config file "
+ "'{0}'".format(default_file_name))
+ sys.exit(1)
+
+ config = ConfigTree(config_file)
+ if not config.exists(['system', 'login', 'user', 'vyos',
+ 'authentication', 'encrypted-password']):
+ print("No password entry in default config file;")
+ print("unable to recover password for user 'vyos'.")
+ sys.exit(1)
+ else:
+ passwd = config.return_value(['system', 'login', 'user', 'vyos',
+ 'authentication',
+ 'encrypted-password'])
+
+ cmd = ("useradd -s /bin/bash -G 'users,sudo' -m -N -p '{0}' "
+ "vyos".format(passwd))
+ try:
+ subprocess.check_call(cmd, shell=True)
+ except subprocess.CalledProcessError as e:
+ sys.exit("{0}".format(e))
+
+ with open('/etc/motd', 'a+') as f:
+ f.write('\n\n')
+ f.write('!!!!!\n')
+ f.write('There were errors loading the initial configuration;\n')
+ f.write('please examine the errors in {0} and correct.'
+ '\n'.format(TRACE_FILE))
+ f.write('!!!!!\n\n')
+
+try:
+ with open(file_name, 'r') as f:
+ config_file = f.read()
+except Exception as e:
+ write_config_status(1)
+ failsafe()
+ trace_to_file(TRACE_FILE)
+ sys.exit("{0}".format(e))
+
+try:
+ session.load_config(file_name)
+ session.commit()
+ write_config_status(0)
+except ConfigSessionError as e:
+ write_config_status(1)
+ failsafe()
+ trace_to_file(TRACE_FILE)
+ sys.exit(1)
diff --git a/src/helpers/vyos-bridge-sync.py b/src/helpers/vyos-bridge-sync.py
new file mode 100755
index 000000000..495eb5d40
--- /dev/null
+++ b/src/helpers/vyos-bridge-sync.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# Script is used to synchronize configured bridge interfaces.
+# one can add a non existing interface to a bridge group (e.g. VLAN)
+# but the vlan interface itself does yet not exist. It should be added
+# to the bridge automatically once it's available
+
+import argparse
+import subprocess
+
+from sys import exit
+from time import sleep
+from vyos.config import Config
+
+def subprocess_cmd(command):
+ process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True)
+ proc_stdout = process.communicate()[0].strip()
+ pass
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-i', '--interface', action='store', help='Interface name which should be added to bridge it is configured for', required=True)
+ args, unknownargs = parser.parse_known_args()
+
+ conf = Config()
+ if not conf.list_nodes('interfaces bridge'):
+ # no bridge interfaces exist .. bail out early
+ exit(0)
+ else:
+ for bridge in conf.list_nodes('interfaces bridge'):
+ for member_if in conf.list_nodes('interfaces bridge {} member interface'.format(bridge)):
+ if args.interface == member_if:
+ cmd = 'brctl addif "{}" "{}"'.format(bridge, args.interface)
+ # let interfaces etc. settle - especially required for OpenVPN bridged interfaces
+ sleep(4)
+ subprocess_cmd(cmd)
+
+ exit(0)
diff --git a/src/helpers/vyos-sudo.py b/src/helpers/vyos-sudo.py
index 0101a0c95..3e4c196d9 100755
--- a/src/helpers/vyos-sudo.py
+++ b/src/helpers/vyos-sudo.py
@@ -15,17 +15,10 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
-import getpass
-import grp
import os
import sys
-
-def is_admin() -> bool:
- """Look if current user is in sudo group"""
- current_user = getpass.getuser()
- (_, _, _, admin_group_members) = grp.getgrnam('sudo')
- return current_user in admin_group_members
+from vyos.util import is_admin
if __name__ == '__main__':
diff --git a/src/migration-scripts/dns-forwarding/0-to-1 b/src/migration-scripts/dns-forwarding/0-to-1
new file mode 100755
index 000000000..6e8720eef
--- /dev/null
+++ b/src/migration-scripts/dns-forwarding/0-to-1
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# This migration script will check if there is a allow-from directive configured
+# for the dns forwarding service - if not, the node will be created with the old
+# default values of 0.0.0.0/0 and ::/0
+
+import sys
+from vyos.configtree import ConfigTree
+
+if (len(sys.argv) < 1):
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+
+base = ['service', 'dns', 'forwarding']
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ if not config.exists(base + ['allow-from']):
+ config.set(base + ['allow-from'], value='0.0.0.0/0', replace=False)
+ config.set(base + ['allow-from'], value='::/0', replace=False)
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/dns-forwarding/1-to-2 b/src/migration-scripts/dns-forwarding/1-to-2
new file mode 100755
index 000000000..31ba5573f
--- /dev/null
+++ b/src/migration-scripts/dns-forwarding/1-to-2
@@ -0,0 +1,78 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# This migration script will remove the deprecated 'listen-on' statement
+# from the dns forwarding service and will add the corresponding
+# listen-address nodes instead. This is required as PowerDNS can only listen
+# on interface addresses and not on interface names.
+
+import sys
+
+from ipaddress import ip_interface
+from vyos.configtree import ConfigTree
+from vyos.interfaces import get_type_of_interface
+
+if (len(sys.argv) < 1):
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+
+base = ['service', 'dns', 'forwarding']
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ if config.exists(base + ['listen-on']):
+ listen_intf = config.return_values(base + ['listen-on'])
+ # Delete node with abandoned command
+ config.delete(base + ['listen-on'])
+
+ # retrieve interface addresses for every configured listen-on interface
+ listen_addr = []
+ for intf in listen_intf:
+ # we need to treat vif and vif-s interfaces differently,
+ # both "real interfaces" use dots for vlan identifiers - those
+ # need to be exchanged with vif and vif-s identifiers
+ if intf.count('.') == 1:
+ # this is a regular VLAN interface
+ intf = intf.split('.')[0] + ' vif ' + intf.split('.')[1]
+ elif intf.count('.') == 2:
+ # this is a QinQ VLAN interface
+ intf = intf.split('.')[0] + ' vif-s ' + intf.split('.')[1] + ' vif-c ' + intf.split('.')[2]
+
+ path = ['interfaces', get_type_of_interface(intf), intf, 'address']
+
+ # retrieve corresponding interface addresses in CIDR format
+ # those need to be converted in pure IP addresses without network information
+ for addr in config.return_values(path):
+ listen_addr.append( ip_interface(addr).ip )
+
+ for addr in listen_addr:
+ config.set(base + ['listen-address'], value=addr, replace=False)
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/interfaces/0-to-1 b/src/migration-scripts/interfaces/0-to-1
index 38f2bd8f5..96e18b5d5 100755
--- a/src/migration-scripts/interfaces/0-to-1
+++ b/src/migration-scripts/interfaces/0-to-1
@@ -30,20 +30,22 @@ else:
#
for br in config.list_nodes(base):
# STP: check if enabled
- stp_val = config.return_value(base + [br, 'stp'])
- # STP: delete node with old syntax
- config.delete(base + [br, 'stp'])
- # STP: set new node - if enabled
- if stp_val == "true":
- config.set(base + [br, 'stp'], value=None)
+ if config.exists(base + [br, 'stp']):
+ stp_val = config.return_value(base + [br, 'stp'])
+ # STP: delete node with old syntax
+ config.delete(base + [br, 'stp'])
+ # STP: set new node - if enabled
+ if stp_val == "true":
+ config.set(base + [br, 'stp'], value=None)
# igmp-snooping: check if enabled
- igmp_val = config.return_value(base + [br, 'igmp-snooping', 'querier'])
- # igmp-snooping: delete node with old syntax
- config.delete(base + [br, 'igmp-snooping', 'querier'])
- # igmp-snooping: set new node - if enabled
- if igmp_val == "enable":
- config.set(base + [br, 'igmp', 'querier'], value=None)
+ if config.exists(base + [br, 'igmp-snooping', 'querier']):
+ igmp_val = config.return_value(base + [br, 'igmp-snooping', 'querier'])
+ # igmp-snooping: delete node with old syntax
+ config.delete(base + [br, 'igmp-snooping', 'querier'])
+ # igmp-snooping: set new node - if enabled
+ if igmp_val == "enable":
+ config.set(base + [br, 'igmp', 'querier'], value=None)
#
# move interface based bridge-group to actual bridge (de-nest)
diff --git a/src/migration-scripts/interfaces/1-to-2 b/src/migration-scripts/interfaces/1-to-2
new file mode 100755
index 000000000..050137318
--- /dev/null
+++ b/src/migration-scripts/interfaces/1-to-2
@@ -0,0 +1,63 @@
+#!/usr/bin/env python3
+
+# Change syntax of bond interface
+# - move interface based bond-group to actual bond (de-nest)
+# https://phabricator.vyos.net/T1614
+
+import sys
+from vyos.configtree import ConfigTree
+
+if (len(sys.argv) < 1):
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+base = ['interfaces', 'bonding']
+
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ #
+ # move interface based bond-group to actual bond (de-nest)
+ #
+ for intf in config.list_nodes(['interfaces', 'ethernet']):
+ # check if bond-group exists
+ if config.exists(['interfaces', 'ethernet', intf, 'bond-group']):
+ # get configured bond interface
+ bond = config.return_value(['interfaces', 'ethernet', intf, 'bond-group'])
+ # delete old interface asigned (nested) bond group
+ config.delete(['interfaces', 'ethernet', intf, 'bond-group'])
+ # create new bond member interface
+ config.set(base + [bond, 'member', 'interface'], value=intf, replace=False)
+
+ #
+ # some combinations were allowed in the past from a CLI perspective
+ # but the kernel overwrote them - remove from CLI to not confuse the users.
+ # In addition new consitency checks are in place so users can't repeat the
+ # mistake. One of those nice issues is https://phabricator.vyos.net/T532
+ for bond in config.list_nodes(base):
+ if config.exists(base + [bond, 'arp-monitor', 'interval']) and config.exists(base + [bond, 'mode']):
+ mode = config.return_value(base + [bond, 'mode'])
+ if mode in ['802.3ad', 'transmit-load-balance', 'adaptive-load-balance']:
+ intvl = int(config.return_value(base + [bond, 'arp-monitor', 'interval']))
+ if intvl > 0:
+ # this is not allowed and the linux kernel replies with:
+ # option arp_interval: mode dependency failed, not supported in mode 802.3ad(4)
+ # option arp_interval: mode dependency failed, not supported in mode balance-alb(6)
+ # option arp_interval: mode dependency failed, not supported in mode balance-tlb(5)
+ #
+ # so we simply disable arp_interval by setting it to 0 and miimon will take care about the link
+ config.set(base + [bond, 'arp-monitor', 'interval'], value='0')
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/op_mode/clear_conntrack.py b/src/op_mode/clear_conntrack.py
new file mode 100755
index 000000000..0e52b9086
--- /dev/null
+++ b/src/op_mode/clear_conntrack.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import subprocess
+import sys
+
+from vyos.util import ask_yes_no
+
+if not ask_yes_no("This will clear all currently tracked and expected connections. Continue?"):
+ sys.exit(1)
+else:
+ subprocess.check_call(['/usr/sbin/conntrack -F'], shell=True, stderr=subprocess.DEVNULL)
+ subprocess.check_call(['/usr/sbin/conntrack -F expect'], shell=True, stderr=subprocess.DEVNULL)
diff --git a/src/op_mode/format_disk.py b/src/op_mode/format_disk.py
new file mode 100755
index 000000000..5a3b250ee
--- /dev/null
+++ b/src/op_mode/format_disk.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import os
+import re
+import subprocess
+import sys
+from datetime import datetime
+from time import sleep
+
+from vyos.util import is_admin, ask_yes_no
+
+
+def list_disks():
+ disks = set()
+ with open('/proc/partitions') as partitions_file:
+ for line in partitions_file:
+ fields = line.strip().split()
+ if len(fields) == 4 and fields[3].isalpha() and fields[3] != 'name':
+ disks.add(fields[3])
+ return disks
+
+
+def is_busy(disk: str):
+ """Check if given disk device is busy by re-reading it's partition table"""
+
+ cmd = 'sudo blockdev --rereadpt /dev/{}'.format(disk)
+ status = subprocess.call([cmd], shell=True, stderr=subprocess.DEVNULL)
+ return status != 0
+
+
+def backup_partitions(disk: str):
+ """Save sfdisk partitions output to a backup file"""
+
+ device_path = '/dev/' + disk
+ backup_ts = datetime.now().strftime('%Y-%m-%d-%H:%M')
+ backup_file = '/var/tmp/backup_{}.{}'.format(disk, backup_ts)
+ cmd = 'sudo /sbin/sfdisk -d {} > {}'.format(device_path, backup_file)
+ subprocess.check_call([cmd], shell=True)
+
+
+def list_partitions(disk: str):
+ """List partition numbers of a given disk"""
+
+ parts = set()
+ part_num_expr = re.compile(disk + '([0-9]+)')
+ with open('/proc/partitions') as partitions_file:
+ for line in partitions_file:
+ fields = line.strip().split()
+ if len(fields) == 4 and fields[3] != 'name' and part_num_expr.match(fields[3]):
+ part_idx = part_num_expr.match(fields[3]).group(1)
+ parts.add(int(part_idx))
+ return parts
+
+
+def delete_partition(disk: str, partition_idx: int):
+ cmd = 'sudo /sbin/parted /dev/{} rm {}'.format(disk, partition_idx)
+ subprocess.check_call([cmd], shell=True)
+
+
+def format_disk_like(target: str, proto: str):
+ cmd = 'sudo /sbin/sfdisk -d /dev/{} | sudo /sbin/sfdisk --force /dev/{}'.format(proto, target)
+ subprocess.check_call([cmd], shell=True)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ group = parser.add_argument_group()
+ group.add_argument('-t', '--target', type=str, required=True, help='Target device to format')
+ group.add_argument('-p', '--proto', type=str, required=True, help='Prototype device to use as reference')
+ args = parser.parse_args()
+
+ if not is_admin():
+ print('Must be admin or root to format disk')
+ sys.exit(1)
+
+ target_disk = args.target
+ eligible_target_disks = list_disks()
+
+ proto_disk = args.proto
+ eligible_proto_disks = eligible_target_disks.copy()
+ eligible_proto_disks.remove(target_disk)
+
+ fmt = {
+ 'target_disk': target_disk,
+ 'proto_disk': proto_disk,
+ }
+
+ if proto_disk == target_disk:
+ print('The two disk drives must be different.')
+ sys.exit(1)
+
+ if not os.path.exists('/dev/' + proto_disk):
+ print('Device /dev/{proto_disk} does not exist'.format_map(fmt))
+ sys.exit(1)
+
+ if not os.path.exists('/dev/' + target_disk):
+ print('Device /dev/{target_disk} does not exist'.format_map(fmt))
+ sys.exit(1)
+
+ if target_disk not in eligible_target_disks:
+ print('Device {target_disk} can not be formatted'.format_map(fmt))
+ sys.exit(1)
+
+ if proto_disk not in eligible_proto_disks:
+ print('Device {proto_disk} can not be used as a prototype for {target_disk}'.format_map(fmt))
+ sys.exit(1)
+
+ if is_busy(target_disk):
+ print("Disk device {target_disk} is busy. Can't format it now".format_map(fmt))
+ sys.exit(1)
+
+ print('This will re-format disk {target_disk} so that it has the same disk\n'
+ 'partion sizes and offsets as {proto_disk}. This will not copy\n'
+ 'data from {proto_disk} to {target_disk}. But this will erase all\n'
+ 'data on {target_disk}.\n'.format_map(fmt))
+
+ if not ask_yes_no("Do you wish to proceed?"):
+ print('OK. Disk drive {target_disk} will not be re-formated'.format_map(fmt))
+ sys.exit(0)
+
+ print('OK. Re-formating disk drive {target_disk}...'.format_map(fmt))
+
+ print('Making backup copy of partitions...')
+ backup_partitions(target_disk)
+ sleep(1)
+
+ print('Deleting old partitions...')
+ for p in list_partitions(target_disk):
+ delete_partition(disk=target_disk, partition_idx=p)
+
+ print('Creating new partitions on {target_disk} based on {proto_disk}...'.format_map(fmt))
+ format_disk_like(target=target_disk, proto=proto_disk)
+ print('Done.')
diff --git a/src/op_mode/generate_ssh_server_key.py b/src/op_mode/generate_ssh_server_key.py
new file mode 100755
index 000000000..f205919b8
--- /dev/null
+++ b/src/op_mode/generate_ssh_server_key.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import subprocess
+import sys
+
+from vyos.util import ask_yes_no
+
+if not ask_yes_no('Do you really want to remove the existing SSH host keys?'):
+ sys.exit(0)
+else:
+ subprocess.check_call(['sudo rm -v /etc/ssh/ssh_host_*'], shell=True)
+ subprocess.check_call(['sudo dpkg-reconfigure openssh-server'], shell=True)
+ subprocess.check_call(['sudo systemctl restart ssh'], shell=True)
diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py
index 2f6112fb7..e3644e063 100755
--- a/src/op_mode/powerctrl.py
+++ b/src/op_mode/powerctrl.py
@@ -22,20 +22,7 @@ import re
from datetime import datetime, timedelta, time as type_time, date as type_date
from subprocess import check_output, CalledProcessError, STDOUT
-
-def yn(msg, default=False):
- default_msg = "[Y/n]" if default else "[y/N]"
- while True:
- sys.stdout.write("%s %s " % (msg,default_msg))
- c = input().lower()
- if c == '':
- return default
- elif c in ("y", "ye","yes"):
- return True
- elif c in ("n", "no"):
- return False
- else:
- sys.stdout.write("Please respond with yes/y or no/n\n")
+from vyos.util import ask_yes_no
def valid_time(s):
@@ -80,7 +67,7 @@ def cancel_shutdown():
def execute_shutdown(time, reboot = True, ask=True):
if not ask:
action = "reboot" if reboot else "poweroff"
- if not yn("Are you sure you want to %s this system?" % action):
+ if not ask_yes_no("Are you sure you want to %s this system?" % action):
sys.exit(0)
action = "-r" if reboot else "-P"
diff --git a/src/op_mode/show_openvpn.py b/src/op_mode/show_openvpn.py
new file mode 100755
index 000000000..23a8156ec
--- /dev/null
+++ b/src/op_mode/show_openvpn.py
@@ -0,0 +1,169 @@
+#!/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 jinja2
+import argparse
+
+from vyos.config import Config
+
+outp_tmpl = """
+{% if clients %}
+OpenVPN status on {{ intf }}
+
+Client CN Remote Host Local Host TX bytes RX bytes Connected Since
+--------- ----------- ---------- -------- -------- ---------------
+{%- for c in clients %}
+{{ "%-15s"|format(c.name) }} {{ "%-21s"|format(c.remote) }} {{ "%-21s"|format(local) }} {{ "%-9s"|format(c.tx_bytes) }} {{ "%-9s"|format(c.tx_bytes) }} {{ c.online_since }}
+{%- endfor %}
+{% endif %}
+"""
+
+def bytes2HR(size):
+ # we need to operate in integers
+ size = int(size)
+
+ suff = ['B', 'KB', 'MB', 'GB', 'TB']
+ suffIdx = 0
+
+ while size > 1024:
+ # incr. suffix index
+ suffIdx += 1
+ # divide
+ size = size/1024.0
+
+ output="{0:.1f} {1}".format(size, suff[suffIdx])
+ return output
+
+def get_status(mode, interface):
+ status_file = '/opt/vyatta/etc/openvpn/status/{}.status'.format(interface)
+ # this is an empirical value - I assume we have no more then 999999
+ # current OpenVPN connections
+ routing_table_line = 999999
+
+ data = {
+ 'mode': mode,
+ 'intf': interface,
+ 'local': 'N/A',
+ 'date': '',
+ 'clients': [],
+ }
+
+ with open(status_file, 'r') as f:
+ lines = f.readlines()
+ for line_no, line in enumerate(lines):
+ # remove trailing newline character first
+ line = line.rstrip('\n')
+
+ # check first line header
+ if line_no == 0:
+ if mode == 'server':
+ if not line == 'OpenVPN CLIENT LIST':
+ raise NameError('Expected "OpenVPN CLIENT LIST"')
+ else:
+ if not line == 'OpenVPN STATISTICS':
+ raise NameError('Expected "OpenVPN STATISTICS"')
+
+ continue
+
+ # second line informs us when the status file has been last updated
+ if line_no == 1:
+ data['date'] = line.lstrip('Updated,').rstrip('\n')
+ continue
+
+ if mode == 'server':
+ # followed by line3 giving output information and the actual output data
+ #
+ # Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since
+ # client1,172.18.202.10:55904,2880587,2882653,Fri Aug 23 16:25:48 2019
+ # client3,172.18.204.10:41328,2850832,2869729,Fri Aug 23 16:25:43 2019
+ # client2,172.18.203.10:48987,2856153,2871022,Fri Aug 23 16:25:45 2019
+ if (line_no >= 3) and (line_no < routing_table_line):
+ # indicator that there are no more clients and we will continue with the
+ # routing table
+ if line == 'ROUTING TABLE':
+ routing_table_line = line_no
+ continue
+
+ client = {
+ 'name': line.split(',')[0],
+ 'remote': line.split(',')[1],
+ 'rx_bytes': bytes2HR(line.split(',')[2]),
+ 'tx_bytes': bytes2HR(line.split(',')[3]),
+ 'online_since': line.split(',')[4]
+ }
+
+ data['clients'].append(client)
+ continue
+ else:
+ if line_no == 2:
+ client = {
+ 'name': 'N/A',
+ 'remote': 'N/A',
+ 'rx_bytes': bytes2HR(line.split(',')[1]),
+ 'tx_bytes': '',
+ 'online_since': 'N/A'
+ }
+ continue
+
+ if line_no == 3:
+ client['tx_bytes'] = bytes2HR(line.split(',')[1])
+ data['clients'].append(client)
+ break
+
+ return data
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-m', '--mode', help='OpenVPN operation mode (server, client, site-2-site)', required=True)
+
+ args = parser.parse_args()
+
+ # Do nothing if service is not configured
+ config = Config()
+ if len(config.list_effective_nodes('interfaces openvpn')) == 0:
+ print("No OpenVPN interfaces configured")
+ sys.exit(0)
+
+ # search all OpenVPN interfaces and add those with a matching mode to our
+ # interfaces list
+ interfaces = []
+ for intf in config.list_effective_nodes('interfaces openvpn'):
+ # get interface type (server, client, site-to-site)
+ mode = config.return_effective_value('interfaces openvpn {} mode'.format(intf))
+ if args.mode == mode:
+ interfaces.append(intf)
+
+ for intf in interfaces:
+ data = get_status(args.mode, intf)
+ local_host = config.return_effective_value('interfaces openvpn {} local-host'.format(intf))
+ local_port = config.return_effective_value('interfaces openvpn {} local-port'.format(intf))
+ if local_host and local_port:
+ data['local'] = local_host + ':' + local_port
+
+ if args.mode in ['client', 'site-to-site']:
+ for client in data['clients']:
+ if config.exists_effective('interfaces openvpn {} shared-secret-key-file'.format(intf)):
+ client['name'] = "None (PSK)"
+
+ remote_host = config.return_effective_values('interfaces openvpn {} remote-host'.format(intf))
+ remote_port = config.return_effective_value('interfaces openvpn {} remote-port'.format(intf))
+ if len(remote_host) >= 1:
+ client['remote'] = str(remote_host[0]) + ':' + remote_port
+
+ tmpl = jinja2.Template(outp_tmpl)
+ print(tmpl.render(data))
+
diff --git a/src/op_mode/toggle_help_binding.sh b/src/op_mode/toggle_help_binding.sh
new file mode 100755
index 000000000..a8708f3da
--- /dev/null
+++ b/src/op_mode/toggle_help_binding.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Script for [un-]binding the question mark key for getting help
+if [ "$1" == 'disable' ]; then
+ sed -i "/^bind '\"?\": .* # vyatta key binding$/d" $HOME/.bashrc
+ echo "bind '\"?\": self-insert' # vyatta key binding" >> $HOME/.bashrc
+ bind '"?": self-insert'
+else
+ sed -i "/^bind '\"?\": .* # vyatta key binding$/d" $HOME/.bashrc
+ bind '"?": possible-completions'
+fi
diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py
index 66622c04c..4e93ec6aa 100755
--- a/src/op_mode/wireguard.py
+++ b/src/op_mode/wireguard.py
@@ -19,91 +19,139 @@
import argparse
import os
import sys
+import shutil
import subprocess
import syslog as sl
+
from vyos import ConfigError
dir = r'/config/auth/wireguard'
-pk = dir + '/private.key'
-pub = dir + '/public.key'
psk = dir + '/preshared.key'
+
def check_kmod():
- """ check if kmod is loaded, if not load it """
- if not os.path.exists('/sys/module/wireguard'):
- sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod")
- if os.system('sudo modprobe wireguard') != 0:
- sl.syslog(sl.LOG_ERR, "modprobe wireguard failed")
- raise ConfigError("modprobe wireguard failed")
-
-def generate_keypair():
- """ generates a keypair which is stored in /config/auth/wireguard """
- ret = subprocess.call(['wg genkey | tee ' + pk + '|wg pubkey > ' + pub], shell=True)
- if ret != 0:
- raise ConfigError("wireguard key-pair generation failed")
- else:
- sl.syslog(sl.LOG_NOTICE, "new keypair wireguard key generated in " + dir)
-
-def genkey():
- """ helper function to check, regenerate the keypair """
- old_umask = os.umask(0o077)
- if os.path.exists(pk) and os.path.exists(pub):
- try:
- choice = input("You already have a wireguard key-pair already, do you want to re-generate? [y/n] ")
- if choice == 'y' or choice == 'Y':
- generate_keypair()
- except KeyboardInterrupt:
- sys.exit(0)
- else:
- """ if keypair is bing executed from a running iso """
- if not os.path.exists(dir):
- os.umask(old_umask)
- subprocess.call(['sudo mkdir -p ' + dir], shell=True)
- subprocess.call(['sudo chgrp vyattacfg ' + dir], shell=True)
- subprocess.call(['sudo chmod 770 ' + dir], shell=True)
- generate_keypair()
- os.umask(old_umask)
+ """ check if kmod is loaded, if not load it """
+ if not os.path.exists('/sys/module/wireguard'):
+ sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod")
+ if os.system('sudo modprobe wireguard') != 0:
+ sl.syslog(sl.LOG_ERR, "modprobe wireguard failed")
+ raise ConfigError("modprobe wireguard failed")
-def showkey(key):
- """ helper function to show privkey or pubkey """
- if key == "pub":
- if os.path.exists(pub):
- print ( open(pub).read().strip() )
+
+def generate_keypair(pk, pub):
+ """ generates a keypair which is stored in /config/auth/wireguard """
+ old_umask = os.umask(0o027)
+ ret = subprocess.call(
+ ['wg genkey | tee ' + pk + '|wg pubkey > ' + pub], shell=True)
+ if ret != 0:
+ raise ConfigError("wireguard key-pair generation failed")
else:
- print("no public key found")
+ sl.syslog(
+ sl.LOG_NOTICE, "new keypair wireguard key generated in " + dir)
+ os.umask(old_umask)
- if key == "pk":
- if os.path.exists(pk):
- print ( open(pk).read().strip() )
+
+def genkey(location):
+ """ helper function to check, regenerate the keypair """
+ pk = "{}/private.key".format(location)
+ pub = "{}/public.key".format(location)
+ old_umask = os.umask(0o027)
+ if os.path.exists(pk) and os.path.exists(pub):
+ try:
+ choice = input(
+ "You already have a wireguard key-pair, do you want to re-generate? [y/n] ")
+ if choice == 'y' or choice == 'Y':
+ generate_keypair(pk, pub)
+ except KeyboardInterrupt:
+ sys.exit(0)
else:
- print("no private key found")
+ """ if keypair is bing executed from a running iso """
+ if not os.path.exists(location):
+ subprocess.call(['sudo mkdir -p ' + location], shell=True)
+ subprocess.call(['sudo chgrp vyattacfg ' + location], shell=True)
+ subprocess.call(['sudo chmod 750 ' + location], shell=True)
+ generate_keypair(pk, pub)
+ os.umask(old_umask)
+
+
+def showkey(key):
+ """ helper function to show privkey or pubkey """
+ if os.path.exists(key):
+ print (open(key).read().strip())
+ else:
+ print ("{} not found".format(key))
+
def genpsk():
- """ generates a preshared key and shows it on stdout, it's stroed only in the config """
- subprocess.call(['wg genpsk'], shell=True)
+ """
+ generates a preshared key and shows it on stdout,
+ it's stored only in the cli config
+ """
+
+ subprocess.call(['wg genpsk'], shell=True)
+
+def list_key_dirs():
+ """ lists all dirs under /config/auth/wireguard """
+ if os.path.exists(dir):
+ nks = next(os.walk(dir))[1]
+ for nk in nks:
+ print (nk)
+
+def del_key_dir(kname):
+ """ deletes /config/auth/wireguard/<kname> """
+ kdir = "{0}/{1}".format(dir,kname)
+ if not os.path.isdir(kdir):
+ print ("named keypair {} not found".format(kname))
+ return 1
+ shutil.rmtree(kdir)
+
if __name__ == '__main__':
- check_kmod()
-
- parser = argparse.ArgumentParser(description='wireguard key management')
- parser.add_argument('--genkey', action="store_true", help='generate key-pair')
- parser.add_argument('--showpub', action="store_true", help='shows public key')
- parser.add_argument('--showpriv', action="store_true", help='shows private key')
- parser.add_argument('--genpsk', action="store_true", help='generates preshared-key')
- args = parser.parse_args()
-
- try:
- if args.genkey:
- genkey()
- if args.showpub:
- showkey("pub")
- if args.showpriv:
- showkey("pk")
- if args.genpsk:
- genpsk()
-
- except ConfigError as e:
- print(e)
- sys.exit(1)
+ check_kmod()
+ parser = argparse.ArgumentParser(description='wireguard key management')
+ parser.add_argument(
+ '--genkey', action="store_true", help='generate key-pair')
+ parser.add_argument(
+ '--showpub', action="store_true", help='shows public key')
+ parser.add_argument(
+ '--showpriv', action="store_true", help='shows private key')
+ parser.add_argument(
+ '--genpsk', action="store_true", help='generates preshared-key')
+ parser.add_argument(
+ '--location', action="store", help='key location within {}'.format(dir))
+ parser.add_argument(
+ '--listkdir', action="store_true", help='lists named keydirectories')
+ parser.add_argument(
+ '--delkdir', action="store_true", help='removes named keydirectories')
+ args = parser.parse_args()
+
+ try:
+ if args.genkey:
+ if args.location:
+ genkey("{0}/{1}".format(dir, args.location))
+ else:
+ genkey("{}/default".format(dir))
+ if args.showpub:
+ if args.location:
+ showkey("{0}/{1}/public.key".format(dir, args.location))
+ else:
+ showkey("{}/default/public.key".format(dir))
+ if args.showpriv:
+ if args.location:
+ showkey("{0}/{1}/private.key".format(dir, args.location))
+ else:
+ showkey("{}/default/private.key".format(dir))
+ if args.genpsk:
+ genpsk()
+ if args.listkdir:
+ list_key_dirs()
+ if args.delkdir:
+ if args.location:
+ del_key_dir(args.location)
+ else:
+ del_key_dir("default")
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd
new file mode 100755
index 000000000..8f70eb4e9
--- /dev/null
+++ b/src/services/vyos-hostsd
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+import os
+import sys
+import time
+import json
+import signal
+import traceback
+
+import zmq
+
+import jinja2
+
+debug = True
+
+DATA_DIR = "/var/lib/vyos/"
+STATE_FILE = os.path.join(DATA_DIR, "hostsd.state")
+
+SOCKET_PATH = "ipc:///run/vyos-hostsd.sock"
+
+RESOLV_CONF_FILE = '/etc/resolv.conf'
+HOSTS_FILE = '/etc/hosts'
+
+hosts_tmpl_source = """
+### Autogenerated by VyOS ###
+### Do not edit, your changes will get overwritten ###
+
+# Local host
+127.0.0.1 localhost
+127.0.1.1 {{ host_name }}{% if domain_name %}.{{ domain_name }}{% endif %}
+
+# The following lines are desirable for IPv6 capable hosts
+::1 localhost ip6-localhost ip6-loopback
+fe00::0 ip6-localnet
+ff00::0 ip6-mcastprefix
+ff02::1 ip6-allnodes
+ff02::2 ip6-allrouters
+
+# From DHCP and "system static host-mapping"
+{%- if hosts %}
+{% for h in hosts -%}
+{{hosts[h]['address']}}\t{{h}}\t{% for a in hosts[h]['aliases'] %} {{a}} {% endfor %}
+{% endfor %}
+{%- endif %}
+"""
+
+hosts_tmpl = jinja2.Template(hosts_tmpl_source)
+
+resolv_tmpl_source = """
+### Autogenerated by VyOS ###
+### Do not edit, your changes will get overwritten ###
+
+{% for ns in name_servers -%}
+nameserver {{ns}}
+{% endfor -%}
+
+{%- if domain_name %}
+domain {{ domain_name }}
+{%- endif %}
+
+{%- if search_domains %}
+search {{ search_domains | join(" ") }}
+{%- endif %}
+
+"""
+
+resolv_tmpl = jinja2.Template(resolv_tmpl_source)
+
+# The state data includes a list of name servers
+# and a list of hosts entries.
+#
+# Name servers have the following structure:
+# {"server": {"tag": <str>}}
+#
+# Hosts entries are similar:
+# {"host": {"tag": <str>, "address": <str>, "aliases": <str list>}}
+#
+# The tag is either "static" or "dhcp-<intf>"
+# It's used to distinguish entries created
+# by different scripts so that they can be removed
+# and re-created without having to track what needs
+# to be changed
+STATE = {
+ "name_servers": {},
+ "hosts": {},
+ "host_name": "vyos",
+ "domain_name": "",
+ "search_domains": []}
+
+
+def make_resolv_conf(data):
+ resolv_conf = resolv_tmpl.render(data)
+ print("Writing /etc/resolv.conf")
+ with open(RESOLV_CONF_FILE, 'w') as f:
+ f.write(resolv_conf)
+
+def make_hosts_file(state):
+ print("Writing /etc/hosts")
+ hosts = hosts_tmpl.render(state)
+ with open(HOSTS_FILE, 'w') as f:
+ f.write(hosts)
+
+def add_hosts(data, entries, tag):
+ hosts = data['hosts']
+
+ if not entries:
+ return
+
+ for e in entries:
+ host = e['host']
+ hosts[host] = {}
+ hosts[host]['tag'] = tag
+ hosts[host]['address'] = e['address']
+ hosts[host]['aliases'] = e['aliases']
+
+def delete_hosts(data, tag):
+ hosts = data['hosts']
+ keys_for_deletion = []
+
+ # You can't delete items from a dict while iterating over it,
+ # so we build a list of doomed items first
+ for h in hosts:
+ if hosts[h]['tag'] == tag:
+ keys_for_deletion.append(h)
+
+ for k in keys_for_deletion:
+ del hosts[k]
+
+def add_name_servers(data, entries, tag):
+ name_servers = data['name_servers']
+
+ if not entries:
+ return
+
+ for e in entries:
+ name_servers[e] = {}
+ name_servers[e]['tag'] = tag
+
+def delete_name_servers(data, tag):
+ name_servers = data['name_servers']
+ keys_for_deletion = []
+
+ for ns in name_servers:
+ if name_servers[ns]['tag'] == tag:
+ keys_for_deletion.append(ns)
+
+ for k in keys_for_deletion:
+ del name_servers[k]
+
+def set_host_name(state, data):
+ if data['host_name']:
+ state['host_name'] = data['host_name']
+ if data['domain_name']:
+ state['domain_name'] = data['domain_name']
+ if data['search_domains']:
+ state['search_domains'] = data['search_domains']
+
+def get_name_servers(state, tag):
+ ns = []
+ data = state['name_servers']
+ for n in data:
+ if data[n]['tag'] == tag:
+ ns.append(n)
+ return ns
+
+def get_option(msg, key):
+ if key in msg:
+ return msg[key]
+ else:
+ raise ValueError("Missing required option \"{0}\"".format(key))
+
+def handle_message(msg_json):
+ msg = json.loads(msg_json)
+
+ op = get_option(msg, 'op')
+ _type = get_option(msg, 'type')
+
+ if op == 'delete':
+ tag = get_option(msg, 'tag')
+
+ if _type == 'name_servers':
+ delete_name_servers(STATE, tag)
+ elif _type == 'hosts':
+ delete_hosts(STATE, tag)
+ else:
+ raise ValueError("Unknown message type {0}".format(_type))
+ elif op == 'add':
+ tag = get_option(msg, 'tag')
+ entries = get_option(msg, 'data')
+ if _type == 'name_servers':
+ add_name_servers(STATE, entries, tag)
+ elif _type == 'hosts':
+ add_hosts(STATE, entries, tag)
+ else:
+ raise ValueError("Unknown message type {0}".format(_type))
+ elif op == 'set':
+ # Host name/domain name/search domain are set without a tag,
+ # there can be only one anyway
+ data = get_option(msg, 'data')
+ if _type == 'host_name':
+ set_host_name(STATE, data)
+ else:
+ raise ValueError("Unknown message type {0}".format(_type))
+ elif op == 'get':
+ tag = get_option(msg, 'tag')
+ if _type == 'name_servers':
+ result = get_name_servers(STATE, tag)
+ else:
+ raise ValueError("Unimplemented")
+ return result
+ else:
+ raise ValueError("Unknown operation {0}".format(op))
+
+ make_resolv_conf(STATE)
+ make_hosts_file(STATE)
+
+ print("Saving state to {0}".format(STATE_FILE))
+ with open(STATE_FILE, 'w') as f:
+ json.dump(STATE, f)
+
+def exit_handler(sig, frame):
+ """ Clean up the state when shutdown correctly """
+ print("Cleaning up state")
+ os.unlink(STATE_FILE)
+ sys.exit(0)
+
+
+if __name__ == '__main__':
+ signal.signal(signal.SIGTERM, exit_handler)
+
+ # Create a directory for state checkpoints
+ os.makedirs(DATA_DIR, exist_ok=True)
+ if os.path.exists(STATE_FILE):
+ with open(STATE_FILE, 'r') as f:
+ try:
+ data = json.load(f)
+ STATE = data
+ except:
+ print(traceback.format_exc())
+ print("Failed to load the state file, using default")
+
+ context = zmq.Context()
+ socket = context.socket(zmq.REP)
+ socket.bind(SOCKET_PATH)
+
+ while True:
+ # Wait for next request from client
+ message = socket.recv().decode()
+ print("Received a configuration change request")
+ if debug:
+ print("Request data: {0}".format(message))
+
+ resp = {}
+
+ try:
+ result = handle_message(message)
+ resp['data'] = result
+ except ValueError as e:
+ resp['error'] = str(e)
+ except:
+ print(traceback.format_exc())
+ resp['error'] = "Internal error"
+
+ if debug:
+ print("Sent response: {0}".format(resp))
+
+ # Send reply back to client
+ socket.send(json.dumps(resp).encode())
diff --git a/src/system/unpriv-ip b/src/system/unpriv-ip
new file mode 100755
index 000000000..1ea0d626a
--- /dev/null
+++ b/src/system/unpriv-ip
@@ -0,0 +1,2 @@
+#!/bin/sh
+sudo /sbin/ip $*
diff --git a/src/systemd/vyos-hostsd.service b/src/systemd/vyos-hostsd.service
new file mode 100644
index 000000000..731e570c9
--- /dev/null
+++ b/src/systemd/vyos-hostsd.service
@@ -0,0 +1,31 @@
+[Unit]
+Description=VyOS DNS configuration keeper
+
+# Without this option, lots of default dependencies are added,
+# among them network.target, which creates a dependency cycle
+DefaultDependencies=no
+
+# Seemingly sensible way to say "as early as the system is ready"
+# All vyos-hostsd needs is read/write mounted root
+After=systemd-remount-fs.service
+
+[Service]
+ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-hostsd
+Type=idle
+KillMode=process
+
+SyslogIdentifier=vyos-hostsd
+SyslogFacility=daemon
+
+Restart=on-failure
+
+# Does't work in Jessie but leave it here
+User=root
+Group=vyattacfg
+
+[Install]
+
+# Note: After= doesn't actually create a dependency,
+# it just sets order for the case when both services are to start,
+# and without RequiredBy it *does not* set vyos-hostsd to start.
+RequiredBy=cloud-init-local.service vyos-router.service
diff --git a/src/utils/vyos-hostsd-client b/src/utils/vyos-hostsd-client
new file mode 100755
index 000000000..d3105c9cf
--- /dev/null
+++ b/src/utils/vyos-hostsd-client
@@ -0,0 +1,82 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+import sys
+import argparse
+
+import vyos.hostsd_client
+
+parser = argparse.ArgumentParser()
+group = parser.add_mutually_exclusive_group()
+group.add_argument('--add-hosts', action="store_true")
+group.add_argument('--delete-hosts', action="store_true")
+group.add_argument('--add-name-servers', action="store_true")
+group.add_argument('--delete-name-servers', action="store_true")
+group.add_argument('--set-host-name', action="store_true")
+
+parser.add_argument('--host', type=str, action="append")
+parser.add_argument('--name-server', type=str, action="append")
+parser.add_argument('--host-name', type=str)
+parser.add_argument('--domain-name', type=str)
+parser.add_argument('--search-domain', type=str, action="append")
+
+parser.add_argument('--tag', type=str)
+
+args = parser.parse_args()
+
+try:
+ client = vyos.hostsd_client.Client()
+
+ if args.add_hosts:
+ if not args.tag:
+ raise ValueError("Tag is required for this operation")
+ data = []
+ for h in args.host:
+ entry = {}
+ params = h.split(",")
+ if len(params) < 2:
+ raise ValueError("Malformed host entry")
+ entry['host'] = params[0]
+ entry['address'] = params[1]
+ entry['aliases'] = params[2:]
+ data.append(entry)
+ client.add_hosts(args.tag, data)
+ elif args.delete_hosts:
+ if not args.tag:
+ raise ValueError("Tag is required for this operation")
+ client.delete_hosts(args.tag)
+ elif args.add_name_servers:
+ if not args.tag:
+ raise ValueError("Tag is required for this operation")
+ client.add_name_servers(args.tag, args.name_server)
+ elif args.delete_name_servers:
+ if not args.tag:
+ raise ValueError("Tag is required for this operation")
+ client.delete_name_servers(args.tag)
+ elif args.set_host_name:
+ client.set_host_name(args.host_name, args.domain_name, args.search_domain)
+ else:
+ raise ValueError("Operation required")
+
+except ValueError as e:
+ print("Incorrect options: {0}".format(e))
+ sys.exit(1)
+except vyos.hostsd_client.VyOSHostsdError as e:
+ print("Server returned an error: {0}".format(e))
+ sys.exit(1)
+