From dd414b00cf38966ec51dafd4d3b4cf6aa0147e87 Mon Sep 17 00:00:00 2001 From: Thomas Mangin Date: Sun, 9 Jun 2024 14:43:34 +0100 Subject: [PATCH 001/100] add testing for python3.6 --- .github/workflows/ci.yaml | 66 -- ...deql-analysis.yml => codeql-analysis.yaml} | 13 - .github/workflows/functional-3.6.yaml | 46 ++ .github/workflows/functional-testing.yaml | 50 ++ .github/workflows/linting.yaml | 39 ++ .github/workflows/unit-testing.yaml | 41 ++ qa/bin/functional | 18 +- qa/bin/functional-3.6 | 526 ++++++++++++++++ qa/sbin/bgp-3.6 | 592 ++++++++++++++++++ src/exabgp/cli/command.py | 5 + src/exabgp/conf/yang/code.py | 6 + src/exabgp/conf/yang/parser.py | 6 + 12 files changed, 1322 insertions(+), 86 deletions(-) delete mode 100644 .github/workflows/ci.yaml rename .github/workflows/{codeql-analysis.yml => codeql-analysis.yaml} (72%) create mode 100644 .github/workflows/functional-3.6.yaml create mode 100644 .github/workflows/functional-testing.yaml create mode 100644 .github/workflows/linting.yaml create mode 100644 .github/workflows/unit-testing.yaml create mode 100755 qa/bin/functional-3.6 create mode 100755 qa/sbin/bgp-3.6 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index 92e0eb30b..000000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,66 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a single version of Python -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: CI - -on: - push: - branches: [ main, 4.2, 3.4 ] - pull_request: - branches: [ main ] - -permissions: - contents: read - -jobs: - build: - - # runs-on: ubuntu-latest - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - python-version: [3.6, 3.7, 3.8, 3.9, 3.10, 3.11, 3.12] - os: [ubuntu-latest] - - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5.1.0 - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - if [ -f qa/requirements.txt ]; then pip install -r qa/requirements.txt; fi - - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --exclude src/exabgp/vendoring/ --exclude build/ --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - - name: Configuration Parsing Checks - run: | - ./qa/bin/functional parsing - - - name: Functional Checks - run: | - ./qa/bin/functional encoding - - - name: Decoding Checks - run: | - ./qa/bin/functional decoding - - - name: Test Coverage - run: | - env PYTHONPATH=src exabgp_log_enable=false pytest --cov --cov-reset ./tests/*_test.py - - # - name: Coveralls - # run: | - # coveralls - -# - name: Test with pytest -# run: | -# pytest diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yaml similarity index 72% rename from .github/workflows/codeql-analysis.yml rename to .github/workflows/codeql-analysis.yaml index 8670e9f00..cafe464e2 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yaml @@ -1,14 +1,3 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# name: "CodeQL" on: @@ -33,8 +22,6 @@ jobs: fail-fast: false matrix: language: [ 'python' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://git.io/codeql-language-support steps: - name: Checkout repository diff --git a/.github/workflows/functional-3.6.yaml b/.github/workflows/functional-3.6.yaml new file mode 100644 index 000000000..cad9455e1 --- /dev/null +++ b/.github/workflows/functional-3.6.yaml @@ -0,0 +1,46 @@ +name: Functional Legacy + +on: + push: + branches: [ main, 4.2, 3.4 ] + pull_request: + branches: [ main ] + +permissions: + contents: read + +jobs: + build: + + # runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + python-version: [ "3.6" ] + os: [ "ubuntu-20.04" ] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5.1.0 + with: + python-version: ${{ matrix.python-version }} + + - name: run python version + run: | + python --version + + - name: Install dependencies + run: | + python -m pip install --no-cache-dir --upgrade pip + pip install --no-cache-dir -r requirements.txt + pip install psutil + + - name: change ownership to exa user + run : | + echo "EXABGP_DAEMON_USER=$(whoami)" >> $GITHUB_ENV + + - name: Python 3.6 Coverage + run: | + ./qa/bin/functional-3.6 run diff --git a/.github/workflows/functional-testing.yaml b/.github/workflows/functional-testing.yaml new file mode 100644 index 000000000..d4a8deeeb --- /dev/null +++ b/.github/workflows/functional-testing.yaml @@ -0,0 +1,50 @@ +name: Functional Testing + +on: + push: + branches: [ main, 4.2, 3.4 ] + pull_request: + branches: [ main ] + +permissions: + contents: read + +jobs: + build: + + # runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11", "3.12" ] + os: [ "ubuntu-latest" ] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5.1.0 + with: + python-version: ${{ matrix.python-version }} + + - name: run python version + run: | + python --version + + - name: Install dependencies + run: | + python -m pip install --no-cache-dir --upgrade pip + pip install --no-cache-dir -r requirements.txt + pip install psutil + + - name: Configuration + run: | + ./qa/bin/functional parsing + + - name: Functional + run: | + ./qa/bin/functional encoding + + - name: Decoding + run: | + ./qa/bin/functional decoding diff --git a/.github/workflows/linting.yaml b/.github/workflows/linting.yaml new file mode 100644 index 000000000..e3dd9a70e --- /dev/null +++ b/.github/workflows/linting.yaml @@ -0,0 +1,39 @@ +name: Linting + +on: + push: + branches: [ main, 4.2, 3.4 ] + pull_request: + branches: [ main ] + +permissions: + contents: read + +jobs: + build: + + # runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + python-version: [ "3.12" ] + os: [ "ubuntu-latest" ] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5.1.0 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install psutil + pip install flake8 + + - name: flake8 + run: | + flake8 . --exclude src/exabgp/vendoring/ --exclude build/ --exclude site-packages --count --select=E9,F63,F7,F82 --show-source --statistics diff --git a/.github/workflows/unit-testing.yaml b/.github/workflows/unit-testing.yaml new file mode 100644 index 000000000..d7b3644ad --- /dev/null +++ b/.github/workflows/unit-testing.yaml @@ -0,0 +1,41 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Unit Testing + +on: + push: + branches: [ main, 4.2, 3.4 ] + pull_request: + branches: [ main ] + +permissions: + contents: read + +jobs: + build: + + # runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11", "3.12" ] + os: [ "ubuntu-latest" ] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5.1.0 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-cov + + - name: pytest + run: | + env PYTHONPATH=src exabgp_log_enable=false pytest --cov --cov-reset ./tests/*_test.py diff --git a/qa/bin/functional b/qa/bin/functional index 764618586..58c6d0e1c 100755 --- a/qa/bin/functional +++ b/qa/bin/functional @@ -98,6 +98,7 @@ class Exec(object): self.code = -1 self.stdout = b'' self.stderr = b'' + self.message = '' self._process = None self.command = [] @@ -130,6 +131,7 @@ class Exec(object): print(f'return: {self.code}') print(f'stdout: {self.stdout}') print(f'stderr: {self.stderr}') + print(f'message: {self.message}') return False def collect(self): @@ -142,13 +144,15 @@ class Exec(object): signal.signal(signal.SIGALRM, alarm_handler) try: - signal.alarm(5) + signal.alarm(15) self.stdout, self.stderr = self._process.communicate() self.code = self._process.returncode signal.alarm(0) - except ValueError: # I/O operation on closed file + except ValueError as exc: # I/O operation on closed file + self.message = str(exc) pass except Alarm: + self.message = str(exc) pass def terminate(self): @@ -327,7 +331,7 @@ class EncodingTests(Tests): if self._check in self.stderr: return True - return self.failed() + return self.failed('completed successfully') API = re.compile(r'^\s*run\s+(.*)\s*?;\s*?$') @@ -498,10 +502,10 @@ class DecodingTests(Tests): def success(self): self.collect() - if not self.stdout: - return self.failed() if self.stderr: - return self.failed() + return self.failed('stderr is \n' + self.stderr) + if not self.stdout: + return self.failed('no stdout received') try: decoded = json.loads(self.stdout) self._cleanup(decoded) @@ -601,7 +605,7 @@ class ParsingTests(Tests): def success(self): self.collect() if self.code != 0: - return self.failed() + return self.failed('return code is not zero') return self.code == 0 diff --git a/qa/bin/functional-3.6 b/qa/bin/functional-3.6 new file mode 100755 index 000000000..8806b820c --- /dev/null +++ b/qa/bin/functional-3.6 @@ -0,0 +1,526 @@ +#!/usr/bin/env python3 +# encoding: utf-8 +""" +cache.py + +Created by Thomas Mangin +Copyright (c) 2013-2017 Exa Networks. All rights reserved. +License: 3-clause BSD. (See the COPYRIGHT file) +""" + +import os +import re +import sys +import glob +import time +import signal +import argparse +import itertools +import subprocess + +PROGRAM = os.path.realpath(__file__) +ROOT = os.path.abspath(os.path.join(os.path.dirname(PROGRAM), os.path.join('..', '..'))) +LIBRARY = os.path.join(ROOT, 'src') + +EXPLAIN = """ +ExaBGP command line +======================================================= + +%(client)s + + +bgp daemon command line +======================================================= + +%(server)s + + +The following extra configuration options could be used +======================================================= + +export exabgp_debug_rotate=true +export exabgp_debug_defensive=true +""" + + +class Color(object): + NONE = '\033[0m' + '\033[0m' + ' ' # NONE + STARTING = '\033[0m' + '\033[96m' + '~' # LIGHT BLUE + READY = '\033[0m' + '\033[94m' + '=' # PENDING + FAIL = '\033[0m' + '\033[91m' + '-' # RED + SUCCESS = '\033[1m' + '\033[92m' + '+' # GREEN + + +class Identifier(dict): + _listing = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzαβγδεζηθικλμνξοπρςστυφχψω' + _from_name = {} + _from_nick = {} + _next = 0 + _nl = 3 + + @classmethod + def get(cls, name): + letter = cls._listing[cls._next] + cls._from_name[name] = letter + cls._from_nick[letter] = name + cls._next += 1 + return letter + + @classmethod + def identifiers(cls): + for n in range(0, cls._next): + yield cls._listing[n], not (n + 1) % cls._nl + + @classmethod + def nick(cls, name): + return cls._from_name[name] + + @classmethod + def name(cls, nick): + return cls._from_nick[nick] + + +class Port(object): + base = 1790 + + @classmethod + def get(cls): + current = cls.base + cls.base += 1 + return current + + +class Path(object): + ETC = os.path.join(ROOT, 'etc', 'exabgp') + EXABGP = os.path.join(ROOT, 'sbin', 'exabgp') + BGP = os.path.join(ROOT, 'qa', 'sbin', 'bgp-3.6') + CI = os.path.join(os.path.join(ROOT, 'qa', 'encoding')) + ALL_CI = glob.glob(os.path.join(CI, '*.ci')) + ALL_CI.sort() + + @classmethod + def validate(cls): + if not os.path.isdir(cls.ETC): + sys.exit('could not find etc folder') + + if not os.path.isdir(cls.CI): + sys.exit('could not find tests in the qa/ci folder') + + if not os.path.isfile(cls.EXABGP): + sys.exit('could not find exabgp') + + if not os.path.isfile(cls.BGP): + sys.exit('could not find the sequence daemon') + + +class CI(dict): + API = re.compile(r'^\s*run\s+(.*)\s*?;\s*?$') + _content = {} + _status = {} + _tests = [] + + @classmethod + def make(cls): + for filename in Path.ALL_CI: + name, extension = os.path.splitext(filename.split('/')[-1]) + if name in ['api-reload', 'api-notification', 'conf-ebgp', 'conf-ipself6']: + continue + nick = Identifier.get(name) + with open(filename, 'r') as reader: + content = reader.readline() + cls._content[nick] = { + 'name': name, + 'confs': [os.path.join(Path.ETC, _) for _ in content.split()], + 'ci': os.path.join(Path.CI, name) + '.ci', + 'msg': os.path.join(Path.CI, name) + '.msg', + 'port': Port.get(), + } + cls._tests.extend(sorted(cls._content.keys())) + + @classmethod + def get(cls, k): + return cls._content.get(k, None) + + @classmethod + def state(cls, name): + if name not in cls._status: + cls._status[name] = Color.NONE + elif cls._status[name] == Color.NONE: + cls._status[name] = Color.STARTING + elif cls._status[name] == Color.STARTING: + cls._status[name] = Color.READY + + @classmethod + def color(cls, name): + return cls._status.get(name, Color.NONE) + + @classmethod + def reset(cls, name): + cls._status[name] = Color.NONE + + @classmethod + def passed(cls, name): + cls._status[name] = Color.SUCCESS + + @classmethod + def failed(cls, name): + cls._status[name] = Color.FAIL + + @classmethod + def files(cls, k): + test = cls._content.get(k, None) + if not test: + return [] + files = [ + test['msg'], + ] + for f in test['confs']: + files.append(f) + with open(f) as reader: + for line in reader: + found = cls.API.match(line) + if not found: + continue + name = found.group(1) + if not name.startswith('/'): + name = os.path.abspath(os.path.join(Path.ETC, name)) + if name not in files: + files.append(name) + return [f for f in files if os.path.isfile(f)] + + @classmethod + def display(cls): + # sys.stdout.write('\r') + for k in cls._tests: + sys.stdout.write('%s%s ' % (CI.color(k), k)) + sys.stdout.write(Color.NONE) + # same line printing now buggy + sys.stdout.write('\r') + sys.stdout.flush() + + @classmethod + def listing(cls): + sys.stdout.write('\n') + sys.stdout.write('The available functional tests are:\n') + sys.stdout.write('\n') + for index, nl in Identifier.identifiers(): + name = cls._content[index]['name'] + sys.stdout.write(' %-2s %s%s' % (index, name, ' ' * (25 - len(name)))) + sys.stdout.write('\n' if nl else '') + sys.stdout.write('\n') + sys.stdout.write('\n') + sys.stdout.write('\n') + sys.stdout.write('checking\n') + sys.stdout.write('\n') + sys.stdout.flush() + + +Path.validate() +CI.make() +# CI.display() + + +class Alarm(Exception): + pass + + +def alarm_handler(number, frame): # pylint: disable=W0613 + raise Alarm() + + +class Process(object): + _running = {} + _result = {} + + @classmethod + def add(cls, name, side, process): + cls._running.setdefault(name, {})[side] = process + for std in ('in', 'out'): + cls._result.setdefault(name, {}).setdefault(side, {})[std] = b'' + + @classmethod + def success(cls, name): + return b'successful' in cls._result[name]['server']['out'] + + @classmethod + def _ready(cls, side, name): + try: + signal.alarm(1) + polled = cls._running[side][name].poll() + signal.alarm(0) + except Alarm: + return False + except (IOError, OSError, ValueError): + return True + if polled is None: + return False + return True + + @classmethod + def collect(cls, name, side): + try: + signal.alarm(1) + stdout, stderr = cls._running[name][side].communicate() + signal.alarm(0) + cls._result[name][side]['out'] = stdout + cls._result[name][side]['err'] = stderr + except ValueError: # I/O operation on closed file + pass + except Alarm: + pass + + @classmethod + def output(cls, name, side): + return cls._result[name][side]['out'] + + def error(cls, name, side): + return cls._result[name][side]['err'] + + @classmethod + def _terminate(cls, name, side): + try: + cls._running[name][side].send_signal(signal.SIGTERM) + except OSError: # No such process, Errno 3 + pass + + @classmethod + def terminate(cls): + for name in cls._running: + for side in cls._running[name]: + if cls.output(name, side) != '' or cls.error(name, side) != '': + continue + cls._terminate(name, side) + cls.collect(name, side) + + +class Command(dict): + @staticmethod + def execute(cmd): + print('starting: %s' % ' '.join(cmd)) + popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + for line in itertools.chain(iter(popen.stdout.readline, ''), iter(popen.stderr.readline, '')): + yield line + popen.stdout.close() + return_code = popen.wait() + if return_code: + raise subprocess.CalledProcessError(return_code, cmd) + + @classmethod + def explain(cls, index): + print( + EXPLAIN % {'client': cls.client(index), 'server': cls.server(index),} + ) + sys.exit(1) + + @staticmethod + def client(index): + test = CI.get(index) + if not test: + sys.exit("can not find any test called '%s'" % index) + + if os.getuid() and os.getgid() and test['port'] <= 1024: + sys.exit('you need to have root privileges to bind to port 79') + + config = { + 'env': ' \\\n '.join( + [ + 'exabgp_tcp_once=true', + 'exabgp_api_cli=false', + 'exabgp_debug_rotate=true', + 'exabgp_debug_configuration=true', + 'exabgp_tcp_bind=\'\'', + 'exabgp_tcp_port=%d' % test['port'], + 'INTERPRETER=%s ' % os.environ.get('__PYVENV_LAUNCHER__', sys.executable), + ] + ), + 'exabgp': Path.EXABGP, + 'confs': ' \\\n '.join(test['confs']), + } + return 'env \\\n %(env)s \\\n %(exabgp)s -d -p \\\n %(confs)s' % config + + @staticmethod + def server(index): + test = CI.get(index) + + if not test: + sys.exit("can not find any test called '%s'" % index) + + if os.getuid() and os.getgid() and test['port'] <= 1024: + sys.exit('you need to have root privileges to bind to port 79') + + config = { + 'env': ' \\\n '.join(['exabgp_tcp_port=%d' % test['port'],]), + 'interpreter': os.environ.get('__PYVENV_LAUNCHER__', sys.executable), + 'bgp': Path.BGP, + 'msg': test['msg'], + } + + return 'env \\\n %(env)s \\\n %(interpreter)s %(bgp)s \\\n %(msg)s' % config + + @staticmethod + def dispatch(running, timeout): + completed = True + names = [] + for name in running: + if CI.get(name) is None: + sys.exit("can not find any test called '%s'" % name) + CI.state(name) + names.append(name) + + for side in ['server', 'client']: + for name in running: + process = subprocess.Popen( + [sys.argv[0], side, name, '--port', str(CI.get(name)['port'])], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + Process.add(name, side, process) + CI.state(name) + CI.display() + time.sleep(0.02) + + exit_time = time.time() + timeout + + while names and time.time() < exit_time: + CI.display() + for name in list(names): + for side in ('server', 'client'): + if not Process._ready(name, side): + continue + + Process.collect(name, side) + + if side == 'server': + names.remove(name) + + if Process.success(name): + CI.passed(name) + else: + CI.failed(name) + completed = False + + CI.display() + time.sleep(0.2) + + Process.terminate() + + for name in names: + print('server stderr\n------\n%s' % str(Process.output(name, 'server')).replace('\\n', '\n')) + print('client stdout\n------\n%s' % str(Process.output(name, 'client')).replace('\\n', '\n')) + + CI.display() + return completed + + +def _run(to_run, chunk, timeout): + success = True + while to_run and success: + running, to_run = to_run[:chunk], to_run[chunk:] + success = Command.dispatch(running, timeout) + sys.stdout.write('\n') + + sys.exit(0 if success else 1) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='The BGP swiss army knife of networking functional testing tool') + subparsers = parser.add_subparsers() + + def all(parsed): + to_run = [index for index, _ in Identifier.identifiers()] + chunk = 1 + _run(to_run, chunk, parsed.timeout) + + sub = subparsers.add_parser('all', help='run all available test') + sub.add_argument('--timeout', help='timeout for test failure', type=int, default=60) + sub.add_argument('--port', help='base port to use', type=int, default=1790) + sub.set_defaults(func=all) + + def run(parsed): + Port.base = parsed.port + if parsed.test: + to_run = [ + parsed.test, + ] + else: + to_run = [index for index, _ in Identifier.identifiers()] + chunk = len(to_run) if not parsed.steps else parsed.steps + _run(to_run, chunk, parsed.timeout) + + sub = subparsers.add_parser('run', help='run a particular test') + sub.add_argument('test', help='name of the test to run', nargs='?', default=None) + sub.add_argument('--timeout', help='timeout for test failure', type=int, default=60) + sub.add_argument('--port', help='base port to use', type=int, default=1790) + sub.add_argument('--steps', help='number of test to run simultaneously', type=int, default=0) + sub.set_defaults(func=run) + + def client(parsed): + command = Command.client(parsed.test) + print(f'> {command}') + if not parsed.dry: + sys.exit(os.system(command)) + sys.exit(0) + + sub = subparsers.add_parser('client', help='start the client for a specific test') + sub.add_argument('test', help='name of the test to run') + sub.add_argument('-d', '--dry', help='show what command would be run but does nothing', action='store_true') + sub.add_argument('--timeout', help='timeout for test failure', type=int, default=60) + sub.add_argument('--port', help='base port to use', type=int, default=1790) + sub.set_defaults(func=client) + + def server(parsed): + command = Command.server(parsed.test) + print(f'> {command}') + if not parsed.dry: + sys.exit(os.system(command)) + sys.exit(0) + + sub = subparsers.add_parser('server', help='start the server for a specific test') + sub.add_argument('test', help='name of the test to run') + sub.add_argument('-d', '--dry', help='show what command would be run but does nothing', action='store_true') + sub.add_argument('--timeout', help='timeout for test failure', type=int, default=60) + sub.add_argument('--port', help='base port to use', type=int, default=1790) + sub.set_defaults(func=server) + + def explain(parsed): + Command.explain(parsed.test) + sys.exit(0) + + sub = subparsers.add_parser('explain', help='show what command for a test are run') + sub.add_argument('test', help='name of the test to explain') + sub.add_argument('--timeout', help='timeout for test failure', type=int, default=60) + sub.add_argument('--port', help='base port to use', type=int, default=1790) + sub.set_defaults(func=explain) + + def edit(parsed): + files = CI.files(parsed.test) + if not files: + sys.exit('no such test') + editor = os.environ.get('EDITOR', 'vi') + os.system('%s %s' % (editor, ' '.join(files))) + sys.exit(0) + + sub = subparsers.add_parser('edit', help='start $EDITOR to edit a specific test') + sub.add_argument('test', help='name of the test to edit') + sub.set_defaults(func=edit) + + def decode(parsed): + test = CI.get(parsed.test) + command = '%s decode %s "%s"' % (Path.EXABGP, test['confs'][0], ''.join(parsed.payload)) + print('> %s' % command) + os.system(command) + sys.exit(0) + + sub = subparsers.add_parser('decode', help='use the test configuration to decode a packet') + sub.add_argument('test', help='name of the test to use to know the BGP configuration') + sub.add_argument('payload', nargs='+', help='the hexadecimal representation of the packet') + sub.set_defaults(func=decode) + + sub = subparsers.add_parser('listing', help='list all functional test available') + sub.set_defaults(func=lambda _: CI.listing()) + + parsed = parser.parse_args() + if vars(parsed): + parsed.func(parsed) + else: + parser.print_help() \ No newline at end of file diff --git a/qa/sbin/bgp-3.6 b/qa/sbin/bgp-3.6 new file mode 100755 index 000000000..b0f42f63e --- /dev/null +++ b/qa/sbin/bgp-3.6 @@ -0,0 +1,592 @@ +#!/usr/bin/env python3 +# encoding: utf-8 +""" +bgp + +Created by Thomas Mangin +Copyright (c) 2013-2017 Exa Networks. All rights reserved. +License: 3-clause BSD. (See the COPYRIGHT file) +""" + +import os +import pwd +import sys +import time +import errno +import socket +import threading +import signal +import asyncore +import subprocess +from struct import unpack + +SIGNAL = dict([(name, getattr(signal, name)) for name in dir(signal) if name.startswith('SIG')]) + + +def flushed(*output): + print(' '.join(str(_) for _ in output)) + sys.stdout.flush() + + +def bytestream(value): + return ''.join(['%02X' % _ for _ in value]) + + +def dump(value): + def spaced(value): + even = None + for v in value: + if even is False: + yield ' ' + yield '%02X' % v + even = not even + + return ''.join(spaced(value)) + + +def cdr_to_length(cidr): + if cidr > 24: + return 4 + if cidr > 16: + return 3 + if cidr > 8: + return 2 + if cidr > 0: + return 1 + return 0 + + +class BGPHandler(asyncore.dispatcher_with_send): + counter = 0 + + keepalive = bytearray([0xFF,] * 16 + [0x0, 0x13, 0x4]) + + _name = { + b'\x01': 'OPEN', + b'\x02': 'UPDATE', + b'\x03': 'NOTIFICATION', + b'\x04': 'KEEPALIVE', + } + + def signal(self, myself, signal_name='SIGUSR1'): + signal_number = SIGNAL.get(signal_name, '') + if not signal_number: + self.announce('invalid signal name in configuration : %s' % signal_name) + self.announce('options are: %s' % ','.join(SIGNAL.keys())) + sys.exit(1) + + conf_name = sys.argv[1].split('/')[-1].split('.')[0] + + processes = [] + + for line in os.popen("/bin/ps x"): + low = line.strip().lower() + if not low: + continue + if 'python' not in low and 'pypy' not in low: + continue + + cmdline = line.strip().split()[4:] + pid = line.strip().split()[0] + + if len(cmdline) > 1 and not cmdline[1].endswith('/bgp.py'): + continue + + if conf_name not in cmdline[-1]: + continue + + if not cmdline[-1].endswith('.conf'): + continue + + processes.append(pid) + + if len(processes) == 0: + self.announce('no running process found, this should not happend, quitting') + sys.exit(1) + + if len(processes) > 1: + self.announce('more than one process running, this should not happend, quitting') + sys.exit(1) + + try: + self.announce('sending signal %s to ExaBGP (pid %s)\n' % (signal_name, processes[0])) + os.kill(int(processes[0]), signal_number) + except Exception as exc: + self.announce('\n failed: %s' % str(exc)) + + def kind(self, header): + return header[18] + + def isupdate(self, header): + return header[18] == 2 + + def isnotification(self, header): + return header[18] == 4 + + def name(self, header): + return self._name.get(header[18], 'SOME WEIRD RFC PACKET') + + def routes(self, header, body): + len_w = unpack('!H', body[0:2])[0] + withdrawn = bytearray([_ for _ in body[2 : 2 + len_w]]) + len_a = unpack('!H', body[2 + len_w : 2 + len_w + 2])[0] + announced = bytearray([_ for _ in body[2 + len_w + 2 + len_a :]]) + + if not withdrawn and not announced: + if len(body) == 4: + yield 'eor:1:1' + elif len(body) == 11: + yield 'eor:%d:%d' % (body[-2], body[-1]) + else: # undecoded MP route + yield 'mp:' + return + + while withdrawn: + cdr, withdrawn = withdrawn[0], withdrawn[1:] + size = cdr_to_length(cdr) + r = [0, 0, 0, 0] + for index in range(size): + r[index], withdrawn = withdrawn[0], withdrawn[1:] + yield 'withdraw:%s' % '.'.join(str(_) for _ in r) + '/' + str(cdr) + + while announced: + cdr, announced = announced[0], announced[1:] + size = cdr_to_length(cdr) + r = [0, 0, 0, 0] + for index in range(size): + r[index], announced = announced[0], announced[1:] + yield 'announce:%s' % '.'.join(str(_) for _ in r) + '/' + str(cdr) + + def notification(self, header, body): + yield 'notification:%d,%d' % (body[0], body[1]), bytestream(body) + + def announce(self, *args): + flushed(' ', self.ip, self.port, ' '.join(str(_) for _ in args) if len(args) > 1 else args[0]) + + def check_signal(self): + if self.messages and self.messages[0].startswith('signal:'): + name = self.messages.pop(0).split(':')[-1] + self.signal(os.getppid(), name) + + def setup(self, ip, port, messages, options): + self.ip = ip + self.port = port + self.options = options + self.handle_read = self.handle_open + self.sequence = {} + self.raw = False + for rule in messages: + sequence, announcement = rule.split(':', 1) + if announcement.startswith('raw:'): + self.raw = True + announcement = ''.join(announcement[4:].replace(':', '')) + self.sequence.setdefault(sequence, []).append(announcement) + self.update_sequence() + return self + + def update_sequence(self): + if self.options['sink'] or self.options['echo']: + self.messages = [] + return True + keys = sorted(list(self.sequence)) + if keys: + key = keys[0] + self.messages = self.sequence[key] + self.step = key + del self.sequence[key] + + self.check_signal() + # we had a list with only one signal + if not self.messages: + return self.update_sequence() + return True + return False + + def read_message(self): + header = b'' + while len(header) != 19: + try: + left = 19 - len(header) + header += self.recv(left) + if left == 19 - len(header): # ugly + # the TCP session is gone. + return None, None + except socket.error as exc: + if exc.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN): + continue + raise exc + + length = unpack('!H', header[16:18])[0] - 19 + + body = b'' + while len(body) != length: + try: + left = length - len(body) + body += self.recv(left) + except socket.error as exc: + if exc.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN): + continue + raise exc + + return bytearray(header), bytearray(body) + + def handle_open(self): + # reply with a IBGP response with the same capability (just changing routerID) + header, body = self.read_message() + routerid = bytearray([body[8] + 1 & 0xFF]) + o = header + body[:8] + routerid + body[9:] + + if self.options['send-unknown-capability']: + # hack capability 66 into the message + + content = b'loremipsum' + cap66 = bytearray([66, len(content)]) + content + param = bytearray([2, len(cap66)]) + cap66 + o = o[:17] + bytearray([o[17] + len(param)]) + o[18:28] + bytearray([o[28] + len(param)]) + o[29:] + param + + self.send(o) + self.send(self.keepalive) + + if self.options['send-default-route']: + self.send( + bytearray( + [0xFF,] * 16 + + [0x00, 0x31] + + [0x02,] + + [0x00, 0x00] + + [0x00, 0x15] + + [] + + [0x40, 0x01, 0x01, 0x00] + + [] + + [0x40, 0x02, 0x00] + + [] + + [0x40, 0x03, 0x04, 0x7F, 0x00, 0x00, 0x01] + + [] + + [0x40, 0x05, 0x04, 0x00, 0x00, 0x00, 0x64] + + [0x20, 0x00, 0x00, 0x00, 0x00] + ) + ) + self.announce('sending default-route\n') + + self.handle_read = self.handle_keepalive + + def handle_keepalive(self): + header, body = self.read_message() + + if header is None: + self.announce('connection closed') + self.close() + if self.options['send-notification']: + self.announce('successful') + sys.exit(0) + return + + if self.raw: + + def parser(self, header, body): + if body: + yield bytestream(header + body) + + else: + parser = self._decoder.get(self.kind(header), None) + + if self.options['sink']: + self.announce( + 'received %d: %s' + % ( + self.counter, + '%s:%s:%s:%s' + % (bytestream(header[:16]), bytestream(header[16:18]), bytestream(header[18:]), bytestream(body)), + ) + ) + self.send(self.keepalive) + return + + if self.options['echo']: + self.announce( + 'received %d: %s' + % ( + self.counter, + '%s:%s:%s:%s' + % (bytestream(header[:16]), bytestream(header[16:18]), bytestream(header[18:]), bytestream(body)), + ) + ) + self.send(header + body) + self.announce( + 'sent %d: %s' + % ( + self.counter, + '%s:%s:%s:%s' + % (bytestream(header[:16]), bytestream(header[16:18]), bytestream(header[18:]), bytestream(body)), + ) + ) + return + + if parser: + for announcement in parser(self, header, body): + self.send(self.keepalive) + if announcement.startswith('eor:'): # skip EOR + self.announce('skipping eor', announcement) + continue + + if announcement.startswith('mp:'): # skip unparsed MP + self.announce('skipping multiprotocol :', dump(body)) + continue + + self.counter += 1 + + if announcement in self.messages: + self.messages.remove(announcement) + if self.raw: + self.announce( + 'received %d (%1s%s):' % (self.counter, self.options['letter'], self.step), + '%s:%s:%s:%s' + % (announcement[:32], announcement[32:36], announcement[36:38], announcement[38:]), + ) + else: + self.announce( + 'received %d (%1s%s):' % (self.counter, self.options['letter'], self.step), announcement + ) + self.check_signal() + else: + if self.raw: + self.announce( + 'received %d (%1s%s):' % (self.counter, self.options['letter'], self.step), + '%s:%s:%s:%s' + % ( + bytestream(header[:16]), + bytestream(header[16:18]), + bytestream(header[18:]), + bytestream(body), + ), + ) + else: + self.announce('received %d :' % self.counter, announcement) + + if len(self.messages) > 1: + self.announce('expected one of the following :') + for message in self.messages: + if message.startswith('F' * 32): + self.announce( + ' %s:%s:%s:%s' + % (message[:32], message[32:36], message[36:38], message[38:]) + ) + else: + self.announce(' %s' % message) + elif self.messages: + message = self.messages[0].upper() + if message.startswith('F' * 32): + self.announce('expected : %s:%s:%s:%s' % (message[:32], message[32:36], message[36:38], message[38:])) + else: + self.announce('expected : %s' % message) + else: + # can happen when the thread is still running + self.announce('extra data') + sys.exit(1) + + sys.exit(1) + + if not self.messages: + if self.options['single-shot']: + self.announce('successful (partial test)') + sys.exit(0) + + if not self.update_sequence(): + if self.options['exit']: + self.announce('successful') + sys.exit(0) + else: + self.send(self.keepalive) + + if self.options['send-notification']: + notification = b'closing session because we can' + self.send( + bytearray([0xFF,] * 16 + [0x00, 19 + 2 + len(notification)] + [0x03] + [0x06] + [0x00]) + notification + ) + + _decoder = { + 2: routes, + 3: notification, + } + + +class BGPServer(asyncore.dispatcher): + def announce(self, *args): + flushed(' ' + ' '.join(str(_) for _ in args) if len(args) > 1 else args[0]) + + def __init__(self, host, options): + asyncore.dispatcher.__init__(self) + + if ':' in host: + self.create_socket(socket.AF_INET6, socket.SOCK_STREAM) + else: + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.set_reuse_addr() + self.bind((host, options['port'])) + self.listen(5) + + self.messages = {} + + self.options = { + 'send-unknown-capability': False, # add an unknown capability to the open message + 'send-default-route': False, # send a default route to the peer + 'send-notification': False, # send notification messages to the backend + 'signal-SIGUSR1': 0, # send SIGUSR1 after X seconds + 'single-shot': False, # we can not test signal on python 2.6 + 'sink': False, # just accept whatever is sent + 'echo': False, # just accept whatever is sent + } + self.options.update(options) + + for message in options['messages']: + if message.strip() == 'option:open:send-unknown-capability': + self.options['send-unknown-capability'] = True + continue + if message.strip() == 'option:update:send-default-route': + self.options['send-default-route'] = True + continue + if message.strip() == 'option:notification:send-notification': + self.options['send-notification'] = True + continue + if message.strip().startswith('option:SIGUSR1:'): + + def notify(delay, myself): + time.sleep(delay) + self.signal(myself) + time.sleep(10) + + # Python 2.6 can not perform this test as it misses the function + if 'check_output' in dir(subprocess): + # thread.start_new_thread(notify,(int(message.split(':')[-1]),os.getpid())) + threading.Thread(target=notify, args=(int(message.split(':')[-1]), os.getpid())) + else: + self.options['single-shot'] = True + continue + + if message[0].isalpha(): + index, content = message[:1].upper(), message[1:] + else: + index, content = 'A', message + self.messages.setdefault(index, []).append(content) + + def handle_accept(self): + messages = None + for number in range(ord('A'), ord('Z') + 1): + letter = chr(number) + if letter in self.messages: + messages = self.messages[letter] + del self.messages[letter] + break + + if self.options['sink']: + flushed('\nsink mode - send us whatever, we can take it ! :p\n') + messages = [] + elif self.options['echo']: + flushed('\necho mode - send us whatever, we can parrot it ! :p\n') + messages = [] + elif not messages: + self.announce('we used all the test data available, can not handle this new connection') + sys.exit(1) + else: + flushed('using :\n ', '\n '.join(messages), '\n\nconversation:\n') + + self.options['exit'] = not len(self.messages.keys()) + self.options['letter'] = letter + + pair = self.accept() + if pair is not None: + sock, addr = pair + handler = BGPHandler(sock).setup(*addr[:2], messages=messages, options=self.options) + + +def drop(): + uid = os.getuid() + gid = os.getgid() + + if uid and gid: + return + + for name in [ + 'nobody', + ]: + try: + user = pwd.getpwnam(name) + nuid = int(user.pw_uid) + ngid = int(user.pw_uid) + except KeyError: + pass + + if not gid: + os.setgid(ngid) + if not uid: + os.setuid(nuid) + + +def main(): + port = os.environ.get('exabgp.tcp.port', os.environ.get('exabgp_tcp_port', '179')) + + if not port.isdigit() and port > 0 and port <= 65535 or len(sys.argv) <= 1: + flushed('--sink accept any BGP messages and reply with a keepalive') + flushed('--echo accept any BGP messages send it back to the emiter') + flushed('--port port to bind to') + flushed( + 'a list of expected route announcement/withdrawl in the format :announce: :withdraw: :raw:' + ) + flushed('for example:', sys.argv[0], '1:announce:10.0.0.0/8 1:announce:192.0.2.0/24 2:withdraw:10.0.0.0/8 ') + flushed('routes with the same can arrive in any order') + sys.exit(1) + + options = {'sink': False, 'echo': False, 'port': int(port), 'messages': []} + + for arg in sys.argv[1:]: + if arg == '--sink': + messages = [] + options['sink'] = True + continue + + if arg == '--echo': + messages = [] + options['echo'] = True + continue + + if arg == '--port': + args = sys.argv[1:] + [ + '', + ] + port = args[args.index('--port') + 1] + if port.isdigit() and int(port) > 0: + options['port'] = int(port) + continue + print('invalid port %s' % port) + sys.exit(1) + + if arg == str(options['port']): + continue + + try: + with open(sys.argv[1]) as content: + options['messages'] = [_.strip() for _ in content.readlines() if _.strip() and '#' not in _] + except IOError: + flushed('could not open file', sys.argv[1]) + sys.exit(1) + + try: + BGPServer('127.0.0.1', options) + try: + BGPServer('::1', options) + except: + # does not work on travis-ci + pass + drop() + asyncore.loop() + except socket.error as exc: + if exc.errno == errno.EACCES: + flushed('failure: could not bind to port %s - most likely not run as root' % port) + elif exc.errno == errno.EADDRINUSE: + flushed('failure: could not bind to port %s - port already in use' % port) + else: + flushed('failure', str(exc)) + + +if __name__ == '__main__': + main() diff --git a/src/exabgp/cli/command.py b/src/exabgp/cli/command.py index 0ba2dffd9..4e13b38a6 100644 --- a/src/exabgp/cli/command.py +++ b/src/exabgp/cli/command.py @@ -6,6 +6,11 @@ from vyos.util import call from vyos.util import popen +if sys.version_info[:3] < (3,7): + def breakpoint(): + import pdb; + pdb.set_trace() + pass def _nop(config, path): order = path[0] diff --git a/src/exabgp/conf/yang/code.py b/src/exabgp/conf/yang/code.py index 5a0f8a4d5..bdd9f2e8e 100644 --- a/src/exabgp/conf/yang/code.py +++ b/src/exabgp/conf/yang/code.py @@ -22,6 +22,12 @@ from exabgp.conf.yang.datatypes import kw from exabgp.conf.yang.datatypes import ranges +import sys +if sys.version_info[:3] < (3,7): + def breakpoint(): + import pdb; + pdb.set_trace() + pass class Code(object): def __init__(self, tree): diff --git a/src/exabgp/conf/yang/parser.py b/src/exabgp/conf/yang/parser.py index 52f65b5af..f95525955 100644 --- a/src/exabgp/conf/yang/parser.py +++ b/src/exabgp/conf/yang/parser.py @@ -16,6 +16,12 @@ from exabgp.conf.yang.datatypes import words from exabgp.conf.yang.datatypes import types +import sys +if sys.version_info[:3] < (3,7): + def breakpoint(): + import pdb; + pdb.set_trace() + pass DEBUG = True From 0500f8ac54c63186148a91528ae11b8ccca1a45e Mon Sep 17 00:00:00 2001 From: Thomas Mangin Date: Wed, 12 Jun 2024 21:36:44 +0100 Subject: [PATCH 002/100] allow bgp to test more than 10 grouped rules --- qa/sbin/bgp | 10 +++++++--- qa/sbin/bgp-3.6 | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/qa/sbin/bgp b/qa/sbin/bgp index 2b9ef3a0e..e9eed7d50 100755 --- a/qa/sbin/bgp +++ b/qa/sbin/bgp @@ -396,13 +396,17 @@ class Checker(object): rule = rule.replace(' ', '').lower() try: - seq, encoding, content = rule.split(':', 2) + prefix, encoding, content = rule.split(':', 2) except ValueError: flushed(f'invalid rule: {rule}') sys.exit(0) - conn = seq[0] if seq[0].isalpha() else 'A' - seq = seq[1:] if seq[0].isalpha() else seq + conn = prefix[0] + if conn.isalpha(): + seq = int(prefix[1:]) + else: + conn = 'A' + seq = int(prefix) raw = (encoding == 'raw') self.raw = self.raw or raw diff --git a/qa/sbin/bgp-3.6 b/qa/sbin/bgp-3.6 index b0f42f63e..0f155b168 100755 --- a/qa/sbin/bgp-3.6 +++ b/qa/sbin/bgp-3.6 @@ -180,7 +180,7 @@ class BGPHandler(asyncore.dispatcher_with_send): if announcement.startswith('raw:'): self.raw = True announcement = ''.join(announcement[4:].replace(':', '')) - self.sequence.setdefault(sequence, []).append(announcement) + self.sequence.setdefault(int(sequence), []).append(announcement) self.update_sequence() return self From be3644dc9a845645be73d3e57c512c950596b11f Mon Sep 17 00:00:00 2001 From: Thomas Mangin Date: Wed, 12 Jun 2024 21:38:49 +0100 Subject: [PATCH 003/100] more testing for #1209 --- etc/exabgp/run/api-flow-merge.run | 7 ++++++- qa/encoding/api-flow-merge.msg | 9 +++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/etc/exabgp/run/api-flow-merge.run b/etc/exabgp/run/api-flow-merge.run index a5a19017c..02b66a6ca 100755 --- a/etc/exabgp/run/api-flow-merge.run +++ b/etc/exabgp/run/api-flow-merge.run @@ -12,7 +12,12 @@ messages = [ 'announce flow route { match { source 5.5.5.5/32; } then { discard; } }', 'announce flow route { match { source 6.6.6.6/32; } scope { interface-set [ non-transitive:input:3405770241:1 ]; } then { discard; } }', 'announce flow route { match { source 7.7.7.7/32; } scope { interface-set [ non-transitive:input:3405770241:1 transitive:output:254:254 ]; } then { discard; } }', - # 'announce flow route { match { source 8.8.8.8/32; } scope { interface-set [ transitive:input-output:1234:10 transitive:input:1234:10 transitive:output:0:0]; } then { discard; } }', + 'announce flow route { match { source 8.8.8.8/32; } then { discard; } scope { interface-set [ non-transitive:input:3405770241:1 transitive:output:254:254 ]; } }', + 'announce flow route { match { source 9.9.9.9/32; } then { discard; } scope { interface-set [ non-transitive:input:3405770241:1 transitive:output:254:254 ]; } }', + 'announce flow route { match { source 10.10.10.10/32; } scope { interface-set [ transitive:input-output:1234:10 transitive:input:1234:10 transitive:output:0:0]; } then { discard; } }', + + # 'announce flow route destination 133.130.1.219/32 discard interface-set [ transitive:input:1234:10]', + # 'announce flow route destination 133.130.1.19/32 interface-set [ transitive:input:1234:10 ] discard', ] while messages: diff --git a/qa/encoding/api-flow-merge.msg b/qa/encoding/api-flow-merge.msg index a95ecaf9f..8d740bf2e 100644 --- a/qa/encoding/api-flow-merge.msg +++ b/qa/encoding/api-flow-merge.msg @@ -1,11 +1,16 @@ 1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0047:02:000000304001010040020040050400000064C010104702CAFFEE0140018006000000000000800E0C0001850000060220CAFFEE01 -1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:004F:02:000000384001010040020040050400000064C010184702CAFFEE0140010702000000FE80FE8006000000000000800E0C0001850000060220CAFFEE01 +2:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:004F:02:000000384001010040020040050400000064C010184702CAFFEE0140010702000000FE80FE8006000000000000800E0C0001850000060220CAFFEE01 3:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:001E:02:00000007900F0003000185 4:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:003F:02:000000284001010040020040050400000064C010088006000000000000800E0C000185000006022004040404 5:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:003F:02:000000284001010040020040050400000064C010088006000000000000800E0C000185000006022005050505 6:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0047:02:000000304001010040020040050400000064C010104702CAFFEE0140018006000000000000800E0C000185000006022006060606 -# 7:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:004F:02:000000384001010040020040050400000064C010184702CAFFEE0140010702000000FE80FE8006000000000000800E0C000185000006022007070707 +7:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:004F:02:000000384001010040020040050400000064C0101880060000000000004702CAFFEE0140010702000000FE80FE800E0C000185000006022007070707 +8:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:004F:02:000000384001010040020040050400000064C0101880060000000000004702CAFFEE0140010702000000FE80FE800E0C000185000006022008080808 +9:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:004F:02:000000384001010040020040050400000064C0101880060000000000004702CAFFEE0140010702000000FE80FE800E0C000185000006022009090909 +10:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0057:02:000000404001010040020040050400000064C010200702000004D2C00A0702000004D2400A07020000000080008006000000000000800E0C00018500000602200A0A0A0A + +# 11:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0000:02: From ae42a214a1a67c5e130cf573d24127f7b2c6851b Mon Sep 17 00:00:00 2001 From: Thomas Mangin Date: Wed, 12 Jun 2024 22:20:10 +0100 Subject: [PATCH 004/100] order communities for predictable packets ensure communities will have the same order whatever order was presented on the command line. --- etc/exabgp/run/api-flow-merge.run | 1 + qa/encoding/api-flow-merge.msg | 15 +++++++++------ qa/encoding/conf-flow-redirect.msg | 4 ++-- qa/encoding/conf-new-v4.msg | 2 +- qa/encoding/conf-vpn.msg | 13 ++++--------- .../attribute/community/initial/communities.py | 1 + 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/etc/exabgp/run/api-flow-merge.run b/etc/exabgp/run/api-flow-merge.run index 02b66a6ca..366ab2efb 100755 --- a/etc/exabgp/run/api-flow-merge.run +++ b/etc/exabgp/run/api-flow-merge.run @@ -25,6 +25,7 @@ while messages: try: sys.stdout.write(message + '\n') sys.stdout.flush() + time.sleep(0.05) except IOError: sys.stderr.write('IOError - ExaBGP exited ? ') sys.stderr.write('Could not write message: %s\n' % message) diff --git a/qa/encoding/api-flow-merge.msg b/qa/encoding/api-flow-merge.msg index 8d740bf2e..09c529d03 100644 --- a/qa/encoding/api-flow-merge.msg +++ b/qa/encoding/api-flow-merge.msg @@ -1,16 +1,19 @@ +1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:004F:02:000000384001010040020040050400000064C010180702000000FE80FE4702CAFFEE0140018006000000000000800E0C0001850000060220CAFFEE01 1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0047:02:000000304001010040020040050400000064C010104702CAFFEE0140018006000000000000800E0C0001850000060220CAFFEE01 -2:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:004F:02:000000384001010040020040050400000064C010184702CAFFEE0140010702000000FE80FE8006000000000000800E0C0001850000060220CAFFEE01 3:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:001E:02:00000007900F0003000185 4:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:003F:02:000000284001010040020040050400000064C010088006000000000000800E0C000185000006022004040404 5:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:003F:02:000000284001010040020040050400000064C010088006000000000000800E0C000185000006022005050505 6:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0047:02:000000304001010040020040050400000064C010104702CAFFEE0140018006000000000000800E0C000185000006022006060606 -7:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:004F:02:000000384001010040020040050400000064C0101880060000000000004702CAFFEE0140010702000000FE80FE800E0C000185000006022007070707 -8:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:004F:02:000000384001010040020040050400000064C0101880060000000000004702CAFFEE0140010702000000FE80FE800E0C000185000006022008080808 -9:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:004F:02:000000384001010040020040050400000064C0101880060000000000004702CAFFEE0140010702000000FE80FE800E0C000185000006022009090909 -10:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0057:02:000000404001010040020040050400000064C010200702000004D2C00A0702000004D2400A07020000000080008006000000000000800E0C00018500000602200A0A0A0A +7:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:004F:02:000000384001010040020040050400000064C010180702000000FE80FE4702CAFFEE0140018006000000000000800E0C000185000006022007070707 +8:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:004F:02:000000384001010040020040050400000064C010180702000000FE80FE4702CAFFEE0140018006000000000000800E0C000185000006022008080808 +9:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:004F:02:000000384001010040020040050400000064C010180702000000FE80FE4702CAFFEE0140018006000000000000800E0C000185000006022009090909 +10:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0057:02:000000404001010040020040050400000064C0102007020000000080000702000004D2400A0702000004D2C00A8006000000000000800E0C00018500000602200A0A0A0A -# 11:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0000:02: +11:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:003F:02:000000284001010040020040050400000064C010088006000000000000800E0C0001850000060120858201DB +12:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:003F:02:000000284001010040020040050400000064C010088006000000000000800E0C0001850000060120858201DB + +13:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0000:02: diff --git a/qa/encoding/conf-flow-redirect.msg b/qa/encoding/conf-flow-redirect.msg index 9b76aecae..07df343b0 100644 --- a/qa/encoding/conf-flow-redirect.msg +++ b/qa/encoding/conf-flow-redirect.msg @@ -1,5 +1,5 @@ -1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:00A0:02:000000894001010040020040050400000064C008087814000078147814C010188008FFDC0000303900030929060708090103020304051A85800E5200018500004C0120C0A8000102200A0000010301068111040150911F9005121F90541F98910C380692040007010301088100080101810009002080040A02C854012C120190D401F40B010A81140C00048008 -1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0060:02:000000494001010040020040050400000064C008087814000078147814C010188008FFDC0000303900030929060708090103020304051A85800E1200018500000C0120C0A8000102200A000001 +1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:00A0:02:000000894001010040020040050400000064C008087814000078147814C0101800030929060708090103020304051A858008FFDC00003039800E5200018500004C0120C0A8000102200A0000010301068111040150911F9005121F90541F98910C380692040007010301088100080101810009002080040A02C854012C120190D401F40B010A81140C00048008 +1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0060:02:000000494001010040020040050400000064C008087814000078147814C0101800030929060708090103020304051A858008FFDC00003039800E1200018500000C0120C0A8000102200A000001 1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0047:02:000000304001010040020040050400000064C01010800600004E9502F98008FDE900000077800E0C000185000006012076B8B006 1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0045:02:0000002E4001010040020040050400000064C01008010C010203040000800E1200018500000C0120C0A8000102200A000001 1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0051:02:0000003A4001010040020040050400000064C01914000C2A020B800000000100000000000000010000800E1200018500000C0120C0A8000202200A000001 diff --git a/qa/encoding/conf-new-v4.msg b/qa/encoding/conf-new-v4.msg index 26fab3468..80df05c45 100644 --- a/qa/encoding/conf-new-v4.msg +++ b/qa/encoding/conf-new-v4.msg @@ -2,5 +2,5 @@ 1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0034:02:00000015400101004002004003040A00FFFE400504000000C8180A0004180A0005 1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:003B:02:0000001C400101004002004003040A00FFFE400504000000C8C0080478147814180A0007180A0008 1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0037:02:0000001C400101004002004003040A00FFFE40050400000064C0080400000000180A0001 -1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:003B:02:00000020400101004002004003040A00FFFE40050400000064C008087814781478140000180A0003 +1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:003B:02:00000020400101004002004003040A00FFFE40050400000064C008087814000078147814180A0003 1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0017:02:00000000 diff --git a/qa/encoding/conf-vpn.msg b/qa/encoding/conf-vpn.msg index b383c5d48..5e7fb046a 100644 --- a/qa/encoding/conf-vpn.msg +++ b/qa/encoding/conf-vpn.msg @@ -1,9 +1,4 @@ -1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0063:02:0000004C40010100400200400304C80A006540050400000064C010100002FDE8000000010002271000000001800E210001800C0000000000000000C80A00650071003E810000FDE8000000010A000000 -1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0063:02:0000004C40010100400200400304C80A006540050400000064C010100002FDE8000000010002271000000001800E210001800C0000000000000000C80A00650071003E810000FDE8000000010A000080 -1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0063:02:0000004C40010100400200400304C80A006540050400000064C010100002FDE8000000010002271000000001800E210001800C0000000000000000C80A00650071003E810000FDE80000000114000000 -1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0063:02:0000004C40010100400200400304C80A006540050400000064C010100002FDE8000000010002271000000001800E210001800C0000000000000000C80A00650071003E810000FDE80000000114000080 - -#1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:005C02:000000454001010040020040050400000064C010100002FDE8000000010002271000000001800E210001800C0000000000000000C80A00650071003E810000FDE8000000010A000000 -#1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:005C02:000000454001010040020040050400000064C010100002FDE8000000010002271000000001800E210001800C0000000000000000C80A00650071003E810000FDE8000000010A000080 -#1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:005C02:000000454001010040020040050400000064C010100002FDE8000000010002271000000001800E210001800C0000000000000000C80A00650071003E810000FDE80000000114000000 -#1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:005C02:000000454001010040020040050400000064C010100002FDE8000000010002271000000001800E210001800C0000000000000000C80A00650071003E810000FDE80000000114000080 +1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0063:02:0000004C40010100400200400304C80A006540050400000064C0101000022710000000010002FDE800000001800E210001800C0000000000000000C80A00650071003E810000FDE8000000010A000000 +1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0063:02:0000004C40010100400200400304C80A006540050400000064C0101000022710000000010002FDE800000001800E210001800C0000000000000000C80A00650071003E810000FDE8000000010A000080 +1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0063:02:0000004C40010100400200400304C80A006540050400000064C0101000022710000000010002FDE800000001800E210001800C0000000000000000C80A00650071003E810000FDE80000000114000000 +1:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0063:02:0000004C40010100400200400304C80A006540050400000064C0101000022710000000010002FDE800000001800E210001800C0000000000000000C80A00650071003E810000FDE80000000114000080 diff --git a/src/exabgp/bgp/message/update/attribute/community/initial/communities.py b/src/exabgp/bgp/message/update/attribute/community/initial/communities.py index d11645881..a109f33d1 100644 --- a/src/exabgp/bgp/message/update/attribute/community/initial/communities.py +++ b/src/exabgp/bgp/message/update/attribute/community/initial/communities.py @@ -30,6 +30,7 @@ def __init__(self, communities=None): def add(self, data): self.communities.append(data) + self.communities.sort() return self def pack(self, negotiated=None): From f3eec66b58509cf78d1799815b1120114021e723 Mon Sep 17 00:00:00 2001 From: Thomas Mangin Date: Wed, 12 Jun 2024 22:38:08 +0100 Subject: [PATCH 005/100] change extended-encoding for #1209 The code was changed in two patches, one to ensure the ordering to be consistend when appending to extended communities and not break the functional testing. The other change was to merge the feature of add_and_merge into attribute.add as some code somewhere is using add instead of add_and_merge and add should do the right thing. This should be all that is needed to put the issue to rest. --- etc/exabgp/run/api-flow-merge.run | 4 ++-- qa/encoding/api-flow-merge.msg | 6 ++--- .../message/update/attribute/attributes.py | 22 +++++++++---------- src/exabgp/configuration/core/scope.py | 4 +--- 4 files changed, 15 insertions(+), 21 deletions(-) diff --git a/etc/exabgp/run/api-flow-merge.run b/etc/exabgp/run/api-flow-merge.run index 366ab2efb..caf3a81f7 100755 --- a/etc/exabgp/run/api-flow-merge.run +++ b/etc/exabgp/run/api-flow-merge.run @@ -16,8 +16,8 @@ messages = [ 'announce flow route { match { source 9.9.9.9/32; } then { discard; } scope { interface-set [ non-transitive:input:3405770241:1 transitive:output:254:254 ]; } }', 'announce flow route { match { source 10.10.10.10/32; } scope { interface-set [ transitive:input-output:1234:10 transitive:input:1234:10 transitive:output:0:0]; } then { discard; } }', - # 'announce flow route destination 133.130.1.219/32 discard interface-set [ transitive:input:1234:10]', - # 'announce flow route destination 133.130.1.19/32 interface-set [ transitive:input:1234:10 ] discard', + 'announce flow route destination 133.130.1.219/32 discard interface-set [ transitive:input:1234:10]', + 'announce flow route destination 133.130.1.19/32 interface-set [ transitive:input:1234:10 ] discard', ] while messages: diff --git a/qa/encoding/api-flow-merge.msg b/qa/encoding/api-flow-merge.msg index 09c529d03..da9726df1 100644 --- a/qa/encoding/api-flow-merge.msg +++ b/qa/encoding/api-flow-merge.msg @@ -12,8 +12,6 @@ 9:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:004F:02:000000384001010040020040050400000064C010180702000000FE80FE4702CAFFEE0140018006000000000000800E0C000185000006022009090909 10:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0057:02:000000404001010040020040050400000064C0102007020000000080000702000004D2400A0702000004D2C00A8006000000000000800E0C00018500000602200A0A0A0A -11:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:003F:02:000000284001010040020040050400000064C010088006000000000000800E0C0001850000060120858201DB -12:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:003F:02:000000284001010040020040050400000064C010088006000000000000800E0C0001850000060120858201DB - -13:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0000:02: +11:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0047:02:000000304001010040020040050400000064C010100702000004D2400A8006000000000000800E0C0001850000060120858201DB +12:raw:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:0047:02:000000304001010040020040050400000064C010100702000004D2400A8006000000000000800E0C000185000006012085820113 diff --git a/src/exabgp/bgp/message/update/attribute/attributes.py b/src/exabgp/bgp/message/update/attribute/attributes.py index 064d9a26f..6323bf7f5 100644 --- a/src/exabgp/bgp/message/update/attribute/attributes.py +++ b/src/exabgp/bgp/message/update/attribute/attributes.py @@ -193,25 +193,23 @@ def add(self, attribute, _=None): # we return None as attribute if the unpack code must not generate them if attribute is None: return + if attribute.ID in self: + if attribute.ID != Attribute.CODE.EXTENDED_COMMUNITY: + # attempting to add duplicate attribute when not allowed + return + + self._str = '' + self._json = '' + + for community in attribute.communities: + self[attribute.ID].add(community) return self._str = '' self._json = '' - self[attribute.ID] = attribute - # This is as when we generate flow spec we can have multiple keywords - # which are all adding information in the extended-community - def add_and_merge(self, attribute): - if attribute.ID not in self: - self.add(attribute) - return - - if attribute.ID == Attribute.CODE.EXTENDED_COMMUNITY: - for community in attribute.communities: - self[attribute.ID].add(community) - def remove(self, attrid): self.pop(attrid) diff --git a/src/exabgp/configuration/core/scope.py b/src/exabgp/configuration/core/scope.py index 22f460f8f..601cd2456 100644 --- a/src/exabgp/configuration/core/scope.py +++ b/src/exabgp/configuration/core/scope.py @@ -101,9 +101,7 @@ def set(self, name, value): self._current[name] = value def attribute_add(self, name, data): - # .add_and_merge() and not .add() is required - # flow spec to have multiple keywords adding to the extended-community - self.get_route().attributes.add_and_merge(data) + self.get_route().attributes.add(data) if name not in self._added: self._added.add(name) From f2d23f4356cbaeabdb77c78b36d060b2e445cfa4 Mon Sep 17 00:00:00 2001 From: Thomas Mangin Date: Wed, 12 Jun 2024 23:11:14 +0100 Subject: [PATCH 006/100] helping users to help themselves --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e427cf5aa..f6ec7f9e7 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,8 @@ The configuration file and API format may change occasionally, but every effort ## Support +The most common issue reported (ExaBGP stopping/hanging) is caused by using code written for ExaBGP 3.4 with 4.2 or master without having read https://github.com/Exa-Networks/exabgp/wiki/Migration-from-3.4-to-4.x + ExaBGP is supported through Github's [issue tracker](https://github.com/Exa-Networks/exabgp/issues). So should you encounter any problems, please do not hesitate to [report it](https://github.com/Exa-Networks/exabgp/issues?labels=bug&page=1&state=open) so we can help you. During "day time" (GMT/BST) feel free to contact us on [`Slack`](https://join.slack.com/t/exabgp/shared_invite/enQtNTM3MTU5NTg5NTcyLTMwNmZlMGMyNTQyNWY3Y2RjYmQxODgyYzY2MGFkZmYwODMxNDZkZjc4YmMyM2QzNzA1YWM0MmZjODhlYThjNTQ). We will try to respond if available. From ef90e3e00fdbf206a3047e658de7da30dc587d96 Mon Sep 17 00:00:00 2001 From: Thomas Mangin Date: Wed, 12 Jun 2024 23:14:12 +0100 Subject: [PATCH 007/100] run slower but more reliable python3.6 tests --- .github/workflows/functional-3.6.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/functional-3.6.yaml b/.github/workflows/functional-3.6.yaml index cad9455e1..8b0e464a1 100644 --- a/.github/workflows/functional-3.6.yaml +++ b/.github/workflows/functional-3.6.yaml @@ -43,4 +43,4 @@ jobs: - name: Python 3.6 Coverage run: | - ./qa/bin/functional-3.6 run + ./qa/bin/functional-3.6 all From c6da455e92c992b121b96e6b009663520b0dff3f Mon Sep 17 00:00:00 2001 From: Thomas Mangin Date: Wed, 12 Jun 2024 23:32:32 +0100 Subject: [PATCH 008/100] better handle bad shell pipe --- src/exabgp/application/decode.py | 8 +++++++- src/exabgp/application/main.py | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/exabgp/application/decode.py b/src/exabgp/application/decode.py index 12fb97b64..4f7265597 100644 --- a/src/exabgp/application/decode.py +++ b/src/exabgp/application/decode.py @@ -119,4 +119,10 @@ def cmdline(cmdarg): if __name__ == '__main__': - sys.exit(main()) + try: + code = main() + sys.exit(code) + except BrokenPipeError: + # there was a PIPE ( ./sbin/exabgp | command ) + # and command does not work as should + sys.exit(1) diff --git a/src/exabgp/application/main.py b/src/exabgp/application/main.py index d209a0a38..7e6193925 100644 --- a/src/exabgp/application/main.py +++ b/src/exabgp/application/main.py @@ -87,4 +87,10 @@ def main(): if __name__ == '__main__': - sys.exit(main()) + try: + code = main() + sys.exit(code) + except BrokenPipeError: + # there was a PIPE ( ./sbin/exabgp | command ) + # and command does not work as should + sys.exit(1) From 7eaee751b76a88f6c97a5243a4dfb3f716db1a2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 23:39:02 +0000 Subject: [PATCH 009/100] Bump urllib3 from 2.2.1 to 2.2.2 Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.2.1 to 2.2.2. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/2.2.1...2.2.2) --- updated-dependencies: - dependency-name: urllib3 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index de76ceb0c..17f7b7c8e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "attrs" @@ -889,13 +889,13 @@ files = [ [[package]] name = "urllib3" -version = "2.2.1" +version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, - {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.extras] From 001873c1c0a2a104afd5f3358e28e1496e7a00ad Mon Sep 17 00:00:00 2001 From: Seena Fallah Date: Sun, 23 Jun 2024 23:01:35 +0200 Subject: [PATCH 010/100] healthcheck: add support exact label match So far loopback ips was looking for the corresponding interface by checking whether the label starts with the desired input. By providing --label-exact-match it checks whether the label exactly matches the desired input. Signed-off-by: Seena Fallah --- src/exabgp/application/healthcheck.py | 28 +++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/exabgp/application/healthcheck.py b/src/exabgp/application/healthcheck.py index 4e2a1327f..0d16370c5 100644 --- a/src/exabgp/application/healthcheck.py +++ b/src/exabgp/application/healthcheck.py @@ -96,6 +96,7 @@ def setargs(parser): g.add_argument("--no-ip-setup", action="store_false", dest="ip_setup", help="don't setup missing IP addresses") g.add_argument("--dynamic-ip-setup", default=False, action="store_true", dest="ip_dynamic", help="delete existing loopback ips on state down and " "disabled, then restore loopback when up") g.add_argument("--label", default=None, help="use the provided label to match loopback addresses") + g.add_argument("--label-exact-match", default=False, action="store_true", help="use the provided label to exactly match loopback addresses, not a prefix match") g.add_argument("--start-ip", metavar='N', type=int, default=0, help="index of the first IP in the list of IP addresses") g.add_argument("--up-metric", metavar='M', type=int, default=100, help="first IP get the metric M when the service is up") g.add_argument("--down-metric", metavar='M', type=int, default=1000, help="first IP get the metric M when the service is down") @@ -180,7 +181,7 @@ def syslog_address(): logger.addHandler(ch) -def loopback_ips(label, label_only): +def loopback_ips(label, label_only, label_exact_match): """Retrieve loopback IP addresses""" logger.debug("Retrieve loopback IP addresses") addresses = [] @@ -188,7 +189,7 @@ def loopback_ips(label, label_only): if sys.platform.startswith("linux"): # Use "ip" (ifconfig is not able to see all addresses) ipre = re.compile(r"^(?P\d+):\s+(?P\S+)\s+inet6?\s+" r"(?P[\da-f.:]+)/(?P\d+)\s+.*") - labelre = re.compile(r".*\s+lo:(?P