summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Poessinger <christian@poessinger.com>2021-01-27 20:38:25 +0100
committerChristian Poessinger <christian@poessinger.com>2021-01-29 10:36:21 +0100
commitca75e23fee7e4c9f809e5cabf158e9aa749c997d (patch)
tree2ab43e75c00e40e0da08166d438df16443498af3
parent8979c546bd343c9d68f42bf1215cb1721a7c3206 (diff)
downloadvyos-1x-ca75e23fee7e4c9f809e5cabf158e9aa749c997d.tar.gz
vyos-1x-ca75e23fee7e4c9f809e5cabf158e9aa749c997d.zip
rpki: T3255: provide full protocol support in XML and Python
This commit provides the implementation of the OSPF CLI with a Jinja2 template that is loaded by FRR reload.
-rw-r--r--Makefile1
-rw-r--r--data/templates/frr/rpki.frr.tmpl16
-rw-r--r--interface-definitions/protocols-rpki.xml.in85
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_bgp.py39
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_rpki.py149
-rwxr-xr-xsrc/conf_mode/protocols_rpki.py147
-rwxr-xr-xsrc/migration-scripts/rpki/0-to-158
7 files changed, 404 insertions, 91 deletions
diff --git a/Makefile b/Makefile
index 2ff72cb21..66b1e8bb7 100644
--- a/Makefile
+++ b/Makefile
@@ -47,7 +47,6 @@ interface_definitions: $(config_xml_obj)
rm -f $(TMPL_DIR)/vpn/node.def
rm -f $(TMPL_DIR)/vpn/ipsec/node.def
rm -rf $(TMPL_DIR)/vpn/nipsec
- rm -rf $(TMPL_DIR)/protocols/nrpki
# XXX: required until OSPF and RIP is migrated from vyatta-cfg-quagga to vyos-1x
mkdir $(TMPL_DIR)/interfaces/loopback/node.tag/ipv6
diff --git a/data/templates/frr/rpki.frr.tmpl b/data/templates/frr/rpki.frr.tmpl
new file mode 100644
index 000000000..bf72c7a9e
--- /dev/null
+++ b/data/templates/frr/rpki.frr.tmpl
@@ -0,0 +1,16 @@
+!
+rpki
+{% if cache is defined and cache is not none %}
+{% for peer, peer_config in cache.items() %}
+{# port is mandatory and preference uses a default value #}
+{% if peer_config.ssh is defined and peer_config.ssh.username is defined and peer_config.ssh.username is not none %}
+ rpki cache {{ peer }} {{ peer_config.port }} {{ peer_config.ssh.username }} {{ peer_config.ssh.private_key_file }} {{ peer_config.ssh.public_key_file }} {{ peer_config.ssh.known_hosts_file }} preference {{ peer_config.preference }}
+{% else %}
+ rpki cache {{ peer }} {{ peer_config.port }} preference {{ peer_config.preference }}
+{% endif %}
+{% endfor %}
+{% endif %}
+{% if polling_period is defined and polling_period is not none %}
+ rpki polling_period {{ polling_period }}
+{% endif %}
+!
diff --git a/interface-definitions/protocols-rpki.xml.in b/interface-definitions/protocols-rpki.xml.in
index b8db49e36..d96db0683 100644
--- a/interface-definitions/protocols-rpki.xml.in
+++ b/interface-definitions/protocols-rpki.xml.in
@@ -1,32 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Protocol RPKI configuration -->
<interfaceDefinition>
<node name="protocols">
<children>
- <node name="nrpki" owner="${vyos_conf_scripts_dir}/protocols_rpki.py">
+ <node name="rpki" owner="${vyos_conf_scripts_dir}/protocols_rpki.py">
<properties>
<help>BGP prefix origin validation</help>
</properties>
<children>
<tagNode name="cache">
<properties>
- <help>RPKI cache server instance</help>
+ <help>RPKI cache server address</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IP address of NTP server</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address of NTP server</description>
+ </valueHelp>
+ <valueHelp>
+ <format>hostname</format>
+ <description>Fully qualified domain name of NTP server</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ <validator name="fqdn"/>
+ </constraint>
</properties>
<children>
- <leafNode name="address">
+ #include <include/port-number.xml.i>
+ <leafNode name="preference">
<properties>
- <help>RPKI cache server address</help>
- </properties>
- </leafNode>
- <leafNode name="port">
- <properties>
- <help>TCP port number</help>
+ <help>Preference of the cache server</help>
<valueHelp>
- <format>u32:1-65535</format>
- <description>TCP port number</description>
+ <format>u32:1-255</format>
+ <description>Polling period</description>
</valueHelp>
<constraint>
- <validator name="numeric" argument="--range 1-65535"/>
+ <validator name="numeric" argument="--range 1-255"/>
</constraint>
</properties>
</leafNode>
@@ -68,55 +80,20 @@
</node>
</children>
</tagNode>
- <leafNode name="initial-synchronization-timeout">
- <properties>
- <help>Initial RPKI cache synchronization timeout</help>
- <valueHelp>
- <format>u32:0-4294967295</format>
- <description>Initial RPKI cache synchronization timeout</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 0-4294967295"/>
- </constraint>
- </properties>
- </leafNode>
<leafNode name="polling-period">
<properties>
- <help>RPKI cache polling period</help>
- <valueHelp>
- <format>u32:1-1300</format>
- <description>Polling period</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 1-1300"/>
- </constraint>
- </properties>
- </leafNode>
- <leafNode name="preference">
- <properties>
- <help>RPKI cache preference</help>
- <valueHelp>
- <format>u32:0-4294967295</format>
- <description>RPKI cache preference</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 0-4294967295"/>
- </constraint>
- </properties>
- </leafNode>
- <leafNode name="timeout">
- <properties>
- <help>RPKI cache reply timeout</help>
+ <help>RPKI cache polling period (default: 600)</help>
<valueHelp>
- <format>u32:0-4294967295</format>
- <description>RPKI cache reply timeout</description>
+ <format>u32:1-86400</format>
+ <description>Polling period in seconds</description>
</valueHelp>
<constraint>
- <validator name="numeric" argument="--range 0-4294967295"/>
+ <validator name="numeric" argument="--range 1-86400"/>
</constraint>
</properties>
+ <defaultValue>600</defaultValue>
</leafNode>
- </children>
+ </children>
</node>
</children>
</node>
diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py
index 87d10eb85..30d98976d 100755
--- a/smoketest/scripts/cli/test_protocols_bgp.py
+++ b/smoketest/scripts/cli/test_protocols_bgp.py
@@ -122,6 +122,9 @@ class TestProtocolsBGP(unittest.TestCase):
self.session.commit()
del self.session
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
def verify_frr_config(self, peer, peer_config, frrconfig):
# recurring patterns to verify for both a simple neighbor and a peer-group
if 'cap_dynamic' in peer_config:
@@ -182,8 +185,6 @@ class TestProtocolsBGP(unittest.TestCase):
self.assertIn(f' bgp default local-preference {local_pref}', frrconfig)
self.assertIn(f' no bgp default ipv4-unicast', frrconfig)
- # Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
def test_bgp_02_neighbors(self):
# Test out individual neighbor configuration items, not all of them are
@@ -419,39 +420,5 @@ class TestProtocolsBGP(unittest.TestCase):
for prefix in listen_ranges:
self.assertIn(f' bgp listen range {prefix} peer-group {peer_group}', frrconfig)
-
- def test_bgp_07_rpki(self):
- rpki_path = ['protocols', 'rpki']
- init_tmo = '50'
- polling = '400'
- preference = '100'
- timeout = '900'
-
- cache = {
- 'foo' : { 'address' : '1.1.1.1', 'port' : '8080' },
-# T3253 only one peer supported
-# 'bar' : { 'address' : '2.2.2.2', 'port' : '9090' },
- }
-
- self.session.set(rpki_path + ['polling-period', polling])
- self.session.set(rpki_path + ['preference', preference])
-
- for name, config in cache.items():
- self.session.set(rpki_path + ['cache', name, 'address', config['address']])
- self.session.set(rpki_path + ['cache', name, 'port', config['port']])
-
- # commit changes
- self.session.commit()
-
- # Verify FRR bgpd configuration
- frrconfig = getFRRRPKIconfig()
- self.assertIn(f'rpki polling_period {polling}', frrconfig)
-
- for name, config in cache.items():
- self.assertIn('rpki cache {address} {port} preference 1'.format(**config), frrconfig)
-
- self.session.delete(rpki_path)
-
-
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_rpki.py b/smoketest/scripts/cli/test_protocols_rpki.py
new file mode 100755
index 000000000..7c65ca26f
--- /dev/null
+++ b/smoketest/scripts/cli/test_protocols_rpki.py
@@ -0,0 +1,149 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import unittest
+
+from vyos.configsession import ConfigSession
+from vyos.configsession import ConfigSessionError
+from vyos.util import cmd
+from vyos.util import process_named_running
+
+base_path = ['protocols', 'rpki']
+PROCESS_NAME = 'bgpd'
+
+rpki_known_hosts = '/config/auth/known_hosts'
+rpki_ssh_key = '/config/auth/id_rsa_rpki'
+rpki_ssh_pub = f'{rpki_ssh_key}.pub'
+
+def getFRRRPKIconfig():
+ return cmd(f'vtysh -c "show run" | sed -n "/rpki/,/^!/p"')
+
+class TestProtocolsRPKI(unittest.TestCase):
+ def setUp(self):
+ self.session = ConfigSession(os.getpid())
+
+ def tearDown(self):
+ self.session.delete(base_path)
+ self.session.commit()
+ del self.session
+
+ # Nothing RPKI specific should be left over in the config
+ frrconfig = getFRRRPKIconfig()
+ self.assertNotIn('rpki', frrconfig)
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ def test_rpki(self):
+ polling = '7200'
+ cache = {
+ '192.0.2.1' : {
+ 'port' : '8080',
+ 'preference' : '1'
+ },
+ '192.0.2.2' : {
+ 'port' : '9090',
+ 'preference' : '2'
+ },
+ }
+
+ self.session.set(base_path + ['polling-period', polling])
+ for peer, peer_config in cache.items():
+ self.session.set(base_path + ['cache', peer, 'port', peer_config['port']])
+ self.session.set(base_path + ['cache', peer, 'preference', peer_config['preference']])
+
+ # commit changes
+ self.session.commit()
+
+ # Verify FRR configuration
+ frrconfig = getFRRRPKIconfig()
+ self.assertIn(f'rpki polling_period {polling}', frrconfig)
+
+ for peer, peer_config in cache.items():
+ port = peer_config['port']
+ preference = peer_config['preference']
+ self.assertIn(f'rpki cache {peer} {port} preference {preference}', frrconfig)
+
+ def test_rpki_ssh(self):
+ polling = '7200'
+ cache = {
+ '192.0.2.3' : {
+ 'port' : '1234',
+ 'username' : 'foo',
+ 'preference' : '10'
+ },
+ '192.0.2.4' : {
+ 'port' : '5678',
+ 'username' : 'bar',
+ 'preference' : '20'
+ },
+ }
+
+ self.session.set(base_path + ['polling-period', polling])
+
+ for peer, peer_config in cache.items():
+ self.session.set(base_path + ['cache', peer, 'port', peer_config['port']])
+ self.session.set(base_path + ['cache', peer, 'preference', peer_config['preference']])
+ self.session.set(base_path + ['cache', peer, 'ssh', 'username', peer_config['username']])
+ self.session.set(base_path + ['cache', peer, 'ssh', 'public-key-file', rpki_ssh_pub])
+ self.session.set(base_path + ['cache', peer, 'ssh', 'private-key-file', rpki_ssh_key])
+ self.session.set(base_path + ['cache', peer, 'ssh', 'known-hosts-file', rpki_known_hosts])
+
+ # commit changes
+ self.session.commit()
+
+ # Verify FRR configuration
+ frrconfig = getFRRRPKIconfig()
+ self.assertIn(f'rpki polling_period {polling}', frrconfig)
+
+ for peer, peer_config in cache.items():
+ port = peer_config['port']
+ preference = peer_config['preference']
+ username = peer_config['username']
+ self.assertIn(f'rpki cache {peer} {port} {username} {rpki_ssh_key} {rpki_known_hosts} preference {preference}', frrconfig)
+
+
+ def test_rpki_verify_preference(self):
+ cache = {
+ '192.0.2.1' : {
+ 'port' : '8080',
+ 'preference' : '1'
+ },
+ '192.0.2.2' : {
+ 'port' : '9090',
+ 'preference' : '1'
+ },
+ }
+
+ for peer, peer_config in cache.items():
+ self.session.set(base_path + ['cache', peer, 'port', peer_config['port']])
+ self.session.set(base_path + ['cache', peer, 'preference', peer_config['preference']])
+
+ # check validate() - preferences must be unique
+ with self.assertRaises(ConfigSessionError):
+ self.session.commit()
+
+
+if __name__ == '__main__':
+ # Create OpenSSH keypair used in RPKI tests
+ if not os.path.isfile(rpki_ssh_key):
+ cmd(f'ssh-keygen -t rsa -f {rpki_ssh_key} -N ""')
+
+ if not os.path.isfile(rpki_known_hosts):
+ cmd(f'touch {rpki_known_hosts}')
+
+ unittest.main(verbosity=2, failfast=True)
diff --git a/src/conf_mode/protocols_rpki.py b/src/conf_mode/protocols_rpki.py
new file mode 100755
index 000000000..9ea066f7f
--- /dev/null
+++ b/src/conf_mode/protocols_rpki.py
@@ -0,0 +1,147 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.configverify import verify_route_maps
+from vyos.template import render
+from vyos.template import render_to_string
+from vyos.util import call
+from vyos.util import dict_search
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import frr
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/tmp/rpki.frr'
+frr_daemon = 'bgpd'
+
+DEBUG = os.path.exists('/tmp/rpki.debug')
+if DEBUG:
+ import logging
+ lg = logging.getLogger("vyos.frr")
+ lg.setLevel(logging.DEBUG)
+ ch = logging.StreamHandler()
+ lg.addHandler(ch)
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['protocols', 'rpki']
+
+ rpki = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ if not conf.exists(base):
+ return rpki
+
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ default_values = defaults(base)
+ rpki = dict_merge(default_values, rpki)
+
+ return rpki
+
+def verify(rpki):
+ import pprint
+ pprint.pprint(rpki)
+
+ if not rpki:
+ return None
+
+ if 'cache' in rpki:
+ preferences = []
+ for peer, peer_config in rpki['cache'].items():
+ for mandatory in ['port', 'preference']:
+ if mandatory not in peer_config:
+ raise ConfigError(f'RPKI cache "{peer}" {mandatory} must be defined!')
+
+ if 'preference' in peer_config:
+ preference = peer_config['preference']
+ if preference in preferences:
+ raise ConfigError(f'RPKI cache with preference {preference} already configured!')
+ preferences.append(preference)
+
+ if 'ssh' in peer_config:
+ files = ['private_key_file', 'public_key_file', 'known_hosts_file']
+ for file in files:
+ if file not in peer_config['ssh']:
+ raise ConfigError('RPKI+SSH requires username, public/private ' \
+ 'keys and known-hosts file to be defined!')
+
+ filename = peer_config['ssh'][file]
+ if not os.path.exists(filename):
+ raise ConfigError(f'RPKI SSH {file.replace("-","-")} "{filename}" does not exist!')
+
+ return None
+
+def generate(rpki):
+ if not rpki:
+ rpki['new_frr_config'] = ''
+ return None
+
+ # render(config) not needed, its only for debug
+ render(config_file, 'frr/rpki.frr.tmpl', rpki)
+ rpki['new_frr_config'] = render_to_string('frr/rpki.frr.tmpl', rpki)
+
+ return None
+
+def apply(rpki):
+ # Save original configuration prior to starting any commit actions
+ frr_cfg = frr.FRRConfig()
+ frr_cfg.load_configuration(frr_daemon)
+ frr_cfg.modify_section('rpki', '')
+ frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', rpki['new_frr_config'])
+
+ # Debugging
+ if DEBUG:
+ from pprint import pprint
+ print('')
+ print('--------- DEBUGGING ----------')
+ pprint(dir(frr_cfg))
+ print('Existing config:\n')
+ for line in frr_cfg.original_config:
+ print(line)
+ print(f'Replacement config:\n')
+ print(f'{rpki["new_frr_config"]}')
+ print(f'Modified config:\n')
+ print(f'{frr_cfg}')
+
+ frr_cfg.commit_configuration(frr_daemon)
+
+ # If FRR config is blank, re-run the blank commit x times due to frr-reload
+ # behavior/bug not properly clearing out on one commit.
+ if rpki['new_frr_config'] == '':
+ for a in range(5):
+ frr_cfg.commit_configuration(frr_daemon)
+
+ raise ConfigError("sadf")
+ 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/migration-scripts/rpki/0-to-1 b/src/migration-scripts/rpki/0-to-1
new file mode 100755
index 000000000..9058af016
--- /dev/null
+++ b/src/migration-scripts/rpki/0-to-1
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from sys import exit
+from sys import argv
+from vyos.configtree import ConfigTree
+
+if (len(argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+base = ['protocols', 'rpki']
+config = ConfigTree(config_file)
+
+# Nothing to do
+if not config.exists(base):
+ exit(0)
+
+if config.exists(base + ['cache']):
+ preference = 1
+ for cache in config.list_nodes(base + ['cache']):
+ address_node = base + ['cache', cache, 'address']
+ if config.exists(address_node):
+ address = config.return_value(address_node)
+ # We do not longer support the address leafNode, RPKI cache server
+ # IP address is now used from the tagNode
+ config.delete(address_node)
+ # VyOS 1.2 had no per instance preference, setting new defaults
+ config.set(base + ['cache', cache, 'preference'], value=preference)
+ # Increase preference for the next caching peer - actually VyOS 1.2
+ # supported only one but better save then sorry (T3253)
+ preference += 1
+ config.rename(base + ['cache', cache], address)
+
+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))
+ exit(1)