diff --git a/src/exabgp/bgp/message/open/capability/capabilities.py b/src/exabgp/bgp/message/open/capability/capabilities.py index a62d59d5d..1388332d4 100644 --- a/src/exabgp/bgp/message/open/capability/capabilities.py +++ b/src/exabgp/bgp/message/open/capability/capabilities.py @@ -22,6 +22,7 @@ from exabgp.bgp.message.open.capability.refresh import EnhancedRouteRefresh from exabgp.bgp.message.open.capability.extended import ExtendedMessage from exabgp.bgp.message.open.capability.hostname import HostName +from exabgp.bgp.message.open.capability.software_version import SoftwareVersion from exabgp.bgp.message.notification import Notify @@ -142,6 +143,12 @@ def _extended_message(self, neighbor): def _hostname(self, neighbor): self[Capability.CODE.HOSTNAME] = HostName(neighbor['host-name'], neighbor['domain-name']) + def _software_version(self, neighbor): + if not neighbor['capability']['software-version']: + return + + self[Capability.CODE.SOFTRWARE_VERSION] = SoftwareVersion() + def _operational(self, neighbor): if not neighbor['capability']['operational']: return @@ -163,6 +170,7 @@ def new(self, neighbor, restarted): self._operational(neighbor) self._extended_message(neighbor) self._hostname(neighbor) # https://datatracker.ietf.org/doc/html/draft-walton-bgp-hostname-capability-02 + self._software_version(neighbor) # https://datatracker.ietf.org/doc/html/draft-abraitis-bgp-version-capability self._session(neighbor) # MUST be the last key added, really !?! dict is not ordered ! return self diff --git a/src/exabgp/bgp/message/open/capability/capability.py b/src/exabgp/bgp/message/open/capability/capability.py index 05f13767b..cb6d8226f 100644 --- a/src/exabgp/bgp/message/open/capability/capability.py +++ b/src/exabgp/bgp/message/open/capability/capability.py @@ -37,6 +37,7 @@ class _CapabilityCode(int): MULTISESSION_CISCO = 0x83 # What Cisco really use for Multisession (yes this is a reserved range in prod !) HOSTNAME = 0x49 # https://datatracker.ietf.org/doc/html/draft-walton-bgp-hostname-capability-02 + SOFTWARE_VERSION = 0x4B # https://datatracker.ietf.org/doc/html/draft-abraitis-bgp-version-capability OPERATIONAL = 0xB9 # ExaBGP only ... # Internal @@ -61,6 +62,7 @@ class _CapabilityCode(int): MULTISESSION_CISCO: 'cisco-multi-sesion', AIGP: 'aigp', HOSTNAME: 'hostname', + SOFTWARE_VERSION: 'software-version', } def __new__(cls, value): @@ -105,6 +107,7 @@ class CODE(int): ROUTE_REFRESH_CISCO = _CapabilityCode(_CapabilityCode.ROUTE_REFRESH_CISCO) MULTISESSION_CISCO = _CapabilityCode(_CapabilityCode.MULTISESSION_CISCO) HOSTNAME = _CapabilityCode(_CapabilityCode.HOSTNAME) + SOFTRWARE_VERSION = _CapabilityCode(_CapabilityCode.SOFTWARE_VERSION) OPERATIONAL = _CapabilityCode(_CapabilityCode.OPERATIONAL) AIGP = _CapabilityCode(_CapabilityCode.AIGP) # fmt: on diff --git a/src/exabgp/bgp/message/open/capability/software_version.py b/src/exabgp/bgp/message/open/capability/software_version.py new file mode 100644 index 000000000..b7ce8f575 --- /dev/null +++ b/src/exabgp/bgp/message/open/capability/software_version.py @@ -0,0 +1,40 @@ +# encoding: utf-8 +""" +software_version.py + +Copyright (c) 2024 Donatas Abraitis. All rights reserved. +License: 3-clause BSD. (See the COPYRIGHT file) +""" + +# https://datatracker.ietf.org/doc/html/draft-abraitis-bgp-version-capability + + +from exabgp.bgp.message.open.capability.capability import Capability +from exabgp.version import version + + +@Capability.register() +class SoftwareVersion(Capability): + ID = Capability.CODE.SOFTRWARE_VERSION + SOFTWARE_VERSION_MAX_LEN = 64 + + def __init__(self): + self.software_version = f"ExaBGP/{version}" + + def __str__(self): + return f'SoftwareVersion({self.software_version})' + + def json(self): + return f'{ "software-version": "{self.software_version}" }' + + def extract(self): + software_version = self.software_version.encode('utf-8') + if len(software_version) > self.SOFTWARE_VERSION_MAX_LEN: + software_version = software_version[: self.SOFTWARE_VERSION_MAX_LEN] + return [bytes([len(software_version)]) + software_version] + + @staticmethod + def unpack_capability(instance, data, capability=None): # pylint: disable=W0613 + l1 = data[0] + instance.software_version = data[1 : l1 + 1].decode('utf-8') + return instance diff --git a/src/exabgp/bgp/neighbor.py b/src/exabgp/bgp/neighbor.py index d3f0593c6..388f4bf66 100644 --- a/src/exabgp/bgp/neighbor.py +++ b/src/exabgp/bgp/neighbor.py @@ -59,6 +59,7 @@ class Capability(dict): 'route-refresh': 0, 'nexthop': None, 'aigp': None, + 'software-version': None, } defaults = { @@ -406,7 +407,7 @@ def string(self, with_changes=True): '\tmanual-eor %s;\n' '%s%s%s%s%s%s%s%s%s%s%s\n' '\tcapability {\n' - '%s%s%s%s%s%s%s%s%s\t}\n' + '%s%s%s%s%s%s%s%s%s%s\t}\n' '\tfamily {%s\n' '\t}\n' '\tnexthop {%s\n' @@ -446,6 +447,8 @@ def string(self, with_changes=True): '\t\troute-refresh %s;\n' % ('enable' if self['capability']['route-refresh'] else 'disable'), '\t\tgraceful-restart %s;\n' % (self['capability']['graceful-restart'] if self['capability']['graceful-restart'] else 'disable'), + '\t\tsoftware-version %s;\n' + % ('enable' if self['capability']['software-version'] else 'disable'), '\t\tnexthop %s;\n' % ('enable' if self['capability']['nexthop'] else 'disable'), '\t\tadd-path %s;\n' % (AddPath.string[self['capability']['add-path']] if self['capability']['add-path'] else 'disable'), diff --git a/src/exabgp/configuration/capability.py b/src/exabgp/configuration/capability.py index d1c51200f..2e487a470 100644 --- a/src/exabgp/configuration/capability.py +++ b/src/exabgp/configuration/capability.py @@ -71,6 +71,7 @@ class ParseCapability(Section): ' operational enable|disable;\n' ' refresh enable|disable;\n' ' extended-message enable|disable;\n' + ' software-version enable|disable;\n' '}\n' ) @@ -84,6 +85,7 @@ class ParseCapability(Section): 'route-refresh': boolean, 'aigp': boolean, 'extended-message': boolean, + 'software-version': boolean, } action = { @@ -96,6 +98,7 @@ class ParseCapability(Section): 'route-refresh': 'set-command', 'aigp': 'set-command', 'extended-message': 'set-command', + 'software-version': 'set-command', } default = { @@ -107,6 +110,7 @@ class ParseCapability(Section): 'route-refresh': True, 'aigp': True, 'extended-message': True, + 'software-version': False, } name = 'capability' diff --git a/src/exabgp/configuration/neighbor/__init__.py b/src/exabgp/configuration/neighbor/__init__.py index 1329111b9..08f3f684e 100644 --- a/src/exabgp/configuration/neighbor/__init__.py +++ b/src/exabgp/configuration/neighbor/__init__.py @@ -44,6 +44,7 @@ from exabgp.configuration.neighbor.parser import description from exabgp.configuration.neighbor.parser import inherit from exabgp.configuration.neighbor.parser import rate_limit +from exabgp.version import version from exabgp.environment import getenv diff --git a/tests/decode_test.py b/tests/decode_test.py index 9e495dd6b..d4be0370b 100755 --- a/tests/decode_test.py +++ b/tests/decode_test.py @@ -33,7 +33,7 @@ from exabgp.bgp.message.update.nlri import NLRI from exabgp.bgp.message.direction import Direction - + from exabgp.logger import log from exabgp.environment import getenv @@ -328,6 +328,7 @@ def __init__(self): 'aigp': None, 'operational': None, 'extended-message': True, + 'software-version': False, }, })