From 6e7de46eebb4f5f6a4d6438a45a621f75ab63a50 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Thu, 26 Dec 2019 17:34:58 +0100
Subject: time-zone: T1906: rename python script to system-timezone.py

---
 src/conf_mode/system-timezone.py | 55 ++++++++++++++++++++++++++++++++++++++++
 src/conf_mode/timezone.py        | 55 ----------------------------------------
 2 files changed, 55 insertions(+), 55 deletions(-)
 create mode 100755 src/conf_mode/system-timezone.py
 delete mode 100755 src/conf_mode/timezone.py

(limited to 'src')

diff --git a/src/conf_mode/system-timezone.py b/src/conf_mode/system-timezone.py
new file mode 100755
index 000000000..d715bd27e
--- /dev/null
+++ b/src/conf_mode/system-timezone.py
@@ -0,0 +1,55 @@
+#!/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 os
+
+from copy import deepcopy
+from vyos.config import Config
+from vyos import ConfigError
+
+default_config_data = {
+    'name': 'UTC'
+}
+
+def get_config():
+    tz = deepcopy(default_config_data)
+    conf = Config()
+    if conf.exists('system time-zone'):
+        tz['name'] = conf.return_value('system time-zone')
+
+    return tz
+
+def verify(tz):
+    pass
+
+def generate(tz):
+    pass
+
+def apply(tz):
+    cmd = '/usr/bin/timedatectl set-timezone {}'.format(tz['name'])
+    os.system(cmd)
+    pass
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        sys.exit(1)
diff --git a/src/conf_mode/timezone.py b/src/conf_mode/timezone.py
deleted file mode 100755
index d715bd27e..000000000
--- a/src/conf_mode/timezone.py
+++ /dev/null
@@ -1,55 +0,0 @@
-#!/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 os
-
-from copy import deepcopy
-from vyos.config import Config
-from vyos import ConfigError
-
-default_config_data = {
-    'name': 'UTC'
-}
-
-def get_config():
-    tz = deepcopy(default_config_data)
-    conf = Config()
-    if conf.exists('system time-zone'):
-        tz['name'] = conf.return_value('system time-zone')
-
-    return tz
-
-def verify(tz):
-    pass
-
-def generate(tz):
-    pass
-
-def apply(tz):
-    cmd = '/usr/bin/timedatectl set-timezone {}'.format(tz['name'])
-    os.system(cmd)
-    pass
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        sys.exit(1)
-- 
cgit v1.2.3


From 63e787e03129cd799a7fdc6078ff885cdb9b13bf Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Thu, 26 Dec 2019 17:12:28 +0100
Subject: ip: migrate 'system ip' subsystem to XML/Python

---
 interface-definitions/system-ip.xml.in | 58 ++++++++++++++++++++++++
 src/conf_mode/system-ip.py             | 81 ++++++++++++++++++++++++++++++++++
 2 files changed, 139 insertions(+)
 create mode 100644 interface-definitions/system-ip.xml.in
 create mode 100755 src/conf_mode/system-ip.py

(limited to 'src')

diff --git a/interface-definitions/system-ip.xml.in b/interface-definitions/system-ip.xml.in
new file mode 100644
index 000000000..14b3b8a07
--- /dev/null
+++ b/interface-definitions/system-ip.xml.in
@@ -0,0 +1,58 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+  <node name="system">
+    <children>
+      <node name="ip" owner="${vyos_conf_scripts_dir}/system-ip.py">
+        <properties>
+          <help>IPv4 Settings</help>
+          <priority>400</priority>
+        </properties>
+        <children>
+          <node name="arp">
+            <properties>
+              <help>Parameters for ARP cache</help>
+            </properties>
+            <children>
+              <leafNode name="table-size">
+                <properties>
+                  <help>Maximum number of entries to keep in the ARP cache</help>
+                  <completionHelp>
+                    <list>1024 2048 4096 8192 16384 32768</list>
+                  </completionHelp>
+                  <constraint>
+                    <regex>(1024|2048|4096|8192|16384|32768)</regex>
+                  </constraint>
+                </properties>
+              </leafNode>
+            </children>
+          </node>
+          <leafNode name="disable-forwarding">
+            <properties>
+              <help>Disable IPv4 forwarding on all interfaces</help>
+              <valueless/>
+            </properties>
+          </leafNode>
+          <node name="multipath">
+            <properties>
+              <help>IPv4 multipath settings</help>
+            </properties>
+            <children>
+              <leafNode name="ignore-unreachable-nexthops">
+                <properties>
+                  <help>Ignore next hops that are not in the ARP table</help>
+                  <valueless/>
+                </properties>
+              </leafNode>
+              <leafNode name="layer4-hashing">
+                <properties>
+                  <help>Use layer 4 information for ECMP hashing</help>
+                  <valueless/>
+                </properties>
+              </leafNode>
+            </children>
+          </node>
+        </children>
+      </node>
+    </children>
+  </node>
+</interfaceDefinition>
diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py
new file mode 100755
index 000000000..335507411
--- /dev/null
+++ b/src/conf_mode/system-ip.py
@@ -0,0 +1,81 @@
+#!/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 sys import exit
+from copy import deepcopy
+from vyos.config import Config
+from vyos import ConfigError
+
+default_config_data = {
+    'arp_table': 8192,
+    'ipv4_forward': '1',
+    'mp_unreach_nexthop': '0',
+    'mp_layer4_hashing': '0'
+}
+
+def sysctl(name, value):
+    os.system('sysctl -wq {}={}'.format(name, value))
+
+def get_config():
+    ip_opt = deepcopy(default_config_data)
+    conf = Config()
+    conf.set_level('system ip')
+    if conf.exists(''):
+        if conf.exists('arp table-size'):
+            ip_opt['arp_table'] = int(conf.return_value('arp table-size'))
+
+        if conf.exists('disable-forwarding'):
+            ip_opt['ipv4_forward'] = '0'
+
+        if conf.exists('multipath ignore-unreachable-nexthops'):
+            ip_opt['mp_unreach_nexthop'] = '1'
+
+        if conf.exists('multipath layer4-hashing'):
+            ip_opt['mp_layer4_hashing'] = '1'
+
+    return ip_opt
+
+def verify(ip_opt):
+    pass
+
+def generate(ip_opt):
+    pass
+
+def apply(ip_opt):
+    # apply ARP threshold values
+    sysctl('net.ipv4.neigh.default.gc_thresh3', ip_opt['arp_table'])
+    sysctl('net.ipv4.neigh.default.gc_thresh2', ip_opt['arp_table'] // 2)
+    sysctl('net.ipv4.neigh.default.gc_thresh1', ip_opt['arp_table'] // 8)
+
+    # enable/disable IPv4 forwarding
+    with open('/proc/sys/net/ipv4/conf/all/forwarding', 'w') as f:
+        f.write(ip_opt['ipv4_forward'])
+
+    # configure multipath
+    sysctl('net.ipv4.fib_multipath_use_neigh', ip_opt['mp_unreach_nexthop'])
+    sysctl('net.ipv4.fib_multipath_hash_policy', ip_opt['mp_layer4_hashing'])
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
-- 
cgit v1.2.3


From b8d41c226d70b3cc5a45f6b8dceca323c8e18bb7 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sat, 28 Dec 2019 14:14:02 +0100
Subject: ipv6: migrate 'system ipv6' subsystem to XML/Python

---
 interface-definitions/system-ipv6.xml.in |  70 +++++++++++++++++
 src/conf_mode/system-ipv6.py             | 124 +++++++++++++++++++++++++++++++
 2 files changed, 194 insertions(+)
 create mode 100644 interface-definitions/system-ipv6.xml.in
 create mode 100755 src/conf_mode/system-ipv6.py

(limited to 'src')

diff --git a/interface-definitions/system-ipv6.xml.in b/interface-definitions/system-ipv6.xml.in
new file mode 100644
index 000000000..623418100
--- /dev/null
+++ b/interface-definitions/system-ipv6.xml.in
@@ -0,0 +1,70 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+  <node name="system">
+    <children>
+      <node name="ipv6" owner="${vyos_conf_scripts_dir}/system-ipv6.py">
+        <properties>
+          <help>IPv6 Settings</help>
+          <priority>290</priority>
+        </properties>
+        <children>
+          <leafNode name="blacklist">
+            <properties>
+              <help>Prevent IPv6 Kernel Module from being loaded</help>
+              <valueless/>
+            </properties>
+          </leafNode>
+          <leafNode name="disable-forwarding">
+            <properties>
+              <help>Disable IPv6 forwarding on all interfaces</help>
+              <valueless/>
+            </properties>
+          </leafNode>
+          <leafNode name="disable">
+            <properties>
+              <help>Disable assignment of IPv6 addresses on all interfaces</help>
+              <valueless/>
+            </properties>
+          </leafNode>
+          <node name="multipath">
+            <properties>
+              <help>IPv4 multipath settings</help>
+            </properties>
+            <children>
+              <leafNode name="layer4-hashing">
+                <properties>
+                  <help>Use layer 4 information for ECMP hashing</help>
+                  <valueless/>
+                </properties>
+              </leafNode>
+            </children>
+          </node>
+          <node name="neighbor">
+            <properties>
+              <help>Parameters for Neighbor cache</help>
+            </properties>
+            <children>
+              <leafNode name="table-size">
+                <properties>
+                  <help>Maximum number of entries to keep in the Neighbor cache</help>
+                  <completionHelp>
+                    <list>1024 2048 4096 8192 16384 32768</list>
+                  </completionHelp>
+                  <constraint>
+                    <regex>(1024|2048|4096|8192|16384|32768)</regex>
+                  </constraint>
+                </properties>
+              </leafNode>
+            </children>
+          </node>
+          <leafNode name="strict-dad">
+            <properties>
+              <help>Disable IPv6 operation on interface when DAD fails on LL addr</help>
+              <valueless/>
+            </properties>
+          </leafNode>
+        </children>
+      </node>
+    </children>
+  </node>
+</interfaceDefinition>
diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py
new file mode 100755
index 000000000..a39d00f9f
--- /dev/null
+++ b/src/conf_mode/system-ipv6.py
@@ -0,0 +1,124 @@
+#!/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
+
+from sys import exit
+from copy import deepcopy
+from vyos.config import Config
+from vyos import ConfigError
+
+ipv6_blacklist_file = '/etc/modprobe.d/vyos_blacklist_ipv6.conf'
+ipv6_disable_file = '/etc/modprobe.d/vyos_disable_ipv6.conf'
+
+default_config_data = {
+    'blacklist': False,
+    'reboot_message': False,
+    'ipv6_forward': '1',
+    'disable_addr_assignment': False,
+    'mp_layer4_hashing': '0',
+    'neighbor_cache': 8192,
+    'strict_dad': '1'
+
+}
+
+def sysctl(name, value):
+    os.system('sysctl -wq {}={}'.format(name, value))
+
+def get_config():
+    ip_opt = deepcopy(default_config_data)
+    conf = Config()
+    conf.set_level('system ipv6')
+    if conf.exists(''):
+        ip_opt['blacklist'] = conf.exists('blacklist')
+        if conf.exists_effective('blacklist') != conf.exists('blacklist'):
+            ip_opt['reboot_message'] = True
+
+        ip_opt['disable_addr_assignment'] = conf.exists('disable')
+        if conf.exists_effective('disable') != conf.exists('disable'):
+            ip_opt['reboot_message'] = True
+
+        if conf.exists('disable-forwarding'):
+            ip_opt['ipv6_forward'] = '0'
+
+        if conf.exists('multipath layer4-hashing'):
+            ip_opt['mp_layer4_hashing'] = '1'
+
+        if conf.exists('neighbor table-size'):
+            ip_opt['neighbor_cache'] = int(conf.return_value('neighbor table-size'))
+
+        if conf.exists('strict-dad'):
+            ip_opt['strict_dad'] = 2
+
+    return ip_opt
+
+def verify(ip_opt):
+    pass
+
+def generate(ip_opt):
+    pass
+
+def apply(ip_opt):
+    # disable IPv6 kernel module
+    if ip_opt['blacklist']:
+        with open(ipv6_blacklist_file, 'w') as f:
+            f.write('blacklist ipv6')
+    else:
+        if os.path.exists(ipv6_blacklist_file):
+            os.unlink(ipv6_blacklist_file)
+
+    # disable IPv6 address assignment
+    if ip_opt['disable_addr_assignment']:
+        with open(ipv6_disable_file, 'w') as f:
+            f.write('options ipv6 disable_ipv6=1')
+    else:
+        if os.path.exists(ipv6_disable_file):
+            os.unlink(ipv6_disable_file)
+
+    if ip_opt['reboot_message']:
+        print('Changing IPv6 blacklist/disable parameter will only take affect\n' \
+              'when the system is rebooted.')
+
+    # configure multipath
+    sysctl('net.ipv6.fib_multipath_hash_policy', ip_opt['mp_layer4_hashing'])
+
+    # apply neighbor table threshold values
+    sysctl('net.ipv6.neigh.default.gc_thresh3', ip_opt['neighbor_cache'])
+    sysctl('net.ipv6.neigh.default.gc_thresh2', ip_opt['neighbor_cache'] // 2)
+    sysctl('net.ipv6.neigh.default.gc_thresh1', ip_opt['neighbor_cache'] // 8)
+
+    # enable/disable IPv6 forwarding
+    with open('/proc/sys/net/ipv6/conf/all/forwarding', 'w') as f:
+        f.write(ip_opt['ipv6_forward'])
+
+    # configure IPv6 strict-dad
+    for root, dirs, files in os.walk('/proc/sys/net/ipv6/conf'):
+        for name in files:
+            if name == "accept_dad":
+                print(os.path.join(root, name))
+                with open(os.path.join(root, name), 'w') as f:
+                    f.write(str(ip_opt['strict_dad']))
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
-- 
cgit v1.2.3


From b0ca6b1fc4c365463837553e925b472dc2d7e7f6 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sat, 28 Dec 2019 14:19:40 +0100
Subject: ipv6: T1912: remove debug print()

---
 src/conf_mode/system-ipv6.py | 1 -
 1 file changed, 1 deletion(-)

(limited to 'src')

diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py
index a39d00f9f..cfbbf80ee 100755
--- a/src/conf_mode/system-ipv6.py
+++ b/src/conf_mode/system-ipv6.py
@@ -109,7 +109,6 @@ def apply(ip_opt):
     for root, dirs, files in os.walk('/proc/sys/net/ipv6/conf'):
         for name in files:
             if name == "accept_dad":
-                print(os.path.join(root, name))
                 with open(os.path.join(root, name), 'w') as f:
                     f.write(str(ip_opt['strict_dad']))
 
-- 
cgit v1.2.3


From dcc8f87b0e2a442668b7851fb48b665c0394e86a Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Sat, 28 Dec 2019 20:00:17 +0100
Subject: ipv6: T1915: remove 'system ipv6 blacklist'

---
 interface-definitions/system-ipv6.xml.in |  6 -----
 src/conf_mode/system-ipv6.py             | 16 +------------
 src/migration-scripts/system/13-to-14    | 40 ++++++++++++++++++++++++++++++++
 3 files changed, 41 insertions(+), 21 deletions(-)
 create mode 100755 src/migration-scripts/system/13-to-14

(limited to 'src')

diff --git a/interface-definitions/system-ipv6.xml.in b/interface-definitions/system-ipv6.xml.in
index 623418100..47fbeb4e1 100644
--- a/interface-definitions/system-ipv6.xml.in
+++ b/interface-definitions/system-ipv6.xml.in
@@ -8,12 +8,6 @@
           <priority>290</priority>
         </properties>
         <children>
-          <leafNode name="blacklist">
-            <properties>
-              <help>Prevent IPv6 Kernel Module from being loaded</help>
-              <valueless/>
-            </properties>
-          </leafNode>
           <leafNode name="disable-forwarding">
             <properties>
               <help>Disable IPv6 forwarding on all interfaces</help>
diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py
index cfbbf80ee..bd28ec357 100755
--- a/src/conf_mode/system-ipv6.py
+++ b/src/conf_mode/system-ipv6.py
@@ -22,11 +22,9 @@ from copy import deepcopy
 from vyos.config import Config
 from vyos import ConfigError
 
-ipv6_blacklist_file = '/etc/modprobe.d/vyos_blacklist_ipv6.conf'
 ipv6_disable_file = '/etc/modprobe.d/vyos_disable_ipv6.conf'
 
 default_config_data = {
-    'blacklist': False,
     'reboot_message': False,
     'ipv6_forward': '1',
     'disable_addr_assignment': False,
@@ -44,10 +42,6 @@ def get_config():
     conf = Config()
     conf.set_level('system ipv6')
     if conf.exists(''):
-        ip_opt['blacklist'] = conf.exists('blacklist')
-        if conf.exists_effective('blacklist') != conf.exists('blacklist'):
-            ip_opt['reboot_message'] = True
-
         ip_opt['disable_addr_assignment'] = conf.exists('disable')
         if conf.exists_effective('disable') != conf.exists('disable'):
             ip_opt['reboot_message'] = True
@@ -73,14 +67,6 @@ def generate(ip_opt):
     pass
 
 def apply(ip_opt):
-    # disable IPv6 kernel module
-    if ip_opt['blacklist']:
-        with open(ipv6_blacklist_file, 'w') as f:
-            f.write('blacklist ipv6')
-    else:
-        if os.path.exists(ipv6_blacklist_file):
-            os.unlink(ipv6_blacklist_file)
-
     # disable IPv6 address assignment
     if ip_opt['disable_addr_assignment']:
         with open(ipv6_disable_file, 'w') as f:
@@ -90,7 +76,7 @@ def apply(ip_opt):
             os.unlink(ipv6_disable_file)
 
     if ip_opt['reboot_message']:
-        print('Changing IPv6 blacklist/disable parameter will only take affect\n' \
+        print('Changing IPv6 disable parameter will only take affect\n' \
               'when the system is rebooted.')
 
     # configure multipath
diff --git a/src/migration-scripts/system/13-to-14 b/src/migration-scripts/system/13-to-14
new file mode 100755
index 000000000..c055dad1f
--- /dev/null
+++ b/src/migration-scripts/system/13-to-14
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+#
+# Delete 'system ipv6 blacklist' option as the IPv6 module can no longer be
+# blacklisted as it is required by e.g. WireGuard and thus will always be
+# loaded.
+
+import os
+import sys
+
+ipv6_blacklist_file = '/etc/modprobe.d/vyatta_blacklist_ipv6.conf'
+
+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)
+ip_base = ['system', 'ipv6']
+if not config.exists(ip_base):
+    # Nothing to do
+    sys.exit(0)
+else:
+    # delete 'system ipv6 blacklist' node
+    if config.exists(ip_base + ['blacklist']):
+        config.delete(ip_base + ['blacklist'])
+        if os.path.isfile(ipv6_blacklist_file):
+            os.unlink(ipv6_blacklist_file)
+
+    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)
-- 
cgit v1.2.3


From 22148286648c8e51fa0fa13649907b6112223dad Mon Sep 17 00:00:00 2001
From: zsdc <taras@vyos.io>
Date: Fri, 27 Dec 2019 23:17:40 +0200
Subject: FRRouting: T1514: Added commands to restart FRRouting daemon

It can be restarted the whole FRRouting (all running) daemons or only selected ones.
The configuration is saving during the restart process, so after it, the active config should be the same as before.
There are no checks for safety, so responsibility for the results of running command is fully on the operator.
---
 op-mode-definitions/restart-frr.xml |  63 ++++++++++++++++++++++
 src/op_mode/restart_frr.py          | 103 ++++++++++++++++++++++++++++++++++++
 2 files changed, 166 insertions(+)
 create mode 100644 op-mode-definitions/restart-frr.xml
 create mode 100755 src/op_mode/restart_frr.py

(limited to 'src')

diff --git a/op-mode-definitions/restart-frr.xml b/op-mode-definitions/restart-frr.xml
new file mode 100644
index 000000000..4b649febd
--- /dev/null
+++ b/op-mode-definitions/restart-frr.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+  <node name="restart">
+    <children>
+      <node name="frr">
+        <properties>
+          <help>Restart FRRouting daemons</help>
+        </properties>
+        <command>${vyos_op_scripts_dir}/restart_frr.py --action restart</command>
+        <children>
+          <leafNode name="bfdd">
+            <properties>
+              <help>Restart Bidirectional Forwarding Detection daemon</help>
+            </properties>
+            <command>${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon bfdd</command>
+          </leafNode>
+          <leafNode name="bgpd">
+            <properties>
+              <help>Restart Border Gateway Protocol daemon</help>
+            </properties>
+            <command>${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon bgpd</command>
+          </leafNode>
+          <leafNode name="ospfd">
+            <properties>
+              <help>Restart OSPFv2 daemon</help>
+            </properties>
+            <command>${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon ospfd</command>
+          </leafNode>
+          <leafNode name="ospf6d">
+            <properties>
+              <help>Restart OSPFv3 daemon</help>
+            </properties>
+            <command>${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon ospf6d</command>
+          </leafNode>
+          <leafNode name="ripd">
+            <properties>
+              <help>Restart Routing Information Protocol daemon</help>
+            </properties>
+            <command>${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon ripd</command>
+          </leafNode>
+          <leafNode name="ripngd">
+            <properties>
+              <help>Restart RIPng daemon</help>
+            </properties>
+            <command>${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon ripngd</command>
+          </leafNode>
+          <leafNode name="staticd">
+            <properties>
+              <help>Restart Static Route daemon</help>
+            </properties>
+            <command>${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon staticd</command>
+          </leafNode>
+          <leafNode name="zebra">
+            <properties>
+              <help>Restart IP routing manager daemon</help>
+            </properties>
+            <command>${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon zebra</command>
+          </leafNode>
+        </children>
+      </node>
+    </children>
+  </node>
+</interfaceDefinition>
diff --git a/src/op_mode/restart_frr.py b/src/op_mode/restart_frr.py
new file mode 100755
index 000000000..d94465603
--- /dev/null
+++ b/src/op_mode/restart_frr.py
@@ -0,0 +1,103 @@
+#!/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 subprocess
+import logging
+from logging.handlers import SysLogHandler
+
+# some default values
+watchfrr = '/usr/lib/frr/watchfrr.sh'
+vtysh = '/usr/bin/vtysh'
+
+# configure logging
+logger = logging.getLogger(__name__)
+logs_handler = SysLogHandler('/dev/log')
+logs_handler.setFormatter(logging.Formatter('%(filename)s: %(message)s'))
+logger.addHandler(logs_handler)
+logger.setLevel(logging.INFO)
+
+# write active config to file
+def _write_config():
+    command = "sudo {} -n -c write ".format(vtysh)
+    return_code = subprocess.call(command, shell=True)
+    if not return_code == 0:
+        logger.error("Failed to save active config: \"{}\" returned exit code: {}".format(command, return_code))
+        print("Failed to save active config: \"{}\" returned exit code: {}".format(command, return_code))
+        sys.exit(1)
+    logger.info("Active config saved to /etc/frr/frr.conf")
+
+# check if daemon is running
+def _daemon_check(daemon):
+    command = "sudo {} print_status {}".format(watchfrr, daemon)
+    return_code = subprocess.call(command, shell=True)
+    if not return_code == 0:
+        logger.error("Daemon \"{}\" is not running".format(daemon))
+        return False
+
+    # return True if all checks were passed
+    return True
+
+# restart daemon
+def _daemon_restart(daemon):
+    command = "sudo {} restart {}".format(watchfrr, daemon)
+    return_code = subprocess.call(command, shell=True)
+    if not return_code == 0:
+        logger.error("Failed to restart daemon \"{}\"".format(daemon))
+        return False
+
+    # return True if restarted sucessfully
+    logger.info("Daemon \"{}\" restarted".format(daemon))
+    return True
+
+# check all daemons if they are running
+def _check_args_daemon(daemons):
+    for daemon in daemons:
+        if not _daemon_check(daemon):
+            return False
+    return True
+
+# define program arguments
+cmd_args_parser = argparse.ArgumentParser(description='restart frr daemons')
+cmd_args_parser.add_argument('--action', choices=['restart'], required=True, help='action to frr daemons')
+cmd_args_parser.add_argument('--daemon', choices=['bfdd', 'bgpd', 'ospfd', 'ospf6d', 'ripd', 'ripngd', 'staticd', 'zebra'], required=False,  nargs='*', help='select single or multiple daemons')
+# parse arguments
+cmd_args = cmd_args_parser.parse_args()
+
+
+# main logic
+# restart daemon
+if cmd_args.action == 'restart':
+    _write_config()
+    if cmd_args.daemon:
+        # check all daemons if they are running
+        if not _check_args_daemon(cmd_args.daemon):
+            print("Warning: some of listed daemons are not running")
+
+        # run command to restart daemon
+        for daemon in cmd_args.daemon:
+            if not _daemon_restart(daemon):
+                print("Failed to restart daemon: {}".format(daemon))
+                sys.exit(1)
+    else:
+        # run command to restart FRR
+        if not _daemon_restart(''):
+            print("Failed to restart FRRouting")
+            sys.exit(1)
+
+sys.exit(0)
-- 
cgit v1.2.3


From 9196be8fe3a37ee7dd7308243f5b0017e13d08b1 Mon Sep 17 00:00:00 2001
From: zsdc <taras@vyos.io>
Date: Fri, 27 Dec 2019 23:55:09 +0200
Subject: FRRouting: T1514: Fix in FRRouting restart command

Added saving and restoring current frr.conf to avoid reapplying of configuration from it during reboot.
---
 src/op_mode/restart_frr.py | 40 ++++++++++++++++++++++++++++++++++++----
 1 file changed, 36 insertions(+), 4 deletions(-)

(limited to 'src')

diff --git a/src/op_mode/restart_frr.py b/src/op_mode/restart_frr.py
index d94465603..085b8c355 100755
--- a/src/op_mode/restart_frr.py
+++ b/src/op_mode/restart_frr.py
@@ -24,6 +24,8 @@ from logging.handlers import SysLogHandler
 # some default values
 watchfrr = '/usr/lib/frr/watchfrr.sh'
 vtysh = '/usr/bin/vtysh'
+frrconfig = '/etc/frr/frr.conf'
+frrconfig_tmp = '/etc/frr/frr.conf.temporary'
 
 # configure logging
 logger = logging.getLogger(__name__)
@@ -32,15 +34,32 @@ logs_handler.setFormatter(logging.Formatter('%(filename)s: %(message)s'))
 logger.addHandler(logs_handler)
 logger.setLevel(logging.INFO)
 
+# save or restore current config file
+def _save_and_restore(action):
+    if action == "save":
+        command = "sudo mv {} {}".format(frrconfig, frrconfig_tmp)
+        logmsg = "Permanent configuration saved to {}".format(frrconfig_tmp)
+    if action == "restore":
+        command = "sudo mv {} {}".format(frrconfig_tmp, frrconfig)
+        logmsg = "Permanent configuration restored from {}".format(frrconfig_tmp)
+
+    return_code = subprocess.call(command, shell=True)
+    if not return_code == 0:
+        logger.error("Failed to rename permanent config: \"{}\" returned exit code: {}".format(command, return_code))
+        return False
+
+    logger.info(logmsg)
+    return True
+
 # write active config to file
 def _write_config():
     command = "sudo {} -n -c write ".format(vtysh)
     return_code = subprocess.call(command, shell=True)
     if not return_code == 0:
         logger.error("Failed to save active config: \"{}\" returned exit code: {}".format(command, return_code))
-        print("Failed to save active config: \"{}\" returned exit code: {}".format(command, return_code))
-        sys.exit(1)
-    logger.info("Active config saved to /etc/frr/frr.conf")
+        return False
+    logger.info("Active config saved to {}".format(frrconfig))
+    return True
 
 # check if daemon is running
 def _daemon_check(daemon):
@@ -83,7 +102,16 @@ cmd_args = cmd_args_parser.parse_args()
 # main logic
 # restart daemon
 if cmd_args.action == 'restart':
-    _write_config()
+    if not _save_and_restore('save'):
+        logger.error("Failed to rename permanent comfig")
+        print("Failed to rename permanent comfig")
+        sys.exit(1)
+
+    if not _write_config():
+        print("Failed to save active config")
+        _save_and_restore('restore')
+        sys.exit(1)
+
     if cmd_args.daemon:
         # check all daemons if they are running
         if not _check_args_daemon(cmd_args.daemon):
@@ -93,11 +121,15 @@ if cmd_args.action == 'restart':
         for daemon in cmd_args.daemon:
             if not _daemon_restart(daemon):
                 print("Failed to restart daemon: {}".format(daemon))
+                _save_and_restore('restore')
                 sys.exit(1)
     else:
         # run command to restart FRR
         if not _daemon_restart(''):
             print("Failed to restart FRRouting")
+            _save_and_restore('restore')
             sys.exit(1)
 
+    _save_and_restore('restore')
+
 sys.exit(0)
-- 
cgit v1.2.3


From 30d0c3c8525b8e39fc7536f5acc424f42d5fe821 Mon Sep 17 00:00:00 2001
From: DmitriyEshenko <dmitriy.eshenko@vyos.io>
Date: Sun, 29 Dec 2019 19:02:32 +0000
Subject: l2tp: T1918: Add check and create tunnels folder

---
 src/conf_mode/ipsec-settings.py | 41 ++++++++++++++++++++++++-----------------
 1 file changed, 24 insertions(+), 17 deletions(-)

(limited to 'src')

diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py
index 331a62316..aab3e9734 100755
--- a/src/conf_mode/ipsec-settings.py
+++ b/src/conf_mode/ipsec-settings.py
@@ -32,14 +32,15 @@ from vyos import ConfigError
 ra_conn_name = "remote-access"
 charon_conf_file = "/etc/strongswan.d/charon.conf"
 ipsec_secrets_flie = "/etc/ipsec.secrets"
-ipsec_ra_conn_file = "/etc/ipsec.d/tunnels/"+ra_conn_name
+ipsec_ra_conn_dir = "/etc/ipsec.d/tunnels/"
+ipsec_ra_conn_file = ipsec_ra_conn_dir + ra_conn_name
 ipsec_conf_flie = "/etc/ipsec.conf"
-ca_cert_path = '/etc/ipsec.d/cacerts'
-server_cert_path = '/etc/ipsec.d/certs'
-server_key_path = '/etc/ipsec.d/private'
+ca_cert_path = "/etc/ipsec.d/cacerts"
+server_cert_path = "/etc/ipsec.d/certs"
+server_key_path = "/etc/ipsec.d/private"
 delim_ipsec_l2tp_begin = "### VyOS L2TP VPN Begin ###"
 delim_ipsec_l2tp_end = "### VyOS L2TP VPN End ###"
-charon_pidfile = '/var/run/charon.pid'
+charon_pidfile = "/var/run/charon.pid"
 
 l2pt_ipsec_conf = '''
 {{delim_ipsec_l2tp_begin}}
@@ -147,21 +148,27 @@ def get_config():
 
 ### ipsec secret l2tp
 def write_ipsec_secrets(c):
-  tmpl = jinja2.Template(l2pt_ipsec_secrets_conf, trim_blocks=True)
-  l2pt_ipsec_secrets_txt = tmpl.render(c)
-  old_umask = os.umask(0o077)
-  open(ipsec_secrets_flie,'w').write(l2pt_ipsec_secrets_txt)
-  os.umask(old_umask)
-  sl.syslog(sl.LOG_NOTICE, ipsec_secrets_flie + ' written')
+    tmpl = jinja2.Template(l2pt_ipsec_secrets_conf, trim_blocks=True)
+    l2pt_ipsec_secrets_txt = tmpl.render(c)
+    old_umask = os.umask(0o077)
+    open(ipsec_secrets_flie,'w').write(l2pt_ipsec_secrets_txt)
+    os.umask(old_umask)
+    sl.syslog(sl.LOG_NOTICE, ipsec_secrets_flie + ' written')
 
 ### ipsec remote access connection config
 def write_ipsec_ra_conn(c):
-  tmpl = jinja2.Template(l2tp_ipsec_ra_conn_conf, trim_blocks=True)
-  ipsec_ra_conn_txt = tmpl.render(c)
-  old_umask = os.umask(0o077)
-  open(ipsec_ra_conn_file,'w').write(ipsec_ra_conn_txt)
-  os.umask(old_umask)
-  sl.syslog(sl.LOG_NOTICE, ipsec_ra_conn_file + ' written')
+    tmpl = jinja2.Template(l2tp_ipsec_ra_conn_conf, trim_blocks=True)
+    ipsec_ra_conn_txt = tmpl.render(c)
+    old_umask = os.umask(0o077)
+
+    # Create tunnels directory if does not exist
+    if not os.path.exists(ipsec_ra_conn_dir):
+        os.makedirs(ipsec_ra_conn_dir)
+        sl.syslog(sl.LOG_NOTICE, ipsec_ra_conn_dir  + " created")
+
+    open(ipsec_ra_conn_file,'w').write(ipsec_ra_conn_txt)
+    os.umask(old_umask)
+    sl.syslog(sl.LOG_NOTICE, ipsec_ra_conn_file + ' written')
 
 ### Remove config from file by delimiter
 def remove_confs(delim_begin, delim_end, conf_file):
-- 
cgit v1.2.3


From 685e9fb19140d002bcae9f3b78d5bd0f33b172d0 Mon Sep 17 00:00:00 2001
From: DmitriyEshenko <dmitriy.eshenko@vyos.io>
Date: Mon, 30 Dec 2019 11:50:15 +0000
Subject: l2tp: T1858: Delete deprecated outside-nexthop

---
 interface-definitions/l2tp-server.xml.in |  4 ++--
 src/conf_mode/accel_l2tp.py              | 24 ++++++++++-------------
 src/migration-scripts/l2tp/1-to-2        | 33 ++++++++++++++++++++++++++++++++
 3 files changed, 45 insertions(+), 16 deletions(-)
 create mode 100755 src/migration-scripts/l2tp/1-to-2

(limited to 'src')

diff --git a/interface-definitions/l2tp-server.xml.in b/interface-definitions/l2tp-server.xml.in
index 98c17b8b2..7fc844054 100644
--- a/interface-definitions/l2tp-server.xml.in
+++ b/interface-definitions/l2tp-server.xml.in
@@ -28,9 +28,9 @@
                   </constraint>
                 </properties>
               </leafNode>
-              <leafNode name="outside-nexthop">
+              <leafNode name="gateway-address">
                 <properties>
-                  <help>Nexthop IP address for reaching the VPN clients</help>
+                  <help>Gatway address uses as client tunnel termination point</help>
                   <constraint>
                     <validator name="ipv4-address"/>
                   </constraint>
diff --git a/src/conf_mode/accel_l2tp.py b/src/conf_mode/accel_l2tp.py
index 37fda2029..a7af9cc68 100755
--- a/src/conf_mode/accel_l2tp.py
+++ b/src/conf_mode/accel_l2tp.py
@@ -118,15 +118,15 @@ secret={{lns_shared_secret}}
 {% endfor -%}
 {% endif %}
 {% endif %}
-{% if outside_nexthop %}
-gw-ip-address={{outside_nexthop}}
+{% if gateway_address %}
+gw-ip-address={{gateway_address}}
 {% endif %}
 
 {% if authentication['mode'] == 'local' %}
 [chap-secrets]
 chap-secrets=/etc/accel-ppp/l2tp/chap-secrets
-{% if outside_nexthop %}
-gw-ip-address={{outside_nexthop}}
+{% if gateway_address %}
+gw-ip-address={{gateway_address}}
 {% endif %}
 {% endif %}
 
@@ -181,7 +181,7 @@ dae-server={{authentication['radiusopt']['dae-srv']['ip-addr']}}:\
 {{authentication['radiusopt']['dae-srv']['port']}},\
 {{authentication['radiusopt']['dae-srv']['secret']}}
 {% endif -%}
-gw-ip-address={{outside_nexthop}}
+gw-ip-address={{gateway_address}}
 verbose=1
 {% endif -%}
 
@@ -290,7 +290,7 @@ def get_config():
     'mppe'                : 'prefer'
     },
     'outside_addr'        : '',
-    'outside_nexthop'     : '10.255.255.0',
+    'gateway_address'     : '10.255.255.0',
     'dns'                 : [],
     'dnsv6'               : [],
     'wins'                : [],
@@ -430,17 +430,17 @@ def get_config():
     config_data['mtu'] = c.return_value('mtu')
 
   ### gateway address 
-  if c.exists('outside-nexthop'):
-    config_data['outside_nexthop'] = c.return_value('outside-nexthop') 
+  if c.exists('gateway-address'):
+    config_data['gateway_address'] = c.return_value('gateway-address')
   else:
     ### calculate gw-ip-address
     if c.exists('client-ip-pool start'):
       ### use start ip as gw-ip-address
-      config_data['outside_nexthop'] = c.return_value('client-ip-pool start')
+      config_data['gateway_address'] = c.return_value('client-ip-pool start')
     elif c.exists('client-ip-pool subnet'):
       ### use first ip address from first defined pool
       lst_ip = re.findall("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}", c.return_values('client-ip-pool subnet')[0])
-      config_data['outside_nexthop'] = lst_ip[0]
+      config_data['gateway_address'] = lst_ip[0]
 
   if c.exists('authentication require'):
     auth_mods = {'pap' : 'pap','chap' : 'auth_chap_md5', 'mschap' : 'auth_mschap_v1', 'mschap-v2' : 'auth_mschap_v2'}
@@ -497,10 +497,6 @@ def verify(c):
   if not c['client_ip_pool'] and not c['client_ip_subnets']:
     raise ConfigError("set vpn l2tp remote-access client-ip-pool requires subnet or start/stop IP pool")
 
-  if not c['outside_nexthop']:
-    #raise ConfigError('set vpn l2tp remote-access outside-nexthop required')
-    print ("WARMING: set vpn l2tp remote-access outside-nexthop required")
-
   ## check ipv6
   if 'delegate_prefix' in c['client_ipv6_pool'] and not 'prefix' in c['client_ipv6_pool']:
     raise ConfigError("\"set vpn l2tp remote-access client-ipv6-pool prefix\" required for delegate-prefix ")
diff --git a/src/migration-scripts/l2tp/1-to-2 b/src/migration-scripts/l2tp/1-to-2
new file mode 100755
index 000000000..c46eba8f8
--- /dev/null
+++ b/src/migration-scripts/l2tp/1-to-2
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+
+# Delete depricated outside-nexthop address
+
+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)
+
+cfg_base = ['vpn', 'l2tp', 'remote-access']
+if not config.exists(cfg_base):
+    # Nothing to do
+    sys.exit(0)
+else:
+    if config.exists(cfg_base + ['outside-nexthop']):
+        config.delete(cfg_base + ['outside-nexthop'])
+
+    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)
-- 
cgit v1.2.3


From ef3b8d81fc98a43e6a7605ea807667ad8b674bd9 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Mon, 30 Dec 2019 13:00:22 +0100
Subject: list_interfaces: remove duplicate list of wireless interfaces

---
 src/completion/list_interfaces.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

(limited to 'src')

diff --git a/src/completion/list_interfaces.py b/src/completion/list_interfaces.py
index f336968a6..0ed683074 100755
--- a/src/completion/list_interfaces.py
+++ b/src/completion/list_interfaces.py
@@ -32,10 +32,9 @@ elif args.bridgeable:
     bond = vyos.interfaces.list_interfaces_of_type("bonding")
     l2tpv3 = vyos.interfaces.list_interfaces_of_type("l2tpv3")
     openvpn = vyos.interfaces.list_interfaces_of_type("openvpn")
-    vxlan = vyos.interfaces.list_interfaces_of_type("vxlan")
     wireless = vyos.interfaces.list_interfaces_of_type("wireless")
     tunnel = vyos.interfaces.list_interfaces_of_type("tunnel")
-    wireless = vyos.interfaces.list_interfaces_of_type("wireless")
+    vxlan = vyos.interfaces.list_interfaces_of_type("vxlan")
     geneve  = vyos.interfaces.list_interfaces_of_type("geneve")
 
     interfaces = eth + bond + l2tpv3 + openvpn + vxlan + tunnel + wireless + geneve
-- 
cgit v1.2.3


From b9a6dab2d4f162eba59b9eec989b1de1b249f3fd Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Mon, 30 Dec 2019 14:42:04 +0100
Subject: options: T1919: migrate 'system options' to XML/Python representation

---
 Makefile                                    |  1 -
 interface-definitions/system-options.xml.in | 45 ++++++++++++++++
 src/conf_mode/system-options.py             | 81 +++++++++++++++++++++++++++++
 src/migration-scripts/system/14-to-15       | 38 ++++++++++++++
 4 files changed, 164 insertions(+), 1 deletion(-)
 create mode 100644 interface-definitions/system-options.xml.in
 create mode 100755 src/conf_mode/system-options.py
 create mode 100755 src/migration-scripts/system/14-to-15

(limited to 'src')

diff --git a/Makefile b/Makefile
index c085872a1..ee0bf3ce9 100644
--- a/Makefile
+++ b/Makefile
@@ -46,7 +46,6 @@ interface_definitions: $(BUILD_DIR) $(obj)
 	rm -f $(TMPL_DIR)/protocols/node.def
 	rm -f $(TMPL_DIR)/protocols/static/node.def
 	rm -f $(TMPL_DIR)/system/node.def
-	rm -f $(TMPL_DIR)/system/options/node.def
 	rm -f $(TMPL_DIR)/vpn/node.def
 	rm -f $(TMPL_DIR)/vpn/ipsec/node.def
 
diff --git a/interface-definitions/system-options.xml.in b/interface-definitions/system-options.xml.in
new file mode 100644
index 000000000..5fa0635bd
--- /dev/null
+++ b/interface-definitions/system-options.xml.in
@@ -0,0 +1,45 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+  <node name="system">
+    <children>
+      <node name="options" owner="${vyos_conf_scripts_dir}/system-options.py">
+        <properties>
+          <help>System Options</help>
+          <priority>400</priority>
+        </properties>
+        <children>
+          <leafNode name="ctrl-alt-del-action">
+            <properties>
+              <help>Ctrl-Alt-Delete action</help>
+              <completionHelp>
+                <list>ignore reboot poweroff</list>
+              </completionHelp>
+              <valueHelp>
+                <format>ignore</format>
+                <description>Ignore Ctrl-Alt-Delete</description>
+              </valueHelp>
+              <valueHelp>
+                <format>reboot</format>
+                <description>Reboot VyOS</description>
+              </valueHelp>
+              <valueHelp>
+                <format>poweroff</format>
+                <description>Poweroff VyOS</description>
+              </valueHelp>
+              <constraint>
+                <regex>(ignore|reboot|poweroff)</regex>
+              </constraint>
+              <constraintErrorMessage>Must be ignore, reboot, or poweroff</constraintErrorMessage>
+            </properties>
+           </leafNode>
+           <leafNode name="reboot-on-panic">
+             <properties>
+               <help>Reboot system on kernel panic</help>
+               <valueless/>
+             </properties>
+           </leafNode>
+        </children>
+      </node>
+    </children>
+  </node>
+</interfaceDefinition>
diff --git a/src/conf_mode/system-options.py b/src/conf_mode/system-options.py
new file mode 100755
index 000000000..4c809d044
--- /dev/null
+++ b/src/conf_mode/system-options.py
@@ -0,0 +1,81 @@
+#!/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 sys import exit
+from copy import deepcopy
+from vyos.config import Config
+from vyos import ConfigError
+
+systemd_ctrl_alt_del = '/lib/systemd/system/ctrl-alt-del.target'
+
+default_config_data = {
+    'ctrl_alt_del': 'ignore',
+    'reboot_on_panic': True
+}
+
+def get_config():
+    opt = deepcopy(default_config_data)
+    conf = Config()
+    conf.set_level('system options')
+    if conf.exists(''):
+        if conf.exists('ctrl-alt-del-action'):
+            opt['ctrl_alt_del'] = conf.return_value('ctrl-alt-del-action')
+
+        opt['reboot_on_panic'] = conf.exists('reboot-on-panic')
+
+    return opt
+
+def verify(opt):
+    pass
+
+def generate(opt):
+    pass
+
+def apply(opt):
+
+    # Ctrl-Alt-Delete action
+    if opt['ctrl_alt_del'] == 'ignore':
+        os.unlink('/lib/systemd/system/ctrl-alt-del.target')
+
+    elif opt['ctrl_alt_del'] == 'reboot':
+        if os.path.exists(systemd_ctrl_alt_del):
+            os.unlink(systemd_ctrl_alt_del)
+        os.symlink('/lib/systemd/system/reboot.target', systemd_ctrl_alt_del)
+
+    elif opt['ctrl_alt_del'] == 'poweroff':
+        if os.path.exists(systemd_ctrl_alt_del):
+            os.unlink(systemd_ctrl_alt_del)
+        os.symlink('/lib/systemd/system/poweroff.target', systemd_ctrl_alt_del)
+
+    # Reboot system on kernel panic
+    with open('/proc/sys/kernel/panic', 'w') as f:
+        if opt['reboot_on_panic']:
+            f.write('60')
+        else:
+            f.write('0')
+
+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/migration-scripts/system/14-to-15 b/src/migration-scripts/system/14-to-15
new file mode 100755
index 000000000..fd89ae57a
--- /dev/null
+++ b/src/migration-scripts/system/14-to-15
@@ -0,0 +1,38 @@
+#!/usr/bin/env python3
+#
+# Make 'system options reboot-on-panic' valueless
+
+import os
+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 = ['system', 'options']
+if not config.exists(base):
+    # Nothing to do
+    sys.exit(0)
+else:
+    # delete 'system ipv6 blacklist' node
+    if config.exists(base + ['reboot-on-panic']):
+        reboot = config.return_value(base + ['reboot-on-panic'])
+        config.delete(base + ['reboot-on-panic'])
+        # create new valueless node if action was true
+        if reboot == "true":
+            config.set(base + ['reboot-on-panic'])
+
+    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)
-- 
cgit v1.2.3