From 4fec6225bc87196bc592ee82ddadb20f4e6ebb60 Mon Sep 17 00:00:00 2001 From: Adrian Celebanski <135693994+acelebanski@users.noreply.github.com> Date: Wed, 21 Feb 2024 11:07:39 +0100 Subject: [PATCH] feat: bgp peers snapshot comparison (#154) Co-authored-by: Alp Kose --- .../api/firewall_proxy.md | 88 +++++++++++ .../configuration_details.mdx | 66 +++++++++ .../run_low_level_methods.py | 2 + .../run_readiness_snapshot.py | 1 + examples/report/fw1.snapshot | 60 ++++++++ examples/report/fw2.snapshot | 43 ++++++ examples/report/snapshot_load_compare.py | 2 + panos_upgrade_assurance/check_firewall.py | 1 + panos_upgrade_assurance/firewall_proxy.py | 96 ++++++++++++ panos_upgrade_assurance/snapshot_compare.py | 1 + panos_upgrade_assurance/utils.py | 1 + tests/snapshots.py | 103 +++++++++++++ tests/test_firewall_proxy.py | 138 ++++++++++++++++++ tests/test_snapshot_compare.py | 24 +++ 14 files changed, 626 insertions(+) diff --git a/docs/panos-upgrade-assurance/api/firewall_proxy.md b/docs/panos-upgrade-assurance/api/firewall_proxy.md index f2f716f..64e8153 100644 --- a/docs/panos-upgrade-assurance/api/firewall_proxy.md +++ b/docs/panos-upgrade-assurance/api/firewall_proxy.md @@ -547,6 +547,94 @@ __Returns__ `dict`: Routes information. +### `FirewallProxy.get_bgp_peers` + +```python +def get_bgp_peers() -> dict +``` + +Get information about BGP peers and their status. + +The actual API command is ``. + +In the returned `dict` the key is made of three route properties delimited with an underscore (`_`) in the following +order: + +* virtual router name, +* peer group name, +* peer name. + +The key does not provide any meaningful information, it's there only to introduce uniqueness for each entry. All +properties that make a key are also available in the value of a dictionary element. + +```python showLineNumbers title="Sample output" +{ + 'default_Peer-Group1_Peer1': { + '@peer': 'Peer1', + '@vr': 'default', + 'peer-group': 'Peer-Group1', + 'peer-router-id': '169.254.8.2', + 'remote-as': '64512', + 'status': 'Established', + 'status-duration': '3804', + 'password-set': 'no', + 'passive': 'no', + 'multi-hop-ttl': '2', + 'peer-address': '169.254.8.2:35355', + 'local-address': '169.254.8.1:179', + 'reflector-client': 'not-client', + 'same-confederation': 'no', + 'aggregate-confed-as': 'yes', + 'peering-type': 'Unspecified', + 'connect-retry-interval': '15', + 'open-delay': '0', + 'idle-hold': '15', + 'prefix-limit': '5000', + 'holdtime': '30', + 'holdtime-config': '30', + 'keepalive': '10', + 'keepalive-config': '10', + 'msg-update-in': '2', + 'msg-update-out': '1', + 'msg-total-in': '385', + 'msg-total-out': '442', + 'last-update-age': '3', + 'last-error': 'None', + 'status-flap-counts': '2', + 'established-counts': '1', + 'ORF-entry-received': '0', + 'nexthop-self': 'no', + 'nexthop-thirdparty': 'yes', + 'nexthop-peer': 'no', + 'config': {'remove-private-as': 'no'}, + 'peer-capability': { + 'list': [ + {'capability': 'Multiprotocol Extensions(1)', 'value': 'IPv4 Unicast'}, + {'capability': 'Route Refresh(2)', 'value': 'yes'}, + {'capability': '4-Byte AS Number(65)', 'value': '64512'}, + {'capability': 'Route Refresh (Cisco)(128)', 'value': 'yes'} + ] + }, + 'prefix-counter': { + 'entry': { + '@afi-safi': 'bgpAfiIpv4-unicast', + 'incoming-total': '2', + 'incoming-accepted': '2', + 'incoming-rejected': '0', + 'policy-rejected': '0', + 'outgoing-total': '0', + 'outgoing-advertised': '0' + } + } + } +} +``` + +__Returns__ + + +`dict`: BGP peers information. + ### `FirewallProxy.get_arp_table` ```python diff --git a/docs/panos-upgrade-assurance/configuration_details.mdx b/docs/panos-upgrade-assurance/configuration_details.mdx index 5cfe117..ee3f9f6 100644 --- a/docs/panos-upgrade-assurance/configuration_details.mdx +++ b/docs/panos-upgrade-assurance/configuration_details.mdx @@ -961,6 +961,7 @@ Following state areas are available: snapshots_config = [ 'nics', 'routes', + 'bgp_peers', 'license', 'arp_table', 'content_version', @@ -979,6 +980,7 @@ snapshots_config = [ snapshots_config: - nics - routes + - bgp_peers - license - arp_table - content_version @@ -1030,6 +1032,12 @@ Takes a snapshot of the Route Table (this includes routes populated from DHCP as **Method:** [`FirewallProxy.get_routes()`](/panos/docs/panos-upgrade-assurance/api/firewall_proxy#firewallproxyget_routes) +### `bgp_peers` + +Takes a snapshot of configuration of BGP peers along with their status. + +**Method:** [`FirewallProxy.get_bgp_peers()`](/panos/docs/panos-upgrade-assurance/api/firewall_proxy#firewallproxyget_bgp_peers) + ### `fib_routes` Takes a snapshot of the Forwarding table (routes that are actually taken into forwarding decisions based on routing table). @@ -1076,6 +1084,9 @@ reports = [ 'properties': ['!flags'], 'count_change_threshold': 10 }}, + {'bgp_peers': { + 'properties': ['status'] + }}, 'content_version', {'session_stats': { 'thresholds': [ @@ -1113,6 +1124,9 @@ reports: properties: - "!flags" count_change_threshold: 10 + - bgp_peers: + properties: + - "status" - content_version - session_stats: thresholds: @@ -1409,6 +1423,58 @@ reports: ``` +### `bgp_peers` + +Compares configuration and the status of BGP peers. + +**Method:** [`SnapshotCompare.get_diff_and_threshold()`](/panos/docs/panos-upgrade-assurance/api/snapshot_compare#snapshotcompareget_diff_and_threshold) + +**Configuration parameters** + +parameter | description +--- | --- +`properties` | (optional) a set of properties to skip when comparing two BGP peers, all properties are checked when this parameter is skipped +`count_change_threshold` | (optional) maximum difference percentage of changed entries in BGP peers in both snapshots, skipped when this property is not specified + +**Sample configuration** + +The following configuration compares the status of BGP peers as +captured in snapshots. + +This report produces the standardized dictionary. + +```mdx-code-block + + +``` + +```python showLineNumbers +reports = [ + { + 'bgp_peers': { + 'properties': ['status'] + } + } +] +``` + +```mdx-code-block + + +``` + +```yaml showLineNumbers +reports: + - bgp_peers: + properties: + - 'status' +``` + +```mdx-code-block + + +``` + ### `fib_routes` Provides a report on differences between FIB Table entries. It includes: diff --git a/examples/low_level_methods/run_low_level_methods.py b/examples/low_level_methods/run_low_level_methods.py index 84e95fc..198399c 100755 --- a/examples/low_level_methods/run_low_level_methods.py +++ b/examples/low_level_methods/run_low_level_methods.py @@ -94,6 +94,8 @@ print(f"\n routes information\n{firewall.get_routes()}") + print(f"\n BGP peers information\n{firewall.get_bgp_peers()}") + print(f"\n arp entries information\n{firewall.get_arp_table()}") print(f"\n session information\n{firewall.get_session_stats()}") diff --git a/examples/readiness_checks/run_readiness_snapshot.py b/examples/readiness_checks/run_readiness_snapshot.py index ae52d51..ebc289b 100755 --- a/examples/readiness_checks/run_readiness_snapshot.py +++ b/examples/readiness_checks/run_readiness_snapshot.py @@ -83,6 +83,7 @@ "nics", "routes", "fib_routes", + "bgp_peers", "license", "arp_table", "content_version", diff --git a/examples/report/fw1.snapshot b/examples/report/fw1.snapshot index 63f8496..3d11174 100644 --- a/examples/report/fw1.snapshot +++ b/examples/report/fw1.snapshot @@ -67,6 +67,66 @@ "route-table": "unicast" } }, + "bgp_peers": { + "default_Peer-Group1_Peer1": { + "@peer": "Peer1", + "@vr": "default", + "peer-group": "Peer-Group1", + "peer-router-id": "169.254.8.2", + "remote-as": "64512", + "status": "Established", + "status-duration": "3804", + "password-set": "no", + "passive": "no", + "multi-hop-ttl": "2", + "peer-address": "169.254.8.2:35355", + "local-address": "169.254.8.1:179", + "reflector-client": "not-client", + "same-confederation": "no", + "aggregate-confed-as": "yes", + "peering-type": "Unspecified", + "connect-retry-interval": "15", + "open-delay": "0", + "idle-hold": "15", + "prefix-limit": "5000", + "holdtime": "30", + "holdtime-config": "30", + "keepalive": "10", + "keepalive-config": "10", + "msg-update-in": "2", + "msg-update-out": "1", + "msg-total-in": "385", + "msg-total-out": "442", + "last-update-age": "3", + "last-error": null, + "status-flap-counts": "2", + "established-counts": "1", + "ORF-entry-received": "0", + "nexthop-self": "no", + "nexthop-thirdparty": "yes", + "nexthop-peer": "no", + "config": {"remove-private-as": "no"}, + "peer-capability": { + "list": [ + {"capability": "Multiprotocol Extensions(1)", "value": "IPv4 Unicast"}, + {"capability": "Route Refresh(2)", "value": "yes"}, + {"capability": "4-Byte AS Number(65)", "value": "64512"}, + {"capability": "Route Refresh (Cisco)(128)", "value": "yes"} + ] + }, + "prefix-counter": { + "entry": { + "@afi-safi": "bgpAfiIpv4-unicast", + "incoming-total": "2", + "incoming-accepted": "2", + "incoming-rejected": "0", + "policy-rejected": "0", + "outgoing-total": "0", + "outgoing-advertised": "0" + } + } + } + }, "session_stats": { "tmo-5gcdelete": "15", "tmo-sctpshutdown": "60", diff --git a/examples/report/fw2.snapshot b/examples/report/fw2.snapshot index fb6c43c..68e8b2c 100644 --- a/examples/report/fw2.snapshot +++ b/examples/report/fw2.snapshot @@ -54,6 +54,49 @@ "route-table": "unicast" } }, + "bgp_peers": { + "default_Peer-Group1_Peer1": { + "@peer": "Peer1", + "@vr": "default", + "peer-group": "Peer-Group1", + "peer-router-id": "169.254.8.2", + "remote-as": "64512", + "status": "Idle", + "status-duration": "0", + "password-set": "no", + "passive": "no", + "multi-hop-ttl": "2", + "peer-address": "169.254.8.2", + "local-address": "169.254.8.1", + "reflector-client": "not-client", + "same-confederation": "no", + "aggregate-confed-as": "yes", + "peering-type": "Unspecified", + "connect-retry-interval": "15", + "open-delay": "0", + "idle-hold": "15", + "prefix-limit": "5000", + "holdtime": "0", + "holdtime-config": "30", + "keepalive": "0", + "keepalive-config": "10", + "msg-update-in": "0", + "msg-update-out": "0", + "msg-total-in": "0", + "msg-total-out": "0", + "last-update-age": "0", + "last-error": null, + "status-flap-counts": "0", + "established-counts": "0", + "ORF-entry-received": "0", + "nexthop-self": "no", + "nexthop-thirdparty": "yes", + "nexthop-peer": "no", + "config": {"remove-private-as": "no"}, + "peer-capability": null, + "prefix-counter": null + } + }, "session_stats": { "tmo-5gcdelete": "15", "tmo-sctpshutdown": "60", diff --git a/examples/report/snapshot_load_compare.py b/examples/report/snapshot_load_compare.py index 7f760de..5e0feb6 100755 --- a/examples/report/snapshot_load_compare.py +++ b/examples/report/snapshot_load_compare.py @@ -21,6 +21,8 @@ def load_snap(fname: str) -> dict: {"nics": {"count_change_threshold": 10}}, {"license": {"properties": ["!serial"]}}, {"routes": {"properties": ["!flags"], "count_change_threshold": 10}}, + {"bgp_peers": {"properties": ["status"]}}, + "!fib_routes", "!content_version", { "session_stats": { diff --git a/panos_upgrade_assurance/check_firewall.py b/panos_upgrade_assurance/check_firewall.py index 31061cd..43edc1c 100644 --- a/panos_upgrade_assurance/check_firewall.py +++ b/panos_upgrade_assurance/check_firewall.py @@ -79,6 +79,7 @@ def __init__(self, node: FirewallProxy, skip_force_locale: Optional[bool] = Fals self._snapshot_method_mapping = { SnapType.NICS: self._node.get_nics, SnapType.ROUTES: self._node.get_routes, + SnapType.BGP_PEERS: self._node.get_bgp_peers, SnapType.LICENSE: self._node.get_licenses, SnapType.ARP_TABLE: self._node.get_arp_table, SnapType.CONTENT_VERSION: self.get_content_db_version, diff --git a/panos_upgrade_assurance/firewall_proxy.py b/panos_upgrade_assurance/firewall_proxy.py index ef1d363..28f9cae 100644 --- a/panos_upgrade_assurance/firewall_proxy.py +++ b/panos_upgrade_assurance/firewall_proxy.py @@ -617,6 +617,102 @@ def get_routes(self) -> dict: return result + def get_bgp_peers(self) -> dict: + """Get information about BGP peers and their status. + + The actual API command is ``. + + In the returned `dict` the key is made of three route properties delimited with an underscore (`_`) in the following + order: + + * virtual router name, + * peer group name, + * peer name. + + The key does not provide any meaningful information, it's there only to introduce uniqueness for each entry. All + properties that make a key are also available in the value of a dictionary element. + + ```python showLineNumbers title="Sample output" + { + 'default_Peer-Group1_Peer1': { + '@peer': 'Peer1', + '@vr': 'default', + 'peer-group': 'Peer-Group1', + 'peer-router-id': '169.254.8.2', + 'remote-as': '64512', + 'status': 'Established', + 'status-duration': '3804', + 'password-set': 'no', + 'passive': 'no', + 'multi-hop-ttl': '2', + 'peer-address': '169.254.8.2:35355', + 'local-address': '169.254.8.1:179', + 'reflector-client': 'not-client', + 'same-confederation': 'no', + 'aggregate-confed-as': 'yes', + 'peering-type': 'Unspecified', + 'connect-retry-interval': '15', + 'open-delay': '0', + 'idle-hold': '15', + 'prefix-limit': '5000', + 'holdtime': '30', + 'holdtime-config': '30', + 'keepalive': '10', + 'keepalive-config': '10', + 'msg-update-in': '2', + 'msg-update-out': '1', + 'msg-total-in': '385', + 'msg-total-out': '442', + 'last-update-age': '3', + 'last-error': 'None', + 'status-flap-counts': '2', + 'established-counts': '1', + 'ORF-entry-received': '0', + 'nexthop-self': 'no', + 'nexthop-thirdparty': 'yes', + 'nexthop-peer': 'no', + 'config': {'remove-private-as': 'no'}, + 'peer-capability': { + 'list': [ + {'capability': 'Multiprotocol Extensions(1)', 'value': 'IPv4 Unicast'}, + {'capability': 'Route Refresh(2)', 'value': 'yes'}, + {'capability': '4-Byte AS Number(65)', 'value': '64512'}, + {'capability': 'Route Refresh (Cisco)(128)', 'value': 'yes'} + ] + }, + 'prefix-counter': { + 'entry': { + '@afi-safi': 'bgpAfiIpv4-unicast', + 'incoming-total': '2', + 'incoming-accepted': '2', + 'incoming-rejected': '0', + 'policy-rejected': '0', + 'outgoing-total': '0', + 'outgoing-advertised': '0' + } + } + } + } + ``` + + # Returns + + dict: BGP peers information. + + """ + + response = self.op_parser(cmd="show routing protocol bgp peer") + + result = {} + if "entry" in response: + bgp_peers = response["entry"] + for peer in bgp_peers if isinstance(bgp_peers, list) else [bgp_peers]: + result[ + f"{peer['@vr'].replace(' ', '-')}_{peer['peer-group'].replace(' ', '-')}_{peer['@peer'].replace(' ', '-')}" + ] = dict(peer) + + return result + def get_arp_table(self) -> dict: """Get the currently available ARP table entries. diff --git a/panos_upgrade_assurance/snapshot_compare.py b/panos_upgrade_assurance/snapshot_compare.py index 6be7d2d..3936317 100644 --- a/panos_upgrade_assurance/snapshot_compare.py +++ b/panos_upgrade_assurance/snapshot_compare.py @@ -60,6 +60,7 @@ def __init__( self._functions_mapping = { SnapType.NICS: self.get_diff_and_threshold, SnapType.ROUTES: self.get_diff_and_threshold, + SnapType.BGP_PEERS: self.get_diff_and_threshold, SnapType.LICENSE: self.get_diff_and_threshold, SnapType.ARP_TABLE: self.get_diff_and_threshold, SnapType.CONTENT_VERSION: self.get_diff_and_threshold, diff --git a/panos_upgrade_assurance/utils.py b/panos_upgrade_assurance/utils.py index 71fce4d..89e56d2 100644 --- a/panos_upgrade_assurance/utils.py +++ b/panos_upgrade_assurance/utils.py @@ -44,6 +44,7 @@ class SnapType: NICS = "nics" ROUTES = "routes" + BGP_PEERS = "bgp_peers" LICENSE = "license" ARP_TABLE = "arp_table" CONTENT_VERSION = "content_version" diff --git a/tests/snapshots.py b/tests/snapshots.py index 2de50aa..4a711c0 100644 --- a/tests/snapshots.py +++ b/tests/snapshots.py @@ -67,6 +67,66 @@ "route-table": "unicast", }, }, + "bgp_peers": { + "default_Peer-Group1_Peer1": { + "@peer": "Peer1", + "@vr": "default", + "peer-group": "Peer-Group1", + "peer-router-id": "169.254.8.2", + "remote-as": "64512", + "status": "Established", + "status-duration": "3804", + "password-set": "no", + "passive": "no", + "multi-hop-ttl": "2", + "peer-address": "169.254.8.2:35355", + "local-address": "169.254.8.1:179", + "reflector-client": "not-client", + "same-confederation": "no", + "aggregate-confed-as": "yes", + "peering-type": "Unspecified", + "connect-retry-interval": "15", + "open-delay": "0", + "idle-hold": "15", + "prefix-limit": "5000", + "holdtime": "30", + "holdtime-config": "30", + "keepalive": "10", + "keepalive-config": "10", + "msg-update-in": "2", + "msg-update-out": "1", + "msg-total-in": "385", + "msg-total-out": "442", + "last-update-age": "3", + "last-error": None, + "status-flap-counts": "2", + "established-counts": "1", + "ORF-entry-received": "0", + "nexthop-self": "no", + "nexthop-thirdparty": "yes", + "nexthop-peer": "no", + "config": {"remove-private-as": "no"}, + "peer-capability": { + "list": [ + {"capability": "Multiprotocol Extensions(1)", "value": "IPv4 Unicast"}, + {"capability": "Route Refresh(2)", "value": "yes"}, + {"capability": "4-Byte AS Number(65)", "value": "64512"}, + {"capability": "Route Refresh (Cisco)(128)", "value": "yes"}, + ] + }, + "prefix-counter": { + "entry": { + "@afi-safi": "bgpAfiIpv4-unicast", + "incoming-total": "2", + "incoming-accepted": "2", + "incoming-rejected": "0", + "policy-rejected": "0", + "outgoing-total": "0", + "outgoing-advertised": "0", + } + }, + } + }, "session_stats": { "tmo-5gcdelete": "15", "tmo-sctpshutdown": "60", @@ -277,6 +337,49 @@ "route-table": "unicast", }, }, + "bgp_peers": { + "default_Peer-Group1_Peer1": { + "@peer": "Peer1", + "@vr": "default", + "peer-group": "Peer-Group1", + "peer-router-id": "169.254.8.2", + "remote-as": "64512", + "status": "Idle", + "status-duration": "0", + "password-set": "no", + "passive": "no", + "multi-hop-ttl": "2", + "peer-address": "169.254.8.2", + "local-address": "169.254.8.1", + "reflector-client": "not-client", + "same-confederation": "no", + "aggregate-confed-as": "yes", + "peering-type": "Unspecified", + "connect-retry-interval": "15", + "open-delay": "0", + "idle-hold": "15", + "prefix-limit": "5000", + "holdtime": "0", + "holdtime-config": "30", + "keepalive": "0", + "keepalive-config": "10", + "msg-update-in": "0", + "msg-update-out": "0", + "msg-total-in": "0", + "msg-total-out": "0", + "last-update-age": "0", + "last-error": None, + "status-flap-counts": "0", + "established-counts": "0", + "ORF-entry-received": "0", + "nexthop-self": "no", + "nexthop-thirdparty": "yes", + "nexthop-peer": "no", + "config": {"remove-private-as": "no"}, + "peer-capability": None, + "prefix-counter": None, + } + }, "session_stats": { "tmo-5gcdelete": "15", "tmo-sctpshutdown": "60", diff --git a/tests/test_firewall_proxy.py b/tests/test_firewall_proxy.py index 5038fb4..409d7eb 100644 --- a/tests/test_firewall_proxy.py +++ b/tests/test_firewall_proxy.py @@ -562,6 +562,144 @@ def test_get_routes_nexthop_name(self, fw_proxy_mock): }, } + def test_get_bgp_peers(self, fw_proxy_mock): + xml_text = """ + + + + Peer-Group1 + 169.254.8.2 + 64512 + Established + 3804 + no + no + 2 + 169.254.8.2:35355 + 169.254.8.1:179 + not-client + no + yes + Unspecified + 15 + 0 + 15 + 5000 + 30 + 30 + 10 + 10 + 2 + 1 + 385 + 442 + 3 + + 2 + 1 + 0 + no + yes + no + + no + + + + Multiprotocol Extensions(1) + IPv4 Unicast + + + Route Refresh(2) + yes + + + 4-Byte AS Number(65) + 64512 + + + Route Refresh (Cisco)(128) + yes + + + + + 2 + 2 + 0 + 0 + 0 + 0 + + + + + + """ + raw_response = ET.fromstring(xml_text) + fw_proxy_mock.op.return_value = raw_response + + assert fw_proxy_mock.get_bgp_peers() == { + "default_Peer-Group1_Peer1": { + "@peer": "Peer1", + "@vr": "default", + "peer-group": "Peer-Group1", + "peer-router-id": "169.254.8.2", + "remote-as": "64512", + "status": "Established", + "status-duration": "3804", + "password-set": "no", + "passive": "no", + "multi-hop-ttl": "2", + "peer-address": "169.254.8.2:35355", + "local-address": "169.254.8.1:179", + "reflector-client": "not-client", + "same-confederation": "no", + "aggregate-confed-as": "yes", + "peering-type": "Unspecified", + "connect-retry-interval": "15", + "open-delay": "0", + "idle-hold": "15", + "prefix-limit": "5000", + "holdtime": "30", + "holdtime-config": "30", + "keepalive": "10", + "keepalive-config": "10", + "msg-update-in": "2", + "msg-update-out": "1", + "msg-total-in": "385", + "msg-total-out": "442", + "last-update-age": "3", + "last-error": None, + "status-flap-counts": "2", + "established-counts": "1", + "ORF-entry-received": "0", + "nexthop-self": "no", + "nexthop-thirdparty": "yes", + "nexthop-peer": "no", + "config": {"remove-private-as": "no"}, + "peer-capability": { + "list": [ + {"capability": "Multiprotocol Extensions(1)", "value": "IPv4 Unicast"}, + {"capability": "Route Refresh(2)", "value": "yes"}, + {"capability": "4-Byte AS Number(65)", "value": "64512"}, + {"capability": "Route Refresh (Cisco)(128)", "value": "yes"}, + ] + }, + "prefix-counter": { + "entry": { + "@afi-safi": "bgpAfiIpv4-unicast", + "incoming-total": "2", + "incoming-accepted": "2", + "incoming-rejected": "0", + "policy-rejected": "0", + "outgoing-total": "0", + "outgoing-advertised": "0", + } + }, + } + } + def test_get_arp_table(self, fw_proxy_mock): xml_text = """ diff --git a/tests/test_snapshot_compare.py b/tests/test_snapshot_compare.py index c7d38b3..d2337ed 100644 --- a/tests/test_snapshot_compare.py +++ b/tests/test_snapshot_compare.py @@ -414,6 +414,30 @@ def test_get_count_change_percentage(self, thresholds, expected_result): } }, ), + ( + [{"bgp_peers": {"properties": ["status"]}}], + { + "bgp_peers": { + "added": {"added_keys": [], "passed": True}, + "changed": { + "changed_raw": { + "default_Peer-Group1_Peer1": { + "added": {"added_keys": [], "passed": True}, + "changed": { + "changed_raw": {"status": {"left_snap": "Established", "right_snap": "Idle"}}, + "passed": False, + }, + "missing": {"missing_keys": [], "passed": True}, + "passed": False, + } + }, + "passed": False, + }, + "missing": {"missing_keys": [], "passed": True}, + "passed": False, + } + }, + ), ( ["arp_table"], {