From 8af060352497e9d74f81aca790a448ba4f4661bf Mon Sep 17 00:00:00 2001
From: "th.l" <thl-cmk@outlook.com>
Date: Tue, 10 May 2022 19:04:06 +0200
Subject: [PATCH] update project

---
 agent_based/bgp_peer.py           |  54 +++--
 agent_based/inv_bgp_peer.py       |  75 +++++--
 agent_based/utils/bgp_peer.py     | 359 +++++++++++++++++++++++++++---
 bgp_peer.mkp                      | Bin 10108 -> 15331 bytes
 packages/bgp_peer                 |   7 +-
 web/plugins/metrics/bgp_peer.py   |  33 ++-
 web/plugins/views/inv_bgp_peer.py |  34 ++-
 web/plugins/wato/bgp_peer.py      |  69 +++++-
 web/plugins/wato/inv_bgp_peer.py  | 107 +++++++++
 9 files changed, 663 insertions(+), 75 deletions(-)
 create mode 100644 web/plugins/wato/inv_bgp_peer.py

diff --git a/agent_based/bgp_peer.py b/agent_based/bgp_peer.py
index a4bf779..0f10cc6 100644
--- a/agent_based/bgp_peer.py
+++ b/agent_based/bgp_peer.py
@@ -28,8 +28,10 @@
 # 2021-11-14: merged check function with cisco_bgp_peer
 #             moved parse function to utils/bgp_peer
 # 2021-04-02: rewritten bgp neighbor state handling (made configurable)
+# 2022-04-29: added upper/lower prefix limits from wato
+#             added info if device is admin prefix limit capable (device_admin_limit)
+# 2022-05-09: made item name configurable (don't use address-family/routing-instance/VRF)
 #
-# ToDo: make check/discovery function the base for huawei_bgp_peer
 
 # Example Agent Output:
 # BGP4-MIB
@@ -85,9 +87,20 @@ def parse_bgp_peer(string_table: StringTable) -> Optional[Dict[str, BgpPeer]]:
     return peer_table
 
 
-def discovery_bgp_peer(section: Dict[str, BgpPeer]) -> DiscoveryResult:
+def discovery_bgp_peer(params, section: Dict[str, BgpPeer]) -> DiscoveryResult:
+    _item_parts = [
+        'remote_address',
+        'address_family',
+        'routing_instance',
+    ]
     for key in section.keys():
-        yield Service(item=key)
+        parameters = {'internal_item': key}
+        item = ''
+        for item_part in _item_parts:
+            if item_part not in params['build_item']:
+                item += f'{section[key].item[item_part]} '
+        item = item.strip(' ')
+        yield Service(item=item, parameters=parameters)
 
 
 def check_bgp_peer(item, params, section: Dict[str, BgpPeer]) -> CheckResult:
@@ -101,6 +114,8 @@ def check_bgp_peer(item, params, section: Dict[str, BgpPeer]) -> CheckResult:
         '6': 0,  # established
     }
 
+    item = params.get('internal_item', item)
+
     neighborstate.update(params.get('neighborstate', neighborstate))  # update neighbor status with params
 
     peer_not_found_state = params['peernotfound']
@@ -135,26 +150,27 @@ def check_bgp_peer(item, params, section: Dict[str, BgpPeer]) -> CheckResult:
     if peer.peer_unavail_reason != 0:  # huawei peer unavailable state
         yield Result(state=State.CRIT, notice=F'Peer unavailable reason: {peer.peer_unavail_reason_str}')
 
-    acceptedprefixes = peer.accepted_prefixes
-    prefixadminlimit = peer.prefix_admin_limit
-    prefixthreshold = peer.prefix_threshold
-    warnthreshold = None
-
-    if prefixadminlimit is not None and prefixthreshold is not None:
-        warnthreshold = int(prefixadminlimit / 100.0 * prefixthreshold)  # use float (100.0) to get xx.xx in division
-    elif acceptedprefixes is not None:
+    if peer.device_admin_limit and peer.prefix_admin_limit is None:
         yield Result(
             state=State(params['noprefixlimit']),
-            notice='Prefix limit/warn threshold: not configured on the device.',
+            notice='Prefix limit/warn threshold not configured on the device.',
         )
-        warnthreshold = None
 
     if peer.admin_state == 2:  # no perfdata if admin shutdown
-        if acceptedprefixes is not None:
+        acceptedprefixes = peer.accepted_prefixes
+        prefixadminlimit = peer.prefix_admin_limit
+        prefixthreshold = peer.prefix_threshold
+        warnthreshold = None
+
+        if prefixadminlimit is not None and prefixthreshold is not None:
+            warnthreshold = int(
+                prefixadminlimit / 100.0 * prefixthreshold)  # use float (100.0) to get xx.xx in division
+        if acceptedprefixes is not None and peer.peer_state == 6:  # peer established and prefixes accepted
             yield from check_levels(
                 value=acceptedprefixes,
                 metric_name='bgp_peer_acceptedprefixes',
-                levels_upper=(warnthreshold, prefixadminlimit),
+                levels_upper=params.get('accepted_prefixes_upper_levels', (warnthreshold, prefixadminlimit)),
+                levels_lower=params.get('accepted_prefixes_lower_levels'),
                 label='Prefixes accepted',
                 render_func=lambda v: f'{v}'
             )
@@ -200,6 +216,14 @@ register.check_plugin(
     name='bgp_peer',
     service_name='BGP peer %s',
     discovery_function=discovery_bgp_peer,
+    discovery_default_parameters={
+        'build_item': [
+       #     'remote_address',
+       #     'address_family',
+       #     'routing_instance',
+        ]
+    },
+    discovery_ruleset_name='discovery_bgp_peer',
     check_function=check_bgp_peer,
     check_default_parameters={
         'minuptime': (7200, 3600),
diff --git a/agent_based/inv_bgp_peer.py b/agent_based/inv_bgp_peer.py
index 52b7086..d273b4b 100644
--- a/agent_based/inv_bgp_peer.py
+++ b/agent_based/inv_bgp_peer.py
@@ -9,27 +9,33 @@
 #
 # inventory of bgp peers
 #
+# 2022-04-30: code cleanup/streamlining
 #
 
+import time
 from typing import List
+
 from cmk.base.plugins.agent_based.agent_based_api.v1 import (
     register,
     SNMPTree,
     TableRow,
     exists,
+    OIDBytes
 )
-
 from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import (
-    StringTable,
+    StringByteTable,
     InventoryResult,
 )
 from cmk.base.plugins.agent_based.utils.bgp_peer import (
-    ByteToHex,
-    bgp_errors,
+    bgp_error_code_as_hex,
+    bgp_error_as_string,
+    InvBgpPeer,
+    get_bgp_type,
+    BgpWhois,
 )
 
 
-def parse_inv_bgp_peer(string_table: List[StringTable]):
+def parse_inv_bgp_peer(string_table: List[StringByteTable]):
     peers, base = string_table
     try:
         local_as, local_id = base[0]
@@ -41,11 +47,11 @@ def parse_inv_bgp_peer(string_table: List[StringTable]):
 
     for entry in peers:
         try:
-            remote_id,  version, local_addr, remote_addr, remote_as, last_error = entry
+            remote_id,  state, version, local_addr, remote_addr, remote_as, last_error, fsm_established_time = entry
         except ValueError:
             return
 
-        bgp_peers.append({
+        bgp_peer: InvBgpPeer = {
             'remote_addr': remote_addr,
             'remote_id': remote_id,
             'version': version,
@@ -53,20 +59,30 @@ def parse_inv_bgp_peer(string_table: List[StringTable]):
             'remote_as': remote_as,
             'local_as': local_as,
             'local_id': local_id,
-            'bgp_type': 'iBGP' if local_as == remote_as else 'eBGP',
-            'status_columns': {
-                'last_error_code': ByteToHex(last_error),
-                'last_error': bgp_errors(last_error),
-            },
-        })
+            'bgp_type': get_bgp_type(local_as, remote_as),
+            'fsm_established_time': int(fsm_established_time),
+            'peer_state': 1 if state == '6' else 2,  # adjust to match if_oper_status for inventory painter
+            'last_error_code': bgp_error_code_as_hex(last_error),
+            'last_error': bgp_error_as_string(last_error),
+            'status_columns': {},
+            'address_family': 'N/A'
+        }
+
+        bgp_peers.append(bgp_peer)
+
     return bgp_peers
 
 
 def inventory_bgp_peers(params, section) -> InventoryResult:
     path = ['networking', 'bgp_peers']
+    whois = None
+    if params.get('whois_enable'):
+        whois = BgpWhois(
+            default_rir=params['whois_enable'].get('whois_rir', 'https://rdap.db.ripe.net'),
+            timeout=params['whois_enable'].get('whois_timeout', 5)
+        )
 
     for bgp_peer in section:
-
         key_columns = {'remote_addr': bgp_peer['remote_addr']}
 
         for key in key_columns.keys():
@@ -75,6 +91,29 @@ def inventory_bgp_peers(params, section) -> InventoryResult:
         status_columns = bgp_peer['status_columns']
         bgp_peer.pop('status_columns')
 
+        if whois:
+            as_info = whois.get_whois_data_by_asn(int(bgp_peer.get('remote_as')))
+            bgp_peer.update(as_info)
+
+        for column in params.get('remove_columns', []):
+            try:
+                bgp_peer.pop(column)
+            except KeyError:
+                pass
+
+        fsm_established_time = bgp_peer.get('fsm_established_time')
+        if fsm_established_time:
+            bgp_peer.pop('fsm_established_time')
+            peer_state = bgp_peer.get('peer_state')
+            in_service = True
+            not_in_service_time = params.get('not_in_service_time', 2592000)
+            if peer_state == 2:  # not established
+                if fsm_established_time >= not_in_service_time:
+                    in_service = False
+
+            bgp_peer.update({'in_service': in_service})
+            bgp_peer.update({'last_change': time.time() - fsm_established_time})
+
         yield TableRow(
             path=path,
             key_columns=key_columns,
@@ -82,6 +121,9 @@ def inventory_bgp_peers(params, section) -> InventoryResult:
             status_columns=status_columns
         )
 
+    if whois:
+        del whois
+
 
 register.snmp_section(
     name='inv_bgp_peer',
@@ -91,11 +133,13 @@ register.snmp_section(
             base='.1.3.6.1.2.1.15.3.1',  # BGP4-MIB::BgpPeerEntry
             oids=[
                 '1',  # bgpPeerIdentifier
+                '2',  # bgpPeerState
                 '4',  # bgpPeerNegotiatedVersion
                 '5',  # bgpPeerLocalAddr
                 '7',  # bgpPeerRemoteAddr
                 '9',  # bgpPeerRemoteAs
-                '14',  # bgpPeerLastError
+                OIDBytes('14'),  # bgpPeerLastError
+                '16',  # bgpPeerFsmEstablishedTime
             ]
         ),
         SNMPTree(
@@ -113,6 +157,7 @@ register.inventory_plugin(
     name='inv_bgp_peer',
     inventory_function=inventory_bgp_peers,
     inventory_default_parameters={
+        'not_in_service_time': 2592000,  # 30 days in seconds
     },
     inventory_ruleset_name='inv_bgp_peer',
 )
diff --git a/agent_based/utils/bgp_peer.py b/agent_based/utils/bgp_peer.py
index f48d764..0284eb0 100644
--- a/agent_based/utils/bgp_peer.py
+++ b/agent_based/utils/bgp_peer.py
@@ -7,16 +7,27 @@
 # URL   : https://thl-cmk.hopto.org
 # Date  : 2021-08-29
 #
-# include file, will be used with bgp_peer, inv_bgp_peer, cisco_bgp_peer, inv_cisco_bgp_peer
+# include file, will be used with (inv_)bgp_peer, (inv_)cisco_bgp_peer, (inv_)juniper_bgp_peer, huawei_bgp_peer
 #
 # 2022-04-17: added peer_unavail_reason/peer_unavail_reason_str for huawei bgp peers
+# 2022-04-29: added device_admin_limit
+# 2022-04-30: code cleanup/streamlining
+# 2022-05-09: added item to BgpPeer class, this is used in the discovery function
 #
 
+import requests
+import json
 import re
-from typing import List, Tuple, Optional, Dict
+import ipaddress
+from typing import List, Tuple, Optional, Dict, TypedDict
 from dataclasses import dataclass
 
 
+class BgpPeerItem(TypedDict):
+    remote_address: str
+    address_family: str
+    routing_instance: str
+
 @dataclass
 class BgpPeer:
     peer_state: int
@@ -26,6 +37,8 @@ class BgpPeer:
     fsm_established_time: int
     metric_rate: List[Tuple[str, int]]
     metric_count: List[Tuple[str, int]]
+    item: BgpPeerItem
+    device_admin_limit: Optional[bool]
     prefix_admin_limit: Optional[int]
     prefix_threshold: Optional[int]
     prefix_clear_threshold: Optional[int]
@@ -34,6 +47,23 @@ class BgpPeer:
     peer_unavail_reason_str: Optional[str]
 
 
+class InvBgpPeer(TypedDict):
+    remote_addr: str
+    remote_id: str
+    version: str
+    local_addr:str
+    remote_as: str
+    local_as: str
+    local_id: str
+    bgp_type: str
+    fsm_established_time: int
+    peer_state: int
+    last_error_code: str
+    last_error: str
+    status_columns: Dict[str, str]
+    address_family:  Optional[str]
+
+
 def sec2hr(seconds):
     m, s = divmod(seconds, 60)
     h, m = divmod(m, 60)
@@ -65,13 +95,24 @@ def bgp_adminstate(st):
     return names.get(st, st)
 
 
-def ByteToHex(byteStr):
-    return ''.join(['%02X ' % ord(x) for x in byteStr]).strip()
+def get_bgp_type(local_as: str, remote_as: str) -> str:
+    bgp_type = 'N/A'
+    if local_as.isdigit() and remote_as.isdigit():
+        if local_as == remote_as:
+            bgp_type = 'iBGP'
+        else:
+            bgp_type = 'eBGP'
 
+    return bgp_type
 
-def bgp_errors(bytestring):
-    byte1, byte2 = ByteToHex(bytestring).split()
 
+def bgp_error_code_as_hex(error_code: List[int]):
+    return ''.join([f'{m:02x}' for m in error_code])
+
+
+def bgp_error_as_string(error_code: List[int]):
+    # byte1, byte2 = ByteToHex(bytestring).split()
+    main_code, sub_code = error_code
     names = {}
     names[0] = {0: 'NO ERROR'}
     names[1] = {
@@ -115,14 +156,10 @@ def bgp_errors(bytestring):
         4: 'Connection Rejected',
         5: 'Other Configuration Change',
     }
-    return names[int(byte1, 16)].get(int(byte2, 16))
-
+    return names[main_code].get(sub_code)
 
-def bgp_render_ipv4_address(bytestring):
-    return '.'.join([f'{ord(m)}' for m in bytestring])
 
-
-def bgp_shorten_ipv6_adress(address):
+def bgp_shorten_ipv6_address(address):
     address = address.split(':')
     span = 2
     address = [''.join(address[i:i + span]) for i in range(0, len(address), span)]
@@ -139,35 +176,38 @@ def bgp_shorten_ipv6_adress(address):
     return address
 
 
-def bgp_render_ipv6_address(bytestring):
-    address = ":".join(["%02s" % hex(ord(m))[2:] for m in bytestring]).replace(' ', '0').upper()
-    address = bgp_shorten_ipv6_adress(address)
-
-    return address
-
-
-def bgp_render_ip_address(bytestring):
-    if len(bytestring) == 4:
-        return bgp_render_ipv4_address(bytestring)
-    elif len(bytestring) == 16:
-        return bgp_render_ipv6_address(bytestring)
+def bgp_render_ip_address(addr_type: str, addr: List[int]) -> str:
+    if addr_type == '1':
+        return '.'.join([str(m) for m in addr])
+    elif addr_type == '2':
+        # IPv6 address from snmp oid in decimal
+        # [10, 1, 7, 40, 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, 5, 25]
+        # change to hex with leading zero
+        # ['0a', '01', '07', '28', '00', '00', '32', '00', '00', '00', '00', '00', '00', '00', '05', '19']
+        remote_address = ':'.join([f'{m:02x}' for m in addr]).split(':')
+        # convert to long ipv6 address 0a01:0728:0000:3200:0000:0000:0000:0519
+        remote_address = ':'.join([''.join(remote_address[i:i + 2]) for i in range(0, len(remote_address), 2)])
+        # convert to short ipv6 address a01:728:0:3200::519
+        remote_address = str(ipaddress.ip_address(remote_address))
+        # replace bytes in entry with ip address
+        return remote_address
     else:
-        return ''
+        return 'N/A'
 
 
-def bgp_get_peer(OID_END):
+def bgp_get_ip_address_from_oid(oid_end):
     # returns peer address string from OID_END
     # u'1.4.217.119.208.34.1.1' --> 217.119.208.34
-    # u'2.20.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.17.2.1' -->  42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.17
-    peer_ip = ''
-    OID_END = OID_END.split('.')
-    if int(OID_END[1]) == 4:  # length of ip address
-        peer_ip = '.'.join(OID_END[2:6])  # ipv4 address
-    elif int(OID_END[1]) == 16:  # ipv6 address
-        peer_ip = ':'.join('%02s' % hex(int(m))[2:] for m in OID_END[2:18]).replace(' ', '0').upper()
-        peer_ip = bgp_shorten_ipv6_adress(peer_ip)
+    # u'2.16.42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.17.2.1' -->  42.5.87.192.0.0.255.255.0.0.0.0.0.0.0.17
+    ip_address = ''
+    oid_end = oid_end.split('.')
+    if int(oid_end[1]) == 4:  # length of ip address
+        ip_address = '.'.join(oid_end[2:6])  # ipv4 address
+    elif int(oid_end[1]) == 16:  # ipv6 address
+        ip_address = ':'.join('%02s' % hex(int(m))[2:] for m in oid_end[2:18]).replace(' ', '0').upper()
+        ip_address = bgp_shorten_ipv6_address(ip_address)
 
-    return peer_ip
+    return ip_address
 
 
 def bgp_get_peer_entry(peer: List) -> Optional[Dict[str, BgpPeer]]:
@@ -185,6 +225,12 @@ def bgp_get_peer_entry(peer: List) -> Optional[Dict[str, BgpPeer]]:
         fsm_established_time=int(fsm_established_time),
         metric_count=[],
         metric_rate=[],
+        item={
+            'remote_address': remote_addr,
+            'address_family': '',
+            'routing_instance': '',
+        },
+        device_admin_limit=None,
         prefix_admin_limit=None,
         prefix_threshold=None,
         prefix_clear_threshold=None,
@@ -214,3 +260,246 @@ def bgp_get_peer_entry(peer: List) -> Optional[Dict[str, BgpPeer]]:
             pass
 
     return {remote_addr: bgp_peer}
+
+
+def _fetch_data(url: str, timeout: int) -> dict:
+    response = requests.get(
+        url=url,
+        timeout=timeout,
+    )
+    # ToDo: improve/Implement error handling
+    if response.status_code == 200:
+        return json.loads(response.text)
+    else:
+        return {}
+
+
+class BgpWhois:
+    __rdap_boot_strap = {
+        'source': 'https://data.iana.org/rdap/asn.json',
+        'description': 'RDAP bootstrap file for Autonomous System Number allocations',
+        'publication': '2021-12-07T20:00:01Z',
+        'services': [
+            [
+                [
+                    '36864-37887',
+                    '327680-328703',
+                    '328704-329727'
+                ],
+                [
+                    'https://rdap.afrinic.net/rdap/',
+                    'http://rdap.afrinic.net/rdap/'
+                ]
+            ],
+            [
+                [
+                    '4608-4865',
+                    '7467-7722',
+                    '9216-10239',
+                    '17408-18431',
+                    '23552-24575',
+                    '37888-38911',
+                    '45056-46079',
+                    '55296-56319',
+                    '58368-59391',
+                    '63488-63999',
+                    '64000-64098',
+                    '64297-64395',
+                    '131072-132095',
+                    '132096-133119',
+                    '133120-133631',
+                    '133632-134556',
+                    '134557-135580',
+                    '135581-136505',
+                    '136506-137529',
+                    '137530-138553',
+                    '138554-139577',
+                    '139578-140601',
+                    '140602-141625',
+                    '141626-142649',
+                    '142650-143673',
+                    '143674-144697',
+                    '144698-145721',
+                    '145722-146745',
+                    '146746-147769',
+                    '147770-148793',
+                    '148794-149817',
+                    '149818-150841',
+                    '150842-151865'
+                ],
+                [
+                    'https://rdap.apnic.net/'
+                ]
+            ],
+            [
+                [
+                    '1-1876',
+                    '1902-2042',
+                    '2044-2046',
+                    '2048-2106',
+                    '2137-2584',
+                    '2615-2772',
+                    '2823-2829',
+                    '2880-3153',
+                    '3354-4607',
+                    '4866-5376',
+                    '5632-6655',
+                    '6912-7466',
+                    '7723-8191',
+                    '10240-12287',
+                    '13312-15359',
+                    '16384-17407',
+                    '18432-20479',
+                    '21504-23455',
+                    '23457-23551',
+                    '25600-26623',
+                    '26624-27647',
+                    '29696-30719',
+                    '31744-32767',
+                    '32768-33791',
+                    '35840-36863',
+                    '39936-40959',
+                    '46080-47103',
+                    '53248-54271',
+                    '54272-55295',
+                    '62464-63487',
+                    '64198-64296',
+                    '393216-394239',
+                    '394240-395164',
+                    '395165-396188',
+                    '396189-397212',
+                    '397213-398236',
+                    '398237-399260',
+                    '399261-400284',
+                    '400285-401308'
+                ],
+                [
+                    'https://rdap.arin.net/registry/',
+                    'http://rdap.arin.net/registry/'
+                ]
+            ],
+            [
+                [
+                    '1877-1901',
+                    '2043',
+                    '2047',
+                    '2107-2136',
+                    '2585-2614',
+                    '2773-2822',
+                    '2830-2879',
+                    '3154-3353',
+                    '5377-5631',
+                    '6656-6911',
+                    '8192-9215',
+                    '12288-13311',
+                    '15360-16383',
+                    '20480-21503',
+                    '24576-25599',
+                    '28672-29695',
+                    '30720-31743',
+                    '33792-34815',
+                    '34816-35839',
+                    '38912-39935',
+                    '40960-41983',
+                    '41984-43007',
+                    '43008-44031',
+                    '44032-45055',
+                    '47104-48127',
+                    '48128-49151',
+                    '49152-50175',
+                    '50176-51199',
+                    '51200-52223',
+                    '56320-57343',
+                    '57344-58367',
+                    '59392-60415',
+                    '60416-61439',
+                    '61952-62463',
+                    '64396-64495',
+                    '196608-197631',
+                    '197632-198655',
+                    '198656-199679',
+                    '199680-200191',
+                    '200192-201215',
+                    '201216-202239',
+                    '202240-203263',
+                    '203264-204287',
+                    '204288-205211',
+                    '205212-206235',
+                    '206236-207259',
+                    '207260-208283',
+                    '208284-209307',
+                    '209308-210331',
+                    '210332-211355',
+                    '211356-212379',
+                    '212380-213403'
+                ],
+                [
+                    'https://rdap.db.ripe.net/'
+                ]
+            ],
+            [
+                [
+                    '27648-28671',
+                    '52224-53247',
+                    '61440-61951',
+                    '64099-64197',
+                    '262144-263167',
+                    '263168-263679',
+                    '263680-264604',
+                    '264605-265628',
+                    '265629-266652',
+                    '266653-267676',
+                    '267677-268700',
+                    '268701-269724',
+                    '269725-270748',
+                    '270749-271772',
+                    '271773-272796',
+                    '272797-273820'
+                ],
+                [
+                    'https://rdap.lacnic.net/rdap/'
+                ]
+            ]
+        ],
+        'version': '1.0'
+    }
+    __rirs = {
+        'ripe': 'https://rdap.db.ripe.net',
+        'arin': 'https://rdap.arin.net/registry',
+        'afrinic': 'https://rdap.afrinic.net/rdap',
+        'lacnic': 'https://rdap.lacnic.net/rdap',
+        'apnic': 'https://rdap.apnic.net',
+    }
+
+    def __find_rir_by_asn(self, asn: int) -> str:
+        for rir in self.__rdap_boot_strap['services']:
+            url = rir[1][0]  # https rdap url
+            for asns in rir[0]:
+                asns = asns.split('-')
+                if len(asns) == 1:
+                    asns = asns + asns
+                if int(asns[0]) <= asn <= int(asns[1]):
+                    return url
+        return self.__rir
+
+    def __init__(self, default_rir: str, timeout: int):
+        self.__timeout = timeout
+        self.__rir = self.__rirs.get(default_rir, 'https://rdap.db.ripe.net')
+        self.__known_asns = {}
+
+    def get_whois_data_by_asn(self, asn: int) -> Dict[str, str]:
+        asn_info = {}
+        # don't fetch for private ASNs, two byte or four byte ASN number
+        if asn < 64512 or (65536 < asn < 4200000000):
+            rir = self.__find_rir_by_asn(asn)
+            query = 'autnum'
+            data = _fetch_data(f'{rir}{query}/{asn}', self.__timeout)
+            if data:
+                asn_info['as_name'] = data.get('name')
+                vcard_array = data.get('entities')[0].get('vcardArray')
+                if vcard_array:
+                    for line in vcard_array[1]:
+                        if line[0] == 'fn':
+                            asn_info['as_org_name'] = line[3]
+        self.__known_asns.update(asn_info)
+        return asn_info
diff --git a/bgp_peer.mkp b/bgp_peer.mkp
index fd34868a5bb1d7cad94daad457f702246101afc8..d710de6e528646e7a828bb7a8182917ab739b04a 100644
GIT binary patch
literal 15331
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
Nz(SNhKvuv&{{zkPC_4ZE

literal 10108
zcmai)Q+FK<ptNJ#w$&t!Z8d0YJB_tt?bvE|(8jiH+qTiz&OYzCIyc{1-`)IynYEso
zA&)^o7^E^Zg94jbI~zM&TDr1wvU75>b8+yod2+DvvN`;6hPd<(bUWawH~A9wM-nP&
zX0Wu>OSSNo0j&GPkEFD6ns?aG92b$~M5j^22;!8<sx7}Lwf*ytd-r*kukL_Mgb0+o
zvk0(wj=Z45TeIOJio1fFwfE}d`nW;{p``)?a=wsh!8_Fl?%i&0TUVwn=MMr9C+;L8
zukjw;??{$}^o}rve(}ox{=U;QlCuIbCi_Q;*w1~edg^q)XS{YCyuK6jwxNQe$iuIl
z=uBM&(N0(^woH~oM-E2yX^aR=sswqDvGL$8MsnCzVNWnS6gEQ}4hYG6tN{rZK6ukx
z>7PVz0lft~&=q~ILrnf&W7<g|8X=PfcC!(uKvTmGAVLM=CfO^SFR=*37W-QiuN9dh
z(B{f4XO#=-#9ndn+s9ECek~155Ih-;=|ihOwqfFh;mF!$CjmQvGkDl)(-<PKc)}!>
zb*P^48(Bm_$NP{?AM;DWPySEHtou_k=c-PNX3M23$C94&PWFoFw5w=eA48%IXWW-O
zK|)q)a(F-*O^jD<3%#!6OusbNx~aR$toqCeM^G@v7BoPH2I=N~Sk2~gh+0AmADKXK
zoy;f0E1<JvV222og{Y;Cm_Xw&SnDHxJ7zEL`B*5bvo5M7HNJ5eIHG;Kt<_lKq2-8`
zStv@04GoyKC1W%@+jA9B+Y}yzj{?2GXz@&xjzcGy@zc6Y3i_XfeeSMCy#4UKf-Q&y
zpUlVD<g%ozdX5C?v>rUOXJSW<)CqpOtVXPl-4e`6NP|Ry3;G)d8TeMs22O>=&G}5=
zL+Sqe33!A5{PPJAi1=Lp3dDWyp}qi52VL4Xg9RO$3o-&LAMEeJzBlsWpU=DK=@&ad
z74G2;z3r+gmb#w}^`ip{%rU(da1cl`OyBEo-mPRCXc5vIWL`>5*Z_`uW%WsLSU%9<
zNo3U=v5}rtxQmc?bWZS!WE<i!viSv>;no&AH>Mc`ltEk~2xjTLmw!#S5HPB;&~jt5
z2C3q*(DF8Tf{@a>6L6T_P!iC<MfQK`)OnctYihvHXHM$iffrA^PcU~h4}VYiCjjj|
zV!!A8Jy~<QkCNrd_5-pUu$p&!9kBE$YQD3@DjZlJuS&cPPD#54J2(-_`T~q<M}Y9n
zUNF`ar`+Av9ikWYE_}Bk?~wX*3tRXvg>x@o_joo4<ByXuYg9<r)PQNB#=$=ge1GVe
zdH1282-wzEQ3-53zQF<ATVMuuX(v(Xe0RkrgU8PbdSO&ReK3`>oChp>b3#^osR1AE
zPw}6Feb6rco^_q~eL$~+IByI$xa~2hoWbw4>i{h5W$!q519^efbBv!UJ#$th_JWO6
zRdDBJS~TN4$kf5bXLNFbgA#Y?dSVaM_kHz6zb|EA@$09R34FMM$Vi}1mr<N0WVN%e
z8w97*IC9uNo5i4ZFIE=Lb7po78wwhBR%>KIAX)2`o=+85+1?5>l1?g#<0<^^h=6Ef
zX~#q%338bD7JAT#sAN$AxysHTBQTu5X^l$FjB<7L>iPW-&q20$wi=a$oi6@%3;b)g
zZ__o(_QTbD$-}VG+?(0+iIAf7LN0y6^6y|`6gL!_x81BZbdqr2Nn!UF1B}6;gX3Tv
z8v{|4tFZguHYBh}DkUrkDLgU3^WZQM{8odCrSWd`v{U=bS16^<XWsXQiC6q|KH0-o
zbXPQ<m+Oi^<ES10boMIt_S;9C<ozKb!05tL5SpOe+V?>KPWJVX27mXrifMM6!{blB
zJQPy)E-me)x5v{rgXtOq8Q!Q?!RM~rP0baKP8efDV>K>S_n_$^PFq4|gtl@Fs5Cs)
zmfHBB=;EEKOfC?nSzCU&W}=?G<DLv(lt$C|unFt@**|J%2$tRL&kMA02D%d;I3gSZ
zkC{w2K)-1kton5TVIKTuK1Vm2ahO=`dp5G`Go2g-L$z!iRC>y(==|(Snjpo~#UT(m
zI=q4UfoqN51pqPAH+OyAE&PdTYCm$d27hzS_OY`TL5}uyj}<`HS=;gM-!Tz>;qJnf
zAte+KKk20zoU1S8?CF2EFm@F5$W;DNUZK%6UsF9_Q(E6sZB3f>RaWZrBAannpScPA
zG#V)*idD|r=17-hHy6gJXR;%;Hmv$A*ON~BniRb-GBnQ4{)CBp)Gev+14=CAY#s@Z
z9HTuMlXRYXiuPE+3lhm7{^;a)nkAFCYweY%&FQ%tHgTJl7j6rX4BBNPr;lUR9&heO
zTlgYtLq<<)L()#+Nv94#uZBz*^t{JSAnSFGA_oZ>y+yrbSzV06`(BS=;aGQl++SP{
zakujB<AWg(XlI30WK@x!u~pdiM&xnARG+^39zlLMK)Y0vl4r;dR=ou7-Il_Rjb!_W
z$@REp`nq1Y>btp674sCrnpBzj4&@HGHy|?wb!(5zR5K}Lp@u`z$Ru50r|~XYSZ^%F
zC>9K{_~$7mu?<&{T3&^^<!WqHq-ux;ihHu2{j*b9=w@#3CWO5{x4W-kZgDAMIjZaD
zZB%yNe390iG#8QF4As0c%8y8g`5t}7T3)3+_+eX_<<>5PAn#)(*p=jhES3T>=oBG2
z*6?9?<uvo&Xkl9A!*KUR&j(@rEu2S(7eZcr!i)qm_#M^hh+@#qD~8yjN#t1~R&dP*
zpTrf(-*8kKTPWJ~ce2fuGGx&{7I-p6m_N4`n&woG%8SK>vbd>dxM}#q-)J{nS=hfh
z44x_ZWeZ>TyG3LJEzyGdFEqdTA3^P*>lFpO!7U^0PQmF_`5%yGyxU}Vi3F{X`b?RV
zNa$hAotMbep(~)kAUj}G6k9^*I5bF9gq2FOE@0~p_kUuR5@pl^J47<~>wgul(up~;
z_@O$T6?tsPNyCh2yT;(zy=*)R;}7#CpwP&d{3Qw%p%;6es-n|kr~Rq3>YP5{sEony
zR?6|@Bx$7!?03mc{7EURly4@7WtIbm;%xmhvvumi*W@TtoSy9%Owy)Cq&I1YOK+JZ
z-$swhSU0OQhy74}MyU81D_2H+sL?7us&7j=?3Yw){6cN}^byMJS)wbW&3vBc5v|ed
zD|78aGfRePsD~b=fVt?~=t)iF00<%Os&9~lYbF(laB^X$UkednlnjM`-mlugi-Ipj
z-TlH8!E^KO6^WkWxJXFfl7FC)hiMo`WgH8pa)1ZcpINA-lN!mHZ?TM`u5D26<qY2^
zg*$~h%HD3S0j^s`8@E|+JjU7@ZEOxBn8{FaMRPZPsO~lENa|c;wtq_Aw#4P$_M=Ak
znvL=F$9<Xl)Cxn*bC3UE_{GhGo?QlXKJHKCa;tP|dVnlC2z@+QV?f#CbS5{Mk2B#Q
z9AJW{bB`ESt8^Z;NMg50I?!0C`FlD7Dp_H)t%0=_Ae#FlULrpxSF%caFnO3FJy^00
z&n_&<FL`*M<U^^!^hr_n+|>qRu(|c4DaP%Z>rEkKVY8xq7!Fm=!M+%egCfN;hvF<g
z-gq?2tVRgU@(N39d|rc@;N63jZwT^E;3w4)t|PzW3%l&oEGKW@9mOPIy;Eflcrgil
z39_@<7xrppq^IY@Fg9^vbQ$&{<}@t!3i)Y5ovHdtz+LF9gAX%+V1w>EGrx@z3DabR
ze^S$mmgDU1Z||Vj#Pn*7Hb>Nz={QP}%ba>(pOL%2*tFvr`v%hllI^T&A6u!=0#3lX
zyR35YEwV*ARH#iEm9kaPRXx5f08RkdAYqi%#kZFm3r)s83WKD8h`D}nV!wkGb$L}k
z1XT;$FFUVTX(-gDLxSr2$vVi0HdYL1g>v&w9uhbpnmnYL{l5vx3n~hK38$Z)vOKd^
zN#}iU2!{UYzbp1U8V`#DsfE$~G4;0mN~A^y-x}nTIi{o}RcT-p)!by?-;+iTOtCjj
zQa6@)6iE)!h8tR6)@**2RzV1&9mgcOZw*RuS_4z%>Zmk*0g`UPOk%}CgfroGffQM(
zt!!j^v>j}Ctb~i6+a?tF%JecVl1^CYPAF_T5&6XiOon&f>CT&zoO$hVi@!~+j`ZZ<
zDXuT0@o?$V7dUp6fq+J=F-d;7#aB*7`19jL!-%N!o+FowZG017@6_|4XVl%5+YcJI
zjB0A$Na1zM=_LUjZDihJ#k8j%x-R=2F?{gfv>?<T`vhQU>=`^`GPPc-FVT1ZG;)pM
zFd1FcRVGd9uxu1zjWA%i)_vtKdNwB{Dmc0P65n}I=$j1dL8AiQP}fh)wlI*{P25eA
zlGPrYwm{!k17^=`ti<%cqnS*l`zEoexOn7rjcbv%63k?0DT}{d$<y=cy$Qjw>hGFV
z+>YG_%G^xz@`0}}$lafN+}`3B&L0ILB5pQ=!DNdk;K2U-Kp~vuk}9A_L?`-Icz{&f
z6T!{a5i!OMWJPQUbh`Fw3Y4O!yX;E?bbF$wv7GhbOSx(@E*Q@0-^orOujjn2=rgB5
z#{0jjVe74X9?u5bcf(upOK<QV>(xl8afIkS`WhXt>^1YWljHdtbxU8|X7`+&rmr2B
zjDw36AJ<c5jIUY%pXVp$)pk^mkU^WtR91W)@6w0hoBaMRWGlxW`RTP58TL}k^izDL
z1ol}>c(-kQf>YzC^Z<11_XpS!J}Lkl5zG%}YGDm}gBzE1q~4EUQjY^E_Ksb_@o6jn
zDc+1r5TSpqq0TN$%CYPM>MTPo9Gq~G^EM4I#4&??Z3Vu(;rPAs1R}pK?@zu?0X^FY
zj<we~Wu(EQH5wE@&<$DC^`>{F))_Jjv7i0dAJ5nHAxwq{sS5+(`}MaINPjr>k2gAE
z#h@$`-?K?Ot7`?VG(Dx#)Ty3-dMax(^apZuxX}DA>O~pKo2?mw@Oj@8{-`CTtSDLW
z(N+f0SFrn_yW#tdE#gMq``$`46<YHE6$MN)3cMmlDEVzQJe*=rT2zjG6v+^5OOYZZ
z-lS^r_wpIzLmw_P>@kioGi>;HyIbPa+`Vi2$%^X{j-TM}Tq41CC;0J53qRjCV8-Eo
z24_w?c<+QCt2jj1#8?(qAFr^|dhq%4XH<PeBTtBSw^r(yj1O>|mwktv_Et}jm6mf;
zbsxSS7=&&yzXRmWM&nKvtrKgIhaH_iAsdGH>B13$9>!^&P#Iu35#TK7>N3l{;J`}T
zPD7XWx-6@hHRf(S1O(o&yH!kgXEht5wFpziiY}CTR2zg|OgprC-_iBk{`iecW}df_
zK<0F&y%!#-d?6>*kb}1bH?PZHz{)i-x8N?a&gzxG$2X^gH>Znl^nWBIH+;kzQ9c!w
zFu$XyvrVgYPK_8viz(cyuH+$cv+^fyRPYrp>{G@Sl||#ncr9TyO;v-CFA|Xu?bLOm
zAx41d9DKnXD)?b%)U}7;?9bf53U)l!5V`D7A5P9eK4j8eA4L*Ys|azz%BNWfHo??s
zyR!&XGflZqY*QWxci#$N)a3)W^U%S#IG#|Li9o8Sg%;xXr#9lM2o{pkw0vfy{xlQn
zeJdoS9><~vv~C@M45hj$dsLCJAiON8!BW&SJ39S8!z@zw(7ZoQYeK%lGe*67?rB{R
zwT?YS$`JGyPL<zBhm3ajrVefUMW*5YR&Qm9po&Z{^q9SZG_g>VG>whb-8q{|-4%%F
z>$7Y~&8aoYzxVpjizN^{J0tSmKFS(;(G<8hmI!+>g%mYL_<PB@sMY?NBfFj$;gnMF
zb%Su?l5er_N?o?j*Mp-o;qv)$PBV5fY-9#nMEq5)FVwpfDqy@>zGdI%4IgMoYo{)y
z)xh#jPRS?5yKF451vbHZM@BA?zn6@8z$cBK@M&Qs=KY1I)Ftbx8BJkEFKadeublM-
zRBffh#F~%C?*^3u$5YY&b3xWLt4N=zsGv1H2y_BU`&w^fjf@b|8oRyZLtQtsHtnw_
z1KzSAL0|X8Ht69bksAtSP?KDsqF<%xDjT>5{o0hA9g#>}Voml4aB)pbSqF%B8stL3
zcTlYmT-fvX{OI~AEz~R*OeL#LJGieZe=L6zxwP@~Uzi~Sn2Y|_rV4XL$4}#v=wX3#
zpMNh9AHh&=j!6{Q&T<x5vkC^_*Th$=xRQ9CzrCRH&U&VnUbk=mgvp)*1H@sNuCirf
zKF9q7pZy`!sVh<s#`V9&&lNgCNPk&ec0X;sr_WI_&7{ThBu7AD)E{wqIs2oh`+2`D
zoax&7Mns-R8-~D#E_NXfggrflg&$rVidM^t<`}2udq>R|hI1CMs-!N=VMb38JrMzD
z+;cY>4C2`0s{`2x=K=)KT}BDXFl8<d2SV;58y8-FhHR;r8v6#r7e;+++i5?@z%LBV
z^gxTol7w%2LHrXVun}mXE&ZEe6x?f3I!PPB6H6GkFR6ZFm?%AQI9N10OItnV8j{bL
z_~Q%xWkL*S=BjKALssErgYe*W4ENlrreB~ko0>V#Z?l#Bq&3t2pt#b+89Jyh^k6H9
zlKNBimZ`~+xeJYl0Qar)(IU#}I#mIG$?*}d7YwP1X@U(u3)4iQpL}}sjezwPq_Mv&
zCL?VoM$vhBoa>bOHz~ofGkD}nVYa1z(MwJ{^rIgEKBpv?fDg%}YQx>!#R#$7;iD}4
zm_ZM-<>1Khd;J&Sq+sF7)KA(%*8#QkH#LEP1qRDD7(YScwz=Swx-)1AMT-*WjwWxC
zz~rWG&@I<+p9{d)(5!9;#wrjlwv)RtNldc8`d#R5c|ueySqwcF=mgiXrEh3Pv#sVW
zSen)n`|odGJWd;_GwnWUt{)tnF@o`Vv0y7JNK|kid584Ac*vQ#&AiDTY1HmG!XhP<
zr};wJ!}<mXqEOy9zF_s+OOyWOFiS`+Oo3Z1<WV0{Ds4b>k?;IJ=!E0fvx>Q~wTiSi
zGjWI!s#=LU&0V}Xulf)eW4#v-{T2R3Quqan&RSppO@oHjtE-*#SD~u;FnG_NU)LMo
z23zc2xb*>nIbS;*Uyv|{VQ~)nwEE)SF^b!?dpBZVMUBZ0MaU$J6?d#JajqswHpV)S
z7-D#3s|1)&`HV%XxXD+$yJ`IjsxYz9To<&L{8S_)BLG85h5KAtWh96a%zS3Wf>@lB
zq2VZtW;N~%no$0X!vQSH6l2V}XNkiOhga4V7b828FspY=2mc;O0EEo`0p@JjuB4^t
z?vMeTdKB+k%}&EX{s*2k-j5}=t)5vT5*JLuq7V*Spv^#VWdK2ie=zOZESIbl45GS}
z8{WV+TQH;++SHP3b+XnEbF#eU0FWf(;W`tyu}X!`@OQhCi!d#fonyj(;_B->Nw=M?
zJKqOiMxeyngVnzTS|(DUne1;4lW=$)!v8kaxTsj$$nVrF&5D|-*X6MT8@{I&huvDy
z%%`zk5S?e@$pPRBl%Svy;2@z<mRM?I36o_(@JlBNp<Ra9$BEe_HU%a9sF{nJIEeoo
z|7$&DISEXOThpgy9U_6leB0svZ8JaWUmcoOroKqdVI+@2@pno!7(!Hb(GWlJ%@Z`1
z$+<l;Nee}}yDNSe{!;_BFBY+R^yRw-2h<%V5Z{Y>z7KsQOn1BeJPG{Bd?oL(AkOLr
zp8`nUzMyvm-r>2v-XlCdW>y$d0f0o6?GK(Q+xgsYaQl!0qqE`qnXc7)C79y)PGmlD
zTrQNB8kojxQ#-tkgLAbZdG--h@R^gjSPS%%{0eFA$)Er<x_Ht7TcY1K-*w(UqU%Iz
z%k$mC)d7keo*XJSf0Rs$#-D|vGA2ZzaJ@#)M8~*n?%u6msjbL4vUkhGbBdw)0`O2G
znetpljA>(M)rE!D@>2kQ$0A<o&{!!t+b*9Sm+&*cQN}~yF5x7fMpJ(&9u@$HNXZVO
zw-tl0_mBvoNlZ=vUSzoDHXcBKjSu2omxU&aQaoFEU7TR1^_GVH)TU&eTmOL#fcA=9
z*(ShpqIaarOyRNEKO$-il)>0!gE)yFmB6KiaJ-62aeJWJ-jSM&kQ@vjl^n)7LyeVo
zq*u3wIe~X+*0CO);%vId`+;ZqRVxLvx;k|Ifg9c8SnKYeTFhWdPY@J^o9pz9)0TE4
z4{ImE;B3wuNi<p|M#fg|98;NX=lmPtXCzUt%pan07@>2HaY(`8!2(Xk;_`6v9H~8E
zM4dNE!@J9RS2W{2MVx~iR+80+JN<_!XjFHFZx?^SCC_3FBb|iR^<!0<V1#U1Y`rdM
z496w<bZ4t*QCg{FNvkQ-@I`-cgUk1(WL#$PVuD;vFZ2CIAN%()q$7~Rl9LYVOrk!7
zi$YMTgXsYxCJj>gr^(Oyea<%9dZZyP*5AK`AkC+g&r@JJRH@boG^xHfynT>KNpRXI
zXx3Bq?P*we%JuD1!lOzNiB=0JQqEmGPzjeaX0@-QP_pPcw@FZY0(tf*adXXEsk~=7
zf*wM}`OrZeG34gsyOMKEy7^jToP!a!Y>HRZ>leu?-Y|YJ*e<Vc>5m-|%2|f;YyUP3
z#~^Wsvbp1oJJ@eIZ~{anNn${N2SBW1AxS~D&_X^7fsGbd6J>q30?BuFJr<G#5N2ss
zGeq{hHqi|{5^V1R|JV2cO*hvn3WZ7dY?9@;0M0ib=Kw!{*C{DS3430bF0oN{8IP>d
zVQVT4l|`-~%N!opXp5peEZ1Eb3fgFSKs@XT%=O{vs5}*v^J3!QTJCQS2WVVj4W;r+
z`yW2<!}43x9N*4LniF8ZYoMkVH;p=B++~r1i&dXTs~)oGNN%{ekXRsAQNzUPs3(-g
zrHsnBI&~-qooUJp$5DMV=P8jtW)hP_GJp`P_s^r#+MIt$1W?Y;QP)t<>-7)CkGV`v
zZfa2zwsHacTJe(?>0O^nbc@+Hj53FnR@+|1{M<=$2azz*+QGipVRc+Hvp-*q$OyQl
z^awwKzWe8PGCvjuZ>*(4;n_|K3SQLA{v8I})F%isE|c;Ev5~R2C-sYc+jifmT(Nq8
z5LwBDa3<LP?gI2~;aHEBh42RQ{`ep}fY=bd*qrC3C9u$Ug8biLDf_>S`G3eG<I>8P
zEfC6Q;oZl3kKnuU^Vfe^cOYtMwkzyC_78syriBu$-_p=ia|QMkc-daNe)RSDI9Xu;
zSHA9$^yk3&mkNd~33uJ7WJ0_LoULWwQ~f-fmqES%J04f%Ob}Ij6ya;cj(YS6O6o|r
z?cu<qFVunaiPWX<0po?eP(lmL6Q@XH8DWwXg@j|=WMQn%dU>1MinINTS!RKcl{Ot)
z4vNc6>kOnu)u}%OfR;IPeX&@qRkkoq<0NzLX!W<QvXRkqnAR6X3JiB_CV`hk7gbK|
zV0lOH3a9%%U48DYoz)fVrY;ZA-|cnJ3egc(`rXGR{bI=@`CKRK80b$Hs#C{~@nyzU
zXBM@mL;Y`<bwE^VqmSg)PFoaTjTJL3G{3RhifnC+pRHfzosauln&q~gs|LLmeSS!h
zMLI3SbR)VGm5At_myEcx8)w0fm+dL=WVA*fC~QUC&FaYK8zW)^@`u$S4p}P9_9cYN
zE%BJa1T<~_*9}A)wg^QTwSM5s$$b`sIi*0G_Bph9OV4(f4=o<dShHgl3u<$fle+AD
z6DfIckyZJ!v5$^V23v|F<hEvEk&uqBFnz;jWCs#Zaokk(A8RLWSwYd)w=&W64)WS7
zb!pz;t3~bQ`EIfb=LUyO)8TBmH4rS<KZQK~dDn@iAPSTr@e^eNn_@|@mX_GlH8l!)
z^z`eRf-Lom*_@-*dtbwCyIQzKW8RP$9{Y|O$N7Ken=WgQ5{5kX(yj|?i@WfozDCMe
ze(G;@VU^Y^{|-9pP=iuihLi>@-6-_nk`-9DZ2NC{>cmxYE?0|4x+eNW6S>i8VpV=j
zKjJL>_Bw-ynxPQN>9K--g}u2HNYWl{4I-i4^Q>m^S8IlclT#u2uvJ)-v%5cQ=a($7
zO}Wc1AR`|#Vn6Ojin^^KlAcVE?(K)r^ll;+Fw71+A>3jWhae@Lhz|fy;h(nR?VtjN
zS_p@k3^(~kzcK|@%O<)&wlPkp)rZZ3^9C|6eTOf?v_M?Ra>K$>N4>iNU;N1<`>Qw)
z4~9@k$mR~l&CIqh4FiuDZRV?<3(p%)>vl~Txq+Fe6kWI}QI`H7czI9Mg3kB@bk){l
zSJM=c^=+BEWwpiDA;lWpNzMDp<WcK7XWyqI1b@4w5n8aT^d(r-%MbV3KVw6T_b~AE
z>iU@jJkTdzvLk#d&<YMbb^rGX7Tx;_e4XY;OlWhe-ikgPLf-NJ6MT(JjIW+{F#%dh
z7O9yBdU`$@`1o21v0v>h_$NO!C~wg>Zr_ivXWooDizBsAHOIvuZ@g(nI$UP=H3ztv
z>UZSVRvB`yPP55Rp3YvQ$gs_ah4w*fV0~S>b0IJIz05mmvhTugbc$aWOo#q>MQnLt
z!sDyM%y2O+2#yia2t;H0{2JvfS2lJ~`H2}tVJ%4V_L*#%N@6Gs<aXhw-PoIT%;j>P
zjl<GkoVnWIDxdg+sQ)O@V{&J1Dyi4$jYsA&WC_dph8I8_tJWx^4!C+v@D7#bj0yH*
zz7cL(E-SuxYx%DZ5P34?4>s0OSIEHZH}yP79#L|T*=MPq6?G`G$5UH73}_(=w0iGx
zO}Y7D2;H*%1rYSF2X8sHEL=XLb(nzP5sWs#Zy!BgU)Lm80$W>GB7366_E1iNbJf4T
z{&z(}hEBwU?%KMdG-70#KiZ9cC$RIlR7MKsfQ8oiP-It{AMEVW$~9NYGmZ7#*AoZ<
z?Jt{6mUH|#?w;HX9TRot2i>5L$sLDb5+|?Yy8W$gH$!WIg1Xw8xnj3WRm`vD`P1h`
zOQdsCca~t;M!6WbhuEVHXf%Shl)YWOA5>912oU*Qb4u5a^UrWsRR-H9(GszskpGzb
zq#aA<$SheB8ni9x9<^c$HI_bd&b8+jERw7uyN{<L-3Il=hy+5$R`E--JHE)iV_6$y
zI8ySoZ1eu5FkD3U{7UjY2lbJ(oZpe<sF@ljX7XHvaIJ!ptim^#*LZrV8!Z}lE$QR;
zjMr-PjkXZxYYc|oW)=$@z-{Pj)-S5ehHn=b2+fmS#EZ?ZVTRtRH~Ay<X@ad$eZHzu
zF2kmH5X_Y9WHR0$coo6SW#_fc+eo8p@~~J`GPtp4+OlB#-6Z8@jj?U5eRZ;Ka!++h
z`2uhKjP!7wBWrHc&WdcBq9lYpBYZ-vr<eZQMB5%0@pUsZ+`EDbM#KSTs5?1Ila+RS
zo5CljitVV{)LB7RzFJh;4V!WM=bWA9BM58c&{4@p%ldDX;14AZ4;MoZz>ec-aa2kB
zFE#n-LT;buM%~Ud&+#4^1;i5Cb~~gZipqBQtB^fSbq#v0R)f5y-s(g6JKcubC~Yl#
zawmdDGjdY8<a%98x7ky)BKA8AJMnQ3SdAUxs23ySuBJ0|mv_!$RUG*wK7nP2G%2!@
z4>zv^kZjv=y5?M^-n9|=`PqcjT|3MBAGyq<H<wYr&H;XmJ$ts;IR#DZwzop7ScZj!
zuDZ1aMiVV&tjS|~tXxw+vC*r|TVLrhMs}6>Y8zdvYb+3uVz9XQu*eid5({eOM7?Ks
z8OQRPRc-$(px{HAMvjRH2lX>vKxyPx<@zF<WzD@sz0fmRielZ$L?jzSR<!=7O|9RR
z$c>+|^b>uNS@zD5d}K9+UYwX=74=oy5F?q(WGEpvIxo6rlUeNA^zG5vqjHmTjrJO>
zN4`?em32mdVRJ>e{XL4%Z~oasC0>REJBXo1u9N6E`O6y@C%}=&`~)F_s*u}G!5@=q
z)^-AG?7(|nfx0bb9sPpXIiXmIac083Ft3oreJ^zdk$BhG>lu8%2EXxpZSJ|$Wu^N=
zSWZ`3EsV%Vq!ZZN?Jr5!`}4V@wPn`>AxTG{9BD@%3f{ZSbrnqF`SCi96xx09{DXhv
zFxcW&oVB_(V&3x<72-6%Wq3^J*kCNZHCJS19ptex=p+jFdW*2)Zi{)AUVik+(o+U|
z^h@Ym{h<^`#cx5LIxRO;xE=PC_NVdL!g&EkU&J6<fEy4j$YIoIo&1~sb1y8m^S0@>
z@?hE;ECoDXG}~4T(?zQe_W)mwK?0emHp}--i>Fk8Gne!suERJVY}pt)@T@trvayM$
z1DGSjq@j?(oJ=vU1m_a$T0WOkAZlLQ)X0#YM+gAQMzQeX+MXrS&NxJ;t|MBe8$#CY
zKjk^li4+ff9UUI-pH7|asN5k1ac}V%P2)OxOQ+~G$w>#XDNW$DRkb`@&1XoYiQCse
za2_t!$xSk+X1e}kFRAEOn19{b?*Ar?%%R!b3DcH)q=*Wa04uK0kxSh)CsOBBfc_*J
zBeDv56WfaWV$?#SZ@N1P-1FOeW4~nYawWWWcyfg7v2hI<9$|QhGcPrW2EXrs>8JH<
zKyf4}>+)9M`KU<1U{V&6dg>K_d`lAPBre;s6x9^FkzC~8%49}dLQO_(eF9mmHqkOf
zffp7k00fmbmS9WMXqks4i@*NDQC(b7U@WL6>6x+7OR>yPTI^^!n(#ug^51T+6{q8B
zu>C_!H#R>>8JDA-PDPe{MKQ)uJ*hS@*!jNbyvTcJsOefO`W0Pa`}LdtgrPNaTgQ|G
zRv48;%vIcntFMVTLray^uh`6u0S2eEZ9Ya`8_V$1f1ey&3DOo^uKb~pCn-8c{JBTj
zXklAfy?VX|{36T0+(j(RteR64g56e!vb_JIeI!h6p||>WF(bO%bjEj`ULX)VKA?;p
z%@J^smp4cA9K|PZf9vo8CP+{?-^-Hoy6`0tSJ$(?D81XaQO8*|;gk$iNywMe^6<v`
zNI2g`*koB>(H#&H!0H{Fllj*nM;Q%L-^xzoc-5LZQmQPmXB|WxJl8u3PIrIa4WSX`
z{k=+BAmkgV*){&l_EFIyO5iSd@vy%aM2Wr0x2C)C9p8zRaIgN{<z@ZR+z3)X>;SuM
zD&B7<u4TVX!9M$r^mi`4Vkf*)2);*aBw{W#o0HY7V@Iy)npsgEkMa$l3c1Od!&RY0
zj?u-qTsW!ASWr552%G|4kMs|K`{xML&(op5>|TF##uCd<^!2tbpQe{>B)sKrb>SxY
zC>YQ*1<>@J?{p<CNm_KOE(!g!0uj7qyM0GK^CAu9eO3Sa4Os{^F#pvHlQ*4?c=$mj
z^uY2bYkeQx<NV>RVMUQes{G4W$chveLP!x9!|ktF6S0v!R<2k2D5GO~+Hv0zB{&+N
zn<b-7Ti~v>u&aKtIGgEZzQvdN=K*bn3~Z$-T@g*x;Nb#vtLSFR{r@4$|1r%6;{+HQ
N9r<rG#0><*{{SI-*K7a)

diff --git a/packages/bgp_peer b/packages/bgp_peer
index acd4ce4..fd6a914 100644
--- a/packages/bgp_peer
+++ b/packages/bgp_peer
@@ -15,11 +15,12 @@
            'checkman': ['bgp_peer'],
            'web': ['plugins/metrics/bgp_peer.py',
                    'plugins/views/inv_bgp_peer.py',
-                   'plugins/wato/bgp_peer.py']},
+                   'plugins/wato/bgp_peer.py',
+                   'plugins/wato/inv_bgp_peer.py']},
  'name': 'bgp_peer',
- 'num_files': 7,
+ 'num_files': 8,
  'title': 'BGP Peer State Check',
- 'version': '20220418.v1.7',
+ 'version': '20220509.v1.8',
  'version.min_required': '2.0.0',
  'version.packaged': '2021.09.20',
  'version.usable_until': None}
\ No newline at end of file
diff --git a/web/plugins/metrics/bgp_peer.py b/web/plugins/metrics/bgp_peer.py
index d6cee6e..21c2c7f 100644
--- a/web/plugins/metrics/bgp_peer.py
+++ b/web/plugins/metrics/bgp_peer.py
@@ -87,7 +87,27 @@ metric_info['bgp_peer_suppressedprefixes'] = {
     'color': '12/a',
 }
 
-
+# Juniper specific metrics
+metric_info['bgp_peer_in_prefixes'] = {
+    'title': _('Prefixes in'),
+    'unit': 'count',
+    'color': '11/a',
+}
+metric_info['bgp_peer_in_prefixes_rejected'] = {
+    'title': _('Prefixes in rejected'),
+    'unit': 'count',
+    'color': '21/a',
+}
+metric_info['bgp_peer_in_prefixes_active'] = {
+    'title': _('Prefixes in active'),
+    'unit': 'count',
+    'color': '31/a',
+}
+metric_info['bgp_peer_out_prefixes'] = {
+    'title': _('Prefixes out'),
+    'unit': 'count',
+    'color': '41/a',
+}
 
 
 ######################################################################################################################
@@ -165,6 +185,17 @@ graph_info['bgp_peer.time_since_last_update'] = {
     'range': (0, 'bgp_peer_inupdateelapsedtime:mac'),
 }
 
+# juniper prefixes
+graph_info['bgp_peer.juniper_prefixes'] = {
+    'title': _('Prefixes in/out'),
+    'metrics': [
+        ('bgp_peer_out_prefixes', '-line'),
+        ('bgp_peer_in_prefixes_rejected', 'line'),
+        ('bgp_peer_in_prefixes_active', 'line'),
+        ('bgp_peer_in_prefixes', 'line'),
+    ],
+}
+
 ######################################################################################################################
 #
 # define perf-o-meter for bgp peer uptime + prefixes accepted/advertised
diff --git a/web/plugins/views/inv_bgp_peer.py b/web/plugins/views/inv_bgp_peer.py
index 3d828a1..af7261d 100644
--- a/web/plugins/views/inv_bgp_peer.py
+++ b/web/plugins/views/inv_bgp_peer.py
@@ -1,16 +1,24 @@
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 
+from cmk.gui.i18n import _
 from cmk.gui.plugins.views import (
     inventory_displayhints,
 )
-from cmk.gui.i18n import _
+from cmk.gui.plugins.visuals.inventory import (
+    FilterInvtableAdminStatus,
+    FilterInvtableTimestampAsAge,
+    FilterInvBool,
+)
+from cmk.gui.plugins.views.inventory import declare_invtable_view
 
 inventory_displayhints.update({
     '.networking.bgp_peers:': {
         'title': _('BGP Peers'),
         'keyorder': [
-            'remote_addr', 'local_addr', 'remote_id', 'local_id', 'remote_as', 'local_as', 'bgp_type', 'version',
+            'remote_addr',
+            'peer_state', 'last_change', 'in_service',
+            'local_addr', 'remote_id', 'local_id', 'remote_as', 'local_as',
         ],
         'view': 'invbgppeer_of_host',
     },
@@ -25,8 +33,26 @@ inventory_displayhints.update({
     '.networking.bgp_peers:*.last_error': {'title': _('Last error'), },
     '.networking.bgp_peers:*.last_error_code': {'title': _('Last error code'), },
     '.networking.bgp_peers:*.address_family': {'title': _('Address family'), },
+    '.networking.bgp_peers:*.as_name': {'title': _('Remote AS Name'), },
+    '.networking.bgp_peers:*.as_org_name': {'title': _('Remote AS Org Name'), },
+    '.networking.bgp_peers:*.peer_state': {
+        'title': _('Peer state'),
+        'short': _('State'),
+        'paint': 'if_admin_status',
+        'filter': FilterInvtableAdminStatus,
+    },
+    '.networking.bgp_peers:*.in_service': {
+        'title': _('In service'),
+        'short': _('In service'),
+        'paint': 'bool',
+        # 'filter': FilterInvBool,
+    },
+    '.networking.bgp_peers:*.last_change': {
+        'title': _('Last change'),
+        'short': _('Last change'),
+        'paint': 'timestamp_as_age_days',
+        'filter': FilterInvtableTimestampAsAge,
+    },
 })
 
-from cmk.gui.plugins.views.inventory import declare_invtable_view
-
 declare_invtable_view('invbgppeer', '.networking.bgp_peers:', _('BGP peers'), _('BGP peers'))
diff --git a/web/plugins/wato/bgp_peer.py b/web/plugins/wato/bgp_peer.py
index 3756c91..5e37eb5 100644
--- a/web/plugins/wato/bgp_peer.py
+++ b/web/plugins/wato/bgp_peer.py
@@ -13,7 +13,10 @@
 # 2021-08-21: modified for bgp_peer plugin (from cisco_bgp_peer)
 # 2021-08-29: removed htmloutput and infotext_values option
 # 2022-04-02: added bgp neighbour states
+# 2022-04-29: added upper/lower prefix limit
+# 2022-05-09: added discovery rule set
 #
+
 from cmk.gui.i18n import _
 from cmk.gui.valuespec import (
     Dictionary,
@@ -23,12 +26,15 @@ from cmk.gui.valuespec import (
     Tuple,
     TextUnicode,
     MonitoringState,
+    ListChoice,
 )
 
 from cmk.gui.plugins.wato import (
     CheckParameterRulespecWithItem,
     rulespec_registry,
     RulespecGroupCheckParametersNetworking,
+    HostRulespec,
+    RulespecGroupCheckParametersDiscovery,
 )
 
 
@@ -37,10 +43,30 @@ def _parameter_valuespec_bgp_peer():
         ('minuptime',
          Tuple(
              title=_('Minimum uptime for peer'),
+             orientation='horizontal',
              help=_('Set the time in seconds, a peer must be up before the peer is considered sable.'),
              elements=[
-                 Integer(title=_('Warning if below'), unit='seconds', default_value=7200, minvalue=0),
-                 Integer(title=_('Critical if below'), unit='seconsa', default_value=3600, minvalue=0)
+                 Integer(title=_('Warning below'), unit='seconds', default_value=7200, minvalue=0),
+                 Integer(title=_('Critical below'), unit='seconds', default_value=3600, minvalue=0)
+             ],
+         )),
+        ('accepted_prefixes_upper_levels',
+         Tuple(
+             title=_('Accepted prefixes upper levels'),
+             help=_('The values from WATO are preferred to the values from the device.'),
+             orientation='horizontal',
+             elements=[
+                 Integer(title=_('Warning at'), minvalue=0, unit=_('prefixes'), size=5),
+                 Integer(title=_('Critical at'), minvalue=0, unit=_('prefixes'), size=5),
+             ],
+         )),
+        ('accepted_prefixes_lower_levels',
+         Tuple(
+             title=_('Accepted prefixes lower levels'),
+             orientation='horizontal',
+             elements=[
+                 Integer(title=_('Warning below'), minvalue=0, unit=_('prefixes'), size=5),
+                 Integer(title=_('Critical below'), minvalue=0, unit=_('prefixes'), size=5),
              ],
          )),
         ('peernotfound',
@@ -136,18 +162,21 @@ def _parameter_valuespec_bgp_peer():
         ('peer_list',
          ListOf(
              Tuple(
+                 orientation='horizontal',
                  elements=[
                      TextUnicode(
                          title=_('BGP Peer'),
                          help=_('The configured value must match a BGP item reported by the monitored '
                                 'device. For example: "10.194.115.98" or "2A10:1CD0:1020:135::20 IPv6 Unicast"'),
                          allow_empty=False,
+                         size=50,
                      ),
                      TextUnicode(
                          title=_('BGP Peer Alias'),
                          help=_('You can configure an individual alias here for the BGP peer matching '
                                 'the text configured in the "BGP Peer IP-address" field. The alias will '
                                 'be shown in the check info'),
+                         size=50,
                      ),
                      MonitoringState(
                          default_value=2,
@@ -172,3 +201,39 @@ rulespec_registry.register(
         parameter_valuespec=_parameter_valuespec_bgp_peer,
         title=lambda: _('BGP peer'),
     ))
+
+
+def _valuespec_discovery_bgp_peer():
+    item_parts = [
+        # ('remote_address', 'Peer remote address'),
+        ('address_family', 'Address family'),
+        ('routing_instance', 'Routing instance/VRF'),
+
+    ]
+    return Dictionary(
+            title=_('BGP peer'),
+            elements=[
+                ('build_item',
+                 ListChoice(
+                     title=_('Information not to use in the item name'),
+                     help=_(
+                         'The Peer remote address is always used as the item name. By default the check will add the '
+                         'address-family and the routing instance/VRF if available. You can decide to not use these '
+                         'additional information in the item name. Do so only if your peers have only one address-'
+                         'family configured and you don\'t have the same peer remote address in different routing '
+                         'instances/VRFs configured.'
+                     ),
+                     choices=item_parts,
+                     default_value=[],
+                 )),
+            ],
+        )
+
+
+rulespec_registry.register(
+    HostRulespec(
+        group=RulespecGroupCheckParametersDiscovery,
+        match_type='dict',
+        name='discovery_bgp_peer',
+        valuespec=_valuespec_discovery_bgp_peer,
+    ))
diff --git a/web/plugins/wato/inv_bgp_peer.py b/web/plugins/wato/inv_bgp_peer.py
new file mode 100644
index 0000000..a62b004
--- /dev/null
+++ b/web/plugins/wato/inv_bgp_peer.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Author: thl-cmk[at]outlook[dot]com
+# URL   : https://thl-cmk.hopto.org
+# Date  : 2022-04-24
+#
+# 2022-04-24: added option for BGP down time
+#             added option to remove some columns from inventory
+# 2022-04-28: added Whois options
+
+from cmk.gui.i18n import _
+from cmk.gui.plugins.wato import (
+    HostRulespec,
+    rulespec_registry,
+)
+from cmk.gui.valuespec import (
+    Dictionary,
+    ListChoice,
+    Age,
+    DropdownChoice,
+    Integer,
+)
+
+from cmk.gui.plugins.wato.inventory import (
+    RulespecGroupInventory,
+)
+
+
+def _valuespec_inv_bgp_peer():
+    removecolumns = [
+        # ('remote_as', 'Remote AS'),
+        # ('remote_id', 'Remote ID'),
+        # ('local_addr', 'Local address'),
+        # ('local_as', 'Local AS'),
+        # ('local_id', 'Local ID'),
+        ('address_family', 'Address family'),
+        ('last_error', 'Last error'),
+        ('last_error_code', 'Last error code'),
+        # ('prev_state', 'Previous state'),
+        ('as_name', 'Remote AS name'),
+        ('as_org_name', 'Remote AS Org Name'),
+        ('bgp_type', 'Type'),
+        ('version', 'Version'),
+    ]
+
+    return Dictionary(
+        title=_('BGP peer'),
+        elements=[
+            ('not_in_service_time',
+             Age(
+                 title=_('Time peer is not up until considered not in service'),
+                 default_value=2592000,  # 30 days in seconds,
+             )),
+            ('remove_columns',
+             ListChoice(
+                 title=_('List of columns to remove'),
+                 help=_('Information to remove from inventory'),
+                 choices=removecolumns,
+                 default_value=[],
+             )),
+            ('whois_enable',
+             Dictionary(
+                 title=_('Add whois data to the inventory'),
+                 help=_(
+                     'The whois data will be fetched via RDAP from the registries. For this the the plugin tries to'
+                     'find the best registry via the RDAP bootstrap data from https://data.iana.org/rdap/asn.json.'
+                     'The query it self will go to the found registry via http(s). Note: the request might be get '
+                     'redirected if there a different authoritative registry for the ASn'
+                 ),
+                 elements=[
+                     ('whois_rir',
+                      DropdownChoice(
+                          title='Preferred RIR to fetch whois data',
+                          help=_(
+                              'This registry will be used if the plugin can not determine the authoritative registry '
+                              'based on the bootstrap data.'
+                          ),
+                          choices=[
+                              ('afrinic', _('AFRINIC (https://rdap.afrinic.net/rdap)')),
+                              ('apnic', _('APNIC (https://rdap.apnic.net)')),
+                              ('arin', _('ARIN (https://rdap.arin.net/registry)')),
+                              ('ripe', _('RIPE (https://rdap.db.ripe.net)')),
+                              ('lacnic', _('LACNIC (https://rdap.apnic.net)')),
+                          ]
+                      )),
+                     ('whois_timeout',
+                      Integer(
+                          title='Timeout for connections to RIRs',
+                          help=_('The connection timeout for each whois request.'),
+                          default_value=5,
+                          minvalue=1,
+                          unit=_('seconds'),
+                      )),
+                 ]
+             )),
+        ],
+    )
+
+
+rulespec_registry.register(
+    HostRulespec(
+        group=RulespecGroupInventory,
+        match_type='dict',
+        name='inv_parameters:inv_bgp_peer',
+        valuespec=_valuespec_inv_bgp_peer,
+    ))
-- 
GitLab