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 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 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