diff --git a/pom.xml b/pom.xml
index 56d209aba196..80111b26a4ab 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1095,6 +1095,7 @@
systemvm/agent/noVNC/**
systemvm/agent/packages/**
systemvm/debian/**
+ systemvm/test/frr-bgppeers-ipv4-ipv6.conf
test/integration/component/test_host_ha.sh
tools/appliance/*/template.json
tools/cli/cloudmonkey.egg-info/*
diff --git a/systemvm/debian/opt/cloud/bin/cs/CsBgpPeers.py b/systemvm/debian/opt/cloud/bin/cs/CsBgpPeers.py
index 137279b74a9e..38d85f4626fc 100755
--- a/systemvm/debian/opt/cloud/bin/cs/CsBgpPeers.py
+++ b/systemvm/debian/opt/cloud/bin/cs/CsBgpPeers.py
@@ -54,7 +54,9 @@ def process(self):
self.frr_conf = CsFile(FRR_CONFIG)
self.frr_conf.repopulate()
self._pre_set()
+ self._access_list_set()
self._process_peers()
+ self._route_map_set()
self._post_set()
if self.frr_conf.commit():
restart_frr = True
@@ -75,11 +77,31 @@ def _process_dbag_item(self, item):
self.peers[as_number]['ip6_peers'].append(item)
def _pre_set(self):
- self.frr_conf.add("frr version 6.0")
self.frr_conf.add("frr defaults traditional")
self.frr_conf.add("hostname {}".format(CsHelper.get_hostname()))
self.frr_conf.add("service integrated-vtysh-config")
self.frr_conf.add("ip nht resolve-via-default")
+ self.frr_conf.add("ipv6 nht resolve-via-default")
+ return
+
+ def _access_list_set(self):
+ self.frr_conf.add("ip prefix-list all-v4 seq 1 permit any")
+ self.frr_conf.add("ip prefix-list default-v4 seq 1 permit 0.0.0.0/0")
+ self.frr_conf.add("ipv6 prefix-list all-v6 seq 1 permit any")
+ self.frr_conf.add("ipv6 prefix-list default-v6 seq 1 permit ::/0")
+
+ ip4_cidrs = set()
+ ip6_cidrs = set()
+ for as_number in self.peers.keys():
+ ip4_cidrs.update(ip4_peer['guest_ip4_cidr'] for ip4_peer in self.peers[as_number]['ip4_peers'])
+ ip6_cidrs.update(ip6_peer['guest_ip6_cidr'] for ip6_peer in self.peers[as_number]['ip6_peers'])
+
+ for seq, ip4_cidr in enumerate(sorted(ip4_cidrs), start=1):
+ self.frr_conf.add("ip prefix-list local-v4 seq {} permit {}".format(seq, ip4_cidr))
+
+ for seq, ip6_cidr in enumerate(sorted(ip6_cidrs), start=1):
+ self.frr_conf.add("ipv6 prefix-list local-v6 seq {} permit {}".format(seq, ip6_cidr))
+
return
def _process_peers(self):
@@ -104,16 +126,46 @@ def _process_peers(self):
self.frr_conf.add(" neighbor {} ebgp-multihop {}".format(ip6_peer['ip6_address'], ip6_peer['details']['EBGP_MultiHop']))
if self.peers[as_number]['ip4_peers']:
self.frr_conf.add(" address-family ipv4 unicast")
+ for ip4_peer in self.peers[as_number]['ip4_peers']:
+ self.frr_conf.add(" neighbor {} route-map upstream-v4-in in".format(ip4_peer['ip4_address']))
+ self.frr_conf.add(" neighbor {} route-map upstream-v4-out out".format(ip4_peer['ip4_address']))
+ self.frr_conf.add(" neighbor {} soft-reconfiguration inbound".format(ip4_peer['ip4_address']))
ip4_cidrs = set({ip4_peer['guest_ip4_cidr'] for ip4_peer in self.peers[as_number]['ip4_peers']})
for ip4_cidr in ip4_cidrs:
self.frr_conf.add(" network {}".format(ip4_cidr))
self.frr_conf.add(" exit-address-family")
if self.peers[as_number]['ip6_peers']:
self.frr_conf.add(" address-family ipv6 unicast")
+ for ip6_peer in self.peers[as_number]['ip6_peers']:
+ self.frr_conf.add(" neighbor {} route-map upstream-v6-in in".format(ip6_peer['ip6_address']))
+ self.frr_conf.add(" neighbor {} route-map upstream-v6-out out".format(ip6_peer['ip6_address']))
+ self.frr_conf.add(" neighbor {} soft-reconfiguration inbound".format(ip6_peer['ip6_address']))
ip6_cidrs = set({ip6_peer['guest_ip6_cidr'] for ip6_peer in self.peers[as_number]['ip6_peers']})
for ip6_cidr in ip6_cidrs:
self.frr_conf.add(" network {}".format(ip6_cidr))
self.frr_conf.add(" exit-address-family")
+ def _route_map_set(self):
+ self.frr_conf.add("route-map upstream-v4-in permit 10")
+ self.frr_conf.add(" match ip address prefix-list default-v4")
+ self.frr_conf.add("route-map upstream-v4-in deny 1000")
+ self.frr_conf.add(" match ip address prefix-list all-v4")
+
+ self.frr_conf.add("route-map upstream-v4-out permit 10")
+ self.frr_conf.add(" match ip address prefix-list local-v4")
+ self.frr_conf.add("route-map upstream-v4-out deny 1000")
+ self.frr_conf.add(" match ip address prefix-list all-v4")
+
+ self.frr_conf.add("route-map upstream-v6-in permit 10")
+ self.frr_conf.add(" match ipv6 address prefix-list default-v6")
+ self.frr_conf.add("route-map upstream-v6-in deny 1000")
+ self.frr_conf.add(" match ipv6 address prefix-list all-v6")
+
+ self.frr_conf.add("route-map upstream-v6-out permit 10")
+ self.frr_conf.add(" match ipv6 address prefix-list local-v6")
+ self.frr_conf.add("route-map upstream-v6-out deny 1000")
+ self.frr_conf.add(" match ipv6 address prefix-list all-v6")
+ return
+
def _post_set(self):
self.frr_conf.add("line vty")
diff --git a/systemvm/test/TestCsBgpPeers.py b/systemvm/test/TestCsBgpPeers.py
new file mode 100644
index 000000000000..f6de192976e4
--- /dev/null
+++ b/systemvm/test/TestCsBgpPeers.py
@@ -0,0 +1,208 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import os
+import unittest
+try:
+ import mock
+except ImportError:
+ from unittest import mock
+from cs.CsBgpPeers import CsBgpPeers
+from cs.CsFile import CsFile
+import merge
+
+
+class TestCsBgpPeers(unittest.TestCase):
+
+ def setUp(self):
+ merge.DataBag.DPATH = "."
+ self.csbgppeers = CsBgpPeers("bgppeers", {})
+ self.csbgppeers.peers = {}
+ self.csbgppeers.public_ip = "100.64.0.10"
+ self.csbgppeers.frr_conf = CsFile("frr.conf.test")
+ self.csbgppeers.frr_conf.repopulate()
+
+ def _peer(self, **kwargs):
+ peer = {
+ 'peer_id': 1,
+ 'network_id': 100,
+ 'network_as_number': 64512,
+ 'peer_as_number': 64496
+ }
+ peer.update(kwargs)
+ return peer
+
+ def _frr_conf(self):
+ return [line.rstrip('\n') for line in self.csbgppeers.frr_conf.new_config if not line.strip().startswith("#")]
+
+ def test_init(self):
+ self.assertTrue(self.csbgppeers is not None)
+
+ def test_process_dbag_item(self):
+ self.csbgppeers._process_dbag_item(self._peer(ip4_address='100.64.0.1',
+ guest_ip4_cidr='10.1.1.0/24'))
+ self.csbgppeers._process_dbag_item(self._peer(ip6_address='2001:db8::1',
+ guest_ip6_cidr='2001:db8:100::/64'))
+ self.csbgppeers._process_dbag_item(self._peer(network_as_number=64513,
+ ip4_address='100.64.1.1',
+ guest_ip4_cidr='10.2.1.0/24'))
+
+ self.assertEqual(sorted(self.csbgppeers.peers.keys()), [64512, 64513])
+ self.assertEqual(len(self.csbgppeers.peers[64512]['ip4_peers']), 1)
+ self.assertEqual(len(self.csbgppeers.peers[64512]['ip6_peers']), 1)
+ self.assertEqual(len(self.csbgppeers.peers[64513]['ip4_peers']), 1)
+ self.assertEqual(len(self.csbgppeers.peers[64513]['ip6_peers']), 0)
+
+ def test_process_dbag_item_dual_stack(self):
+ self.csbgppeers._process_dbag_item(self._peer(ip4_address='100.64.0.1',
+ guest_ip4_cidr='10.1.1.0/24',
+ ip6_address='2001:db8::1',
+ guest_ip6_cidr='2001:db8:100::/64'))
+
+ self.assertEqual(len(self.csbgppeers.peers[64512]['ip4_peers']), 1)
+ self.assertEqual(len(self.csbgppeers.peers[64512]['ip6_peers']), 1)
+
+ def test_access_list_set(self):
+ self.csbgppeers._process_dbag_item(self._peer(ip4_address='100.64.0.1',
+ guest_ip4_cidr='10.1.1.0/24',
+ ip6_address='2001:db8::1',
+ guest_ip6_cidr='2001:db8:100::/64'))
+ self.csbgppeers._access_list_set()
+
+ config = self._frr_conf()
+ self.assertIn("ip prefix-list all-v4 seq 1 permit any", config)
+ self.assertIn("ip prefix-list default-v4 seq 1 permit 0.0.0.0/0", config)
+ self.assertIn("ipv6 prefix-list all-v6 seq 1 permit any", config)
+ self.assertIn("ipv6 prefix-list default-v6 seq 1 permit ::/0", config)
+ self.assertIn("ip prefix-list local-v4 seq 1 permit 10.1.1.0/24", config)
+ self.assertIn("ipv6 prefix-list local-v6 seq 1 permit 2001:db8:100::/64", config)
+
+ def test_access_list_set_deduplicates_cidrs(self):
+ # Two peers announcing the same guest CIDR should only result in a single prefix-list entry
+ self.csbgppeers._process_dbag_item(self._peer(ip4_address='100.64.0.1',
+ guest_ip4_cidr='10.1.1.0/24'))
+ self.csbgppeers._process_dbag_item(self._peer(peer_id=2,
+ ip4_address='100.64.0.2',
+ guest_ip4_cidr='10.1.1.0/24'))
+ self.csbgppeers._access_list_set()
+
+ config = self._frr_conf()
+ local_v4 = [line for line in config if line.startswith("ip prefix-list local-v4")]
+ self.assertEqual(local_v4, ["ip prefix-list local-v4 seq 1 permit 10.1.1.0/24"])
+
+ def test_access_list_set_multiple_as_numbers(self):
+ # Sequence numbers may not collide when there are peers for multiple AS numbers,
+ # otherwise entries overwrite each other in the shared local-v4/local-v6 prefix-lists
+ self.csbgppeers._process_dbag_item(self._peer(ip4_address='100.64.0.1',
+ guest_ip4_cidr='10.1.1.0/24'))
+ self.csbgppeers._process_dbag_item(self._peer(peer_id=2,
+ network_as_number=64513,
+ ip4_address='100.64.1.1',
+ guest_ip4_cidr='10.2.1.0/24'))
+ self.csbgppeers._access_list_set()
+
+ config = self._frr_conf()
+ local_v4 = [line for line in config if line.startswith("ip prefix-list local-v4")]
+ self.assertEqual(local_v4, ["ip prefix-list local-v4 seq 1 permit 10.1.1.0/24",
+ "ip prefix-list local-v4 seq 2 permit 10.2.1.0/24"])
+
+ def test_process_peers_ip4(self):
+ self.csbgppeers._process_dbag_item(self._peer(ip4_address='100.64.0.1',
+ guest_ip4_cidr='10.1.1.0/24'))
+ self.csbgppeers._process_peers()
+
+ config = self._frr_conf()
+ self.assertIn("router bgp 64512", config)
+ self.assertIn(" bgp router-id 100.64.0.10", config)
+ self.assertIn(" neighbor 100.64.0.1 remote-as 64496", config)
+ self.assertIn(" neighbor 100.64.0.1 route-map upstream-v4-in in", config)
+ self.assertIn(" neighbor 100.64.0.1 route-map upstream-v4-out out", config)
+ self.assertIn(" neighbor 100.64.0.1 soft-reconfiguration inbound", config)
+ self.assertIn(" network 10.1.1.0/24", config)
+ self.assertNotIn(" bgp default ipv6-unicast", config)
+ self.assertNotIn(" neighbor 100.64.0.1 route-map upstream-v4-in in", config)
+ self.assertNotIn(" neighbor 100.64.0.1 route-map upstream-v4-out out", config)
+
+ def test_process_peers_ip6(self):
+ self.csbgppeers._process_dbag_item(self._peer(ip6_address='2001:db8::1',
+ guest_ip6_cidr='2001:db8:100::/64'))
+ self.csbgppeers._process_peers()
+
+ config = self._frr_conf()
+ self.assertIn(" bgp default ipv6-unicast", config)
+ self.assertIn(" neighbor 2001:db8::1 remote-as 64496", config)
+ self.assertIn(" neighbor 2001:db8::1 route-map upstream-v6-in in", config)
+ self.assertIn(" neighbor 2001:db8::1 route-map upstream-v6-out out", config)
+ self.assertIn(" neighbor 2001:db8::1 soft-reconfiguration inbound", config)
+ self.assertIn(" network 2001:db8:100::/64", config)
+ self.assertNotIn(" neighbor 2001:db8::1 route-map upstream-v6-in in", config)
+ self.assertNotIn(" neighbor 2001:db8::1 route-map upstream-v6-out out", config)
+
+ def test_process_peers_password_and_multihop(self):
+ self.csbgppeers._process_dbag_item(self._peer(ip4_address='100.64.0.1',
+ guest_ip4_cidr='10.1.1.0/24',
+ peer_password='S3cr3t!',
+ details={'EBGP_MultiHop': 2}))
+ self.csbgppeers._process_peers()
+
+ config = self._frr_conf()
+ self.assertIn(" neighbor 100.64.0.1 password S3cr3t!", config)
+ self.assertIn(" neighbor 100.64.0.1 ebgp-multihop 2", config)
+
+ def test_route_map_set(self):
+ self.csbgppeers._route_map_set()
+
+ expected = [
+ "route-map upstream-v4-in permit 10",
+ " match ip address prefix-list default-v4",
+ "route-map upstream-v4-in deny 1000",
+ " match ip address prefix-list all-v4",
+ "route-map upstream-v4-out permit 10",
+ " match ip address prefix-list local-v4",
+ "route-map upstream-v4-out deny 1000",
+ " match ip address prefix-list all-v4",
+ "route-map upstream-v6-in permit 10",
+ " match ipv6 address prefix-list default-v6",
+ "route-map upstream-v6-in deny 1000",
+ " match ipv6 address prefix-list all-v6",
+ "route-map upstream-v6-out permit 10",
+ " match ipv6 address prefix-list local-v6",
+ "route-map upstream-v6-out deny 1000",
+ " match ipv6 address prefix-list all-v6"
+ ]
+ self.assertEqual(self._frr_conf(), expected)
+
+ @mock.patch('cs.CsBgpPeers.CsHelper.get_hostname')
+ def test_full_frr_conf(self, mock_hostname):
+ mock_hostname.return_value = "r-1001-VM"
+ self.csbgppeers._process_dbag_item(self._peer(ip4_address='100.64.0.1',
+ guest_ip4_cidr='10.1.1.0/24',
+ ip6_address='2001:db8::1',
+ guest_ip6_cidr='2001:db8:100::/64'))
+ self.csbgppeers._pre_set()
+ self.csbgppeers._access_list_set()
+ self.csbgppeers._process_peers()
+ self.csbgppeers._route_map_set()
+ self.csbgppeers._post_set()
+
+ with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "frr-bgppeers-ipv4-ipv6.conf")) as f:
+ expected = [line.rstrip('\n') for line in f if not line.strip().startswith("#")]
+ self.assertEqual(self._frr_conf(), expected)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/systemvm/test/frr-bgppeers-ipv4-ipv6.conf b/systemvm/test/frr-bgppeers-ipv4-ipv6.conf
new file mode 100644
index 000000000000..9881e1c87dc1
--- /dev/null
+++ b/systemvm/test/frr-bgppeers-ipv4-ipv6.conf
@@ -0,0 +1,61 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+frr defaults traditional
+hostname r-1001-VM
+service integrated-vtysh-config
+ip nht resolve-via-default
+ipv6 nht resolve-via-default
+ip prefix-list all-v4 seq 1 permit any
+ip prefix-list default-v4 seq 1 permit 0.0.0.0/0
+ipv6 prefix-list all-v6 seq 1 permit any
+ipv6 prefix-list default-v6 seq 1 permit ::/0
+ip prefix-list local-v4 seq 1 permit 10.1.1.0/24
+ipv6 prefix-list local-v6 seq 1 permit 2001:db8:100::/64
+router bgp 64512
+ bgp router-id 100.64.0.10
+ bgp default ipv6-unicast
+ neighbor 100.64.0.1 remote-as 64496
+ neighbor 2001:db8::1 remote-as 64496
+ address-family ipv4 unicast
+ neighbor 100.64.0.1 route-map upstream-v4-in in
+ neighbor 100.64.0.1 route-map upstream-v4-out out
+ neighbor 100.64.0.1 soft-reconfiguration inbound
+ network 10.1.1.0/24
+ exit-address-family
+ address-family ipv6 unicast
+ neighbor 2001:db8::1 route-map upstream-v6-in in
+ neighbor 2001:db8::1 route-map upstream-v6-out out
+ neighbor 2001:db8::1 soft-reconfiguration inbound
+ network 2001:db8:100::/64
+ exit-address-family
+route-map upstream-v4-in permit 10
+ match ip address prefix-list default-v4
+route-map upstream-v4-in deny 1000
+ match ip address prefix-list all-v4
+route-map upstream-v4-out permit 10
+ match ip address prefix-list local-v4
+route-map upstream-v4-out deny 1000
+ match ip address prefix-list all-v4
+route-map upstream-v6-in permit 10
+ match ipv6 address prefix-list default-v6
+route-map upstream-v6-in deny 1000
+ match ipv6 address prefix-list all-v6
+route-map upstream-v6-out permit 10
+ match ipv6 address prefix-list local-v6
+route-map upstream-v6-out deny 1000
+ match ipv6 address prefix-list all-v6
+line vty