From 8b7a224360bc83f8b28eca33cb8d5b020fd9e461 Mon Sep 17 00:00:00 2001
From: "th.l" <thl-cmk@outlook.com>
Date: Mon, 22 Jul 2024 21:39:11 +0200
Subject: [PATCH] update project

---
 README.md                                     |   2 +-
 mkp/bgp_topology-0.0.1-20240722.mkp           | Bin 0 -> 9704 bytes
 source/checkman/.gitkeep                      |   0
 source/checkman/bgp_topology                  |  45 ---
 .../bgp_topology/constants.py                 |  62 ++++
 .../bgp_topology/graphing/bgp_topology.py     |  35 ++
 .../bgp_topology/lib/bgp_topology.py          | 344 ++++++++++++++++++
 .../bgp_topology/lib/utils.py                 | 336 +++++++++++++++++
 .../bgp_topology/libexec/check_bgp_topology   |  18 +
 .../bgp_topology/rulesets/bgp_topology.py     | 176 +++++++++
 .../server_side_calls/bgp_topology.py         | 178 +++++++++
 source/packages/bgp_topology                  |  16 +
 12 files changed, 1166 insertions(+), 46 deletions(-)
 create mode 100644 mkp/bgp_topology-0.0.1-20240722.mkp
 delete mode 100644 source/checkman/.gitkeep
 delete mode 100644 source/checkman/bgp_topology
 create mode 100644 source/cmk_addons_plugins/bgp_topology/constants.py
 create mode 100644 source/cmk_addons_plugins/bgp_topology/graphing/bgp_topology.py
 create mode 100644 source/cmk_addons_plugins/bgp_topology/lib/bgp_topology.py
 create mode 100644 source/cmk_addons_plugins/bgp_topology/lib/utils.py
 create mode 100755 source/cmk_addons_plugins/bgp_topology/libexec/check_bgp_topology
 create mode 100644 source/cmk_addons_plugins/bgp_topology/rulesets/bgp_topology.py
 create mode 100644 source/cmk_addons_plugins/bgp_topology/server_side_calls/bgp_topology.py
 create mode 100644 source/packages/bgp_topology

diff --git a/README.md b/README.md
index 9209cf4..9e11d58 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[PACKAGE]: ../../raw/master/packagee-0.1.2-20230706.mkp "package-0.1.2-20230706.mkp"
+[PACKAGE]: ../../raw/master/mkp/bgp_topology-0.0.1-20240722.mkp "bgp_topology-0.0.1-20240722.mkp"
 # Title
 
 A short description about the plugin
diff --git a/mkp/bgp_topology-0.0.1-20240722.mkp b/mkp/bgp_topology-0.0.1-20240722.mkp
new file mode 100644
index 0000000000000000000000000000000000000000..c2016d68bdc1e69682bf793b7e24f92f311828cb
GIT binary patch
literal 9704
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
K2(TM4u>S#Qj1f@)

literal 0
HcmV?d00001

diff --git a/source/checkman/.gitkeep b/source/checkman/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/source/checkman/bgp_topology b/source/checkman/bgp_topology
deleted file mode 100644
index 08ef898..0000000
--- a/source/checkman/bgp_topology
+++ /dev/null
@@ -1,45 +0,0 @@
-title: Dummy check man page - used as template for new check manuals
-agents: linux, windows, aix, solaris, hpux, vms, freebsd, snmp
-catalog: see modules/catalog.py for possible values
-license: GPL
-distribution: check_mk
-description:
- Describe here: (1) what the check actually does, (2) under which
- circumstances it goes warning/critical, (3) which devices are supported
- by the check, (4) if the check requires a separated plugin or
- tool or separate configuration on the target host.
-
-item:
- Describe the syntax and meaning of the check's item here. Provide all
- information one needs if coding a manual check with {checks +=} in {main.mk}.
- Give an example.  If the check uses {None} as sole item,
- then leave out this section.
-
-examples:
- # Give examples for configuration in {main.mk} here. If the check has
- # configuration variable, then give example for them here.
-
- # set default levels to 40 and 60 percent:
- foo_default_values = (40, 60)
-
- # another configuration variable here:
- inventory_foo_filter = [ "superfoo", "superfoo2" ]
-
-perfdata:
- Describe precisely the number and meaning of performance variables
- the check sends. If it outputs no performance data, then leave out this
- section.
-
-inventory:
- Describe how the inventory for the check works. Which items
- will it find? Describe the influence of check specific
- configuration parameters to the inventory.
-
-[parameters]
-foofirst(int): describe the first parameter here (if parameters are grouped
-        as tuple)
-fooother(string): describe another parameter here.
-
-[configuration]
-foo_default_levels(int, int): Describe global configuration variable of
-    foo here. Important: also tell the user how they are preset.
diff --git a/source/cmk_addons_plugins/bgp_topology/constants.py b/source/cmk_addons_plugins/bgp_topology/constants.py
new file mode 100644
index 0000000..1213084
--- /dev/null
+++ b/source/cmk_addons_plugins/bgp_topology/constants.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# License: GNU General Public License v2
+#
+# Author: thl-cmk[at]outlook[dot]com
+# URL   : https://thl-cmk.hopto.org
+# Date  : 2024-07-16
+# File  : bgp_topology/constants.py
+
+# 2024-07-16: copied from bgp_topology/lib/ruleset_names.py
+
+
+from typing import Final
+
+ARG_PARSER_ANCHOR: Final[str] = '--bgp-anchor'
+ARG_PARSER_ANCHOR_AS: Final[str] = 'bgp-as'
+ARG_PARSER_ANCHOR_BOTH: Final[str] = 'both'
+ARG_PARSER_ANCHOR_ID: Final[str] = 'bgp-id'
+ARG_PARSER_EMBLEM_AS: Final[str] = '--bgp-emblem-as'
+ARG_PARSER_EMBLEM_ID: Final[str] = '--bgp-emblem-id'
+ARG_PARSER_HOST: Final[str] = '--host'
+ARG_PARSER_MAKE_DEFAULT: Final[str] = '--make-default'
+ARG_PARSER_NONE: Final[str] = 'none'
+ARG_PARSER_SITES_EXCLUDE: Final[str] = '--exclude-sites'
+ARG_PARSER_SITES_INCLUDE: Final[str] = '--include-sites'
+
+BGP_PEER_LOCAL_ADDR: Final[str] = 'local_addr'
+BGP_PEER_LOCAL_AS: Final[str] = 'local_as'
+BGP_PEER_LOCAL_ID: Final[str] = 'local_id'
+BGP_PEER_REMOTE_ADDR: Final[str] = 'remote_addr'
+BGP_PEER_REMOTE_AS: Final[str] = 'remote_as'
+BGP_PEER_REMOTE_ID: Final[str] = 'remote_id'
+BGP_PEER_STATE: Final[str] = 'state'
+BGP_PEER_UPTIME: Final[str] = 'uptime'
+
+EMBLEM_BGP_ID: Final[str] = 'icon_topic_system'  # icon_plugins_hw
+EMBLEM_BGP_AS: Final[str] = 'icon_cloud'
+
+METRIC_TIME_TAKEN: Final[str] = 'topology_time_taken'
+
+PARAM_BGP_AS: Final[str] = 'bgp_as'
+PARAM_BGP_EXT_ANCHOR: Final[str] = 'bgp_ext_anchor'
+PARAM_BGP_EXT_ANCHOR_AS: Final[str] = 'bgp_ext_anchor_as'
+PARAM_BGP_EXT_ANCHOR_BOTH: Final[str] = 'bgp_ext_anchor_both'
+PARAM_BGP_EXT_ANCHOR_ID: Final[str] = 'bgp_ext_anchor_id'
+PARAM_BGP_EXT_ANCHOR_NONE: Final[str] = 'bgp_ext_anchor_none'
+PARAM_BGP_ID: Final[str] = 'bgp_id'
+PARAM_BGP_SITES_EXCLUDE: Final[str] = 'exclude_sites'
+PARAM_BGP_SITES_FILTER: Final[str] = 'bgp_filter_sites'
+PARAM_BGP_SITES_INCLUDE: Final[str] = 'include_sites'
+PARAM_EMBLEM_CUSTOM: Final[str] = 'custom_emblem'
+PARAM_EMBLEM_DEFAULT: Final[str] = 'default_emblem'
+PARAM_EMBLEM_NO_EMBLEM: Final[str] = 'no_emblem'
+PARAM_MAKE_DEFAULT: Final[str] = 'make_default'
+
+PICTURE_TYPE_EMBLEM: Final[str] = 'emblem'
+PICTURE_TYPE_ICON: Final[str] = 'icon'
+
+RULE_SET_NAME_BGP_TOPOLOGY: Final[str] = 'bgp_topology'
+
+TOPOLOGY_NAME: Final[str] = 'BGP'
diff --git a/source/cmk_addons_plugins/bgp_topology/graphing/bgp_topology.py b/source/cmk_addons_plugins/bgp_topology/graphing/bgp_topology.py
new file mode 100644
index 0000000..69af60e
--- /dev/null
+++ b/source/cmk_addons_plugins/bgp_topology/graphing/bgp_topology.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# License: GNU General Public License v2
+#
+# Author: thl-cmk[at]outlook[dot]com
+# URL   : https://thl-cmk.hopto.org
+# Date  : 2024-07-16
+# File  : bgp_topology/graphing/bgp_topology.py
+
+from cmk.graphing.v1 import Title
+from cmk.graphing.v1.graphs import Graph
+from cmk.graphing.v1.metrics import Color, Metric, Unit, TimeNotation, AutoPrecision
+from cmk.graphing.v1.perfometers import Closed, FocusRange, Open, Perfometer
+
+from cmk_addons.plugins.bgp_topology.constants import METRIC_TIME_TAKEN
+
+metric_topology_time_taken = Metric(
+    name=METRIC_TIME_TAKEN,
+    title=Title('Time taken'),
+    unit=Unit(TimeNotation(), AutoPrecision(4)),
+    color=Color.BLUE,
+)
+
+graph_topology_time_taken = Graph(
+    name=METRIC_TIME_TAKEN,
+    title=Title('Time taken'),
+    compound_lines=[METRIC_TIME_TAKEN],
+)
+
+perfometer_topology_time_taken = Perfometer(
+    name=METRIC_TIME_TAKEN,
+    focus_range=FocusRange(Closed(0), Open(1)),
+    segments=[METRIC_TIME_TAKEN]
+)
diff --git a/source/cmk_addons_plugins/bgp_topology/lib/bgp_topology.py b/source/cmk_addons_plugins/bgp_topology/lib/bgp_topology.py
new file mode 100644
index 0000000..76e4d0a
--- /dev/null
+++ b/source/cmk_addons_plugins/bgp_topology/lib/bgp_topology.py
@@ -0,0 +1,344 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# License: GNU General Public License v2
+#
+# Author: thl-cmk[at]outlook[dot]com
+# URL   : https://thl-cmk.hopto.org
+# Date  : 2024-07-16
+# File  : bgp_topology/lib/bgp_topology.py
+
+# 2024-07-20: moved to lib -> is now an active check
+
+__AUTHOR__ = 'thl-cmk[at]outlook[dot]com'
+__URL__ = 'https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/bgp_topology'
+__USAGE__ = '/local/lib/python3/cmk_addons/plugins/bgp_topology/libexec/check_bgp_topology --make-default'
+__VERSION__ = '0.0.1-20240721'
+
+from argparse import ArgumentParser, Namespace, RawTextHelpFormatter
+from collections.abc import MutableMapping, MutableSequence, Sequence
+from dataclasses import dataclass
+from time import time_ns
+
+from cmk.agent_based.v2 import render
+from cmk_addons.plugins.bgp_topology.constants import (
+    ARG_PARSER_ANCHOR,
+    ARG_PARSER_ANCHOR_AS,
+    ARG_PARSER_ANCHOR_BOTH,
+    ARG_PARSER_ANCHOR_ID,
+    ARG_PARSER_EMBLEM_AS,
+    ARG_PARSER_EMBLEM_ID,
+    ARG_PARSER_HOST,
+    ARG_PARSER_MAKE_DEFAULT,
+    ARG_PARSER_NONE,
+    ARG_PARSER_SITES_EXCLUDE,
+    ARG_PARSER_SITES_INCLUDE,
+    BGP_PEER_LOCAL_ADDR,
+    BGP_PEER_LOCAL_AS,
+    BGP_PEER_LOCAL_ID,
+    BGP_PEER_REMOTE_ADDR,
+    BGP_PEER_REMOTE_AS,
+    BGP_PEER_REMOTE_ID,
+    BGP_PEER_STATE,
+    BGP_PEER_UPTIME,
+    EMBLEM_BGP_AS,
+    EMBLEM_BGP_ID,
+    METRIC_TIME_TAKEN,
+    PARAM_BGP_EXT_ANCHOR_AS,
+    PARAM_BGP_EXT_ANCHOR_BOTH,
+    PARAM_BGP_EXT_ANCHOR_ID,
+    TOPOLOGY_NAME,
+)
+from cmk_addons.plugins.bgp_topology.lib.utils import (
+    BASE_TOPO_PATH,
+    LiveStatusConnection,
+    Metric,
+    OMD_ROOT,
+    TopoConnections,
+    TopoObjects,
+    add_dummy_topologies,
+    get_anchor,
+    get_bgp_peer_clean_key,
+    get_bgp_peer_clean_value,
+    get_emblem,
+    get_service,
+    make_topo_default,
+    save_topology,
+)
+
+
+class Params(Namespace):
+    bgp_anchor: str
+    make_default: bool = False
+    bgp_emblem_as: str = EMBLEM_BGP_AS
+    bgp_emblem_id: str = EMBLEM_BGP_ID
+    include_sites: Sequence[str] | None = None
+    exclude_sites: Sequence[str] | None = None
+    host: str = ''
+
+
+@dataclass(frozen=True)
+class BgpPeer:
+    host: str
+    service: str
+    state: int
+    state_str: str
+    remote_address: str
+    uptime: str | None = None
+    local_as: str | None = None
+    local_address: str | None = None
+    local_id: str | None = None
+    remote_as: str | None = None
+    remote_id: str | None = None
+
+    @classmethod
+    def parse(cls, host: str, state: int, servie: str, raw_peer_data: str):
+        raw_peer_attributes: Sequence[str] = raw_peer_data.split('\\n')
+
+        bgp_peer: MutableMapping[str, str] = {}
+        for entry in raw_peer_attributes:
+            try:
+                key, value = entry.split(':', 1)
+            except ValueError:
+                key = None
+                value = None
+            key: str | None = get_bgp_peer_clean_key(key)
+            value: str | None = get_bgp_peer_clean_value(key, value)
+            if key is not None and value is not None:
+                bgp_peer[key] = value
+
+        return cls(
+            host=host,
+            local_address=bgp_peer.get(BGP_PEER_LOCAL_ADDR),
+            local_as=bgp_peer.get(BGP_PEER_LOCAL_AS),
+            local_id=bgp_peer.get(BGP_PEER_LOCAL_ID),
+            remote_address=bgp_peer.get(BGP_PEER_REMOTE_ADDR),
+            remote_as=bgp_peer.get(BGP_PEER_REMOTE_AS),
+            remote_id=bgp_peer.get(BGP_PEER_REMOTE_ID),
+            service=servie,
+            state=state,
+            state_str=bgp_peer.get(BGP_PEER_STATE),
+            uptime=bgp_peer.get(BGP_PEER_UPTIME),
+        )
+
+
+def create_bgp_topology(params: Params | None) -> int:
+    start_time = time_ns()
+
+    summary: list[str] = []
+    details: list[str] = []
+
+    objects = TopoObjects()
+    connections = TopoConnections()
+    sub_directory = TOPOLOGY_NAME
+
+    ls_connection = LiveStatusConnection()
+    if params.include_sites is not None:
+        ls_connection.filter_sites(include=True, sites=params.include_sites)
+        sites_str: str = ', '.join(params.include_sites)
+        details.append(f'Site(s) included: {sites_str}')
+        sub_directory = f'{TOPOLOGY_NAME}_{params.host}'
+    elif params.exclude_sites is not None:
+        ls_connection.filter_sites(include=False, sites=params.exclude_sites)
+        sites_str: str = ', '.join(params.exclude_sites)
+        details.append(f'Site(s) excluded: {sites_str}')
+        sub_directory = f'{TOPOLOGY_NAME}_{params.host}'
+
+    bgp_anchor = get_anchor(params.bgp_anchor)
+    emblem_as = get_emblem(params.bgp_emblem_as)
+    emblem_id = get_emblem(params.bgp_emblem_id)
+
+    query: str = (
+        'GET services\n'
+        'Columns: host_name state description long_plugin_output\n'
+        'Filter: description ~ BGP peer\n'
+        'OutputFormat: python3\n'
+    )
+    if (raw_bgp_peers := ls_connection.query(query=query)) is not None:
+        bgp_peers: MutableSequence[BgpPeer] = [
+            BgpPeer.parse(host, state, service, raw_peer_data) for host, state, service, raw_peer_data in raw_bgp_peers
+        ]
+
+        # create index by local address, add host and service objects
+        bgp_peers_by_local_addr: MutableMapping[str, MutableSequence[BgpPeer]] = {'0.0.0.0': []}
+        for bgp_peer in bgp_peers:
+            objects.add_host(bgp_peer.host)
+            objects.add_service(bgp_peer.host, bgp_peer.service)
+            connections.add_connection(
+                right=bgp_peer.host,
+                left=get_service(bgp_peer.host, bgp_peer.service),
+                left_state=bgp_peer.state
+            )
+
+            if bgp_peer.local_address is not None:
+                if bgp_peers_by_local_addr.get(bgp_peer.local_address) is None:
+                    bgp_peers_by_local_addr[bgp_peer.local_address]: MutableSequence[BgpPeer] = []
+                bgp_peers_by_local_addr[bgp_peer.local_address].append(bgp_peer)
+            else:
+                bgp_peers_by_local_addr['0.0.0.0'].append(bgp_peer)
+
+        # find connections
+        for bgp_peer in bgp_peers:
+            if (peer_list := bgp_peers_by_local_addr.get(bgp_peer.remote_address)) is not None:
+                for peer in peer_list:
+                    if bgp_peer.local_as == peer.remote_as and bgp_peer.local_as is not None:
+                        if bgp_peer.local_id == peer.remote_id and bgp_peer.local_id is not None:
+                            if bgp_peer.local_address == peer.remote_address and bgp_peer.local_address is not None:
+                                connections.add_connection(
+                                    left=get_service(bgp_peer.host, bgp_peer.service),
+                                    right=get_service(peer.host, peer.service),
+                                    right_state=bgp_peer.state,
+                                    left_state=peer.state,
+                                )
+
+            # if there is no peer_list the peer is either external (to checkmk) or the connection is not
+            # established, so we have no local address or remote id
+            else:
+                ext_bgp_host_as: str | None = None
+                ext_bgp_host_id: str | None = None
+                if bgp_anchor in [
+                    PARAM_BGP_EXT_ANCHOR_BOTH,
+                    PARAM_BGP_EXT_ANCHOR_AS,
+                ] and bgp_peer.remote_as is not None:
+                    ext_bgp_host_as: str = f'BGP-AS: {bgp_peer.remote_as}'
+                    objects.add_host(
+                        host=ext_bgp_host_as,
+                        link2core=False,
+                        emblem=emblem_as,
+                    )
+                if bgp_anchor in [
+                    PARAM_BGP_EXT_ANCHOR_BOTH,
+                    PARAM_BGP_EXT_ANCHOR_ID
+                ] and bgp_peer.remote_id is not None:
+                    ext_bgp_host_id: str = f'BGP-ID: {bgp_peer.remote_id}'
+                    objects.add_host(
+                        host=ext_bgp_host_id,
+                        link2core=False,
+                        emblem=emblem_id
+                    )
+                    connections.add_connection(
+                        right=ext_bgp_host_id,
+                        left=get_service(bgp_peer.host, bgp_peer.service),
+                        left_state=bgp_peer.state
+                    )
+                    if ext_bgp_host_as is not None:
+                        connections.add_connection(ext_bgp_host_id, ext_bgp_host_as)
+
+                if ext_bgp_host_as is not None and ext_bgp_host_id is None:
+                    connections.add_connection(
+                        right=get_service(bgp_peer.host, bgp_peer.service),
+                        left=ext_bgp_host_as,
+                        right_state=bgp_peer.state,
+                    )
+
+    connections.topo_connections.sort()
+    data_set = {
+        'version': 1,
+        'name':  TOPOLOGY_NAME,
+        'objects': dict(sorted(objects.topo_objects.items())),
+        'connections': connections.topo_connections,
+    }
+
+    save_topology(data=data_set, sub_directory=sub_directory)
+    # workaround for backend is only picking up topologies from default folder
+    add_dummy_topologies(sub_directory=sub_directory)
+    # end workaround
+    make_topo_default(sub_directory=sub_directory, make_default=params.make_default)
+
+    summary.append(f'Objects: {len(objects.topo_objects)}')
+    details.append(f'Objects: {len(objects.topo_objects)}')
+
+    summary.append(f'Connections: {len(connections.topo_connections)}')
+    details.append(f'Connections: {len(connections.topo_connections)}')
+
+    details.append(f'Written to: {BASE_TOPO_PATH}/{sub_directory}/data_{TOPOLOGY_NAME.lower()}.json')
+
+    value = (time_ns() - start_time) / 1e9
+    summary.append(f'Time taken: {render.timespan(value)}')
+    details.append(f'Time taken: {render.timespan(value)}')
+
+    perf_data = Metric(
+        name=METRIC_TIME_TAKEN,
+        value=value,
+        # levels=None,
+        # boundaries=None,
+    )
+
+    all_summary: str = ', '.join(summary)
+    all_details: str = '\n'.join(details)
+    print(f'{all_summary}\n{all_details}|{perf_data}')
+    return 0
+
+
+def parse_arguments(argv: Sequence[str]) -> Params:
+    parser = ArgumentParser(
+        prog='bgp_topology',
+        formatter_class=RawTextHelpFormatter,
+        description=f"""Create BGP peer network topology for Checkmk""",
+        epilog=f"""
+Example usage:
+{OMD_ROOT}/{__USAGE__}        
+
+Version: {__VERSION__} | Written by {__AUTHOR__}
+for more information see: {__URL__}
+        """
+    )
+    parser.add_argument(
+        ARG_PARSER_ANCHOR,
+        choices=[
+            ARG_PARSER_ANCHOR_BOTH,
+            ARG_PARSER_ANCHOR_AS,
+            ARG_PARSER_ANCHOR_ID,
+            ARG_PARSER_NONE,
+        ],
+        default=ARG_PARSER_ANCHOR_BOTH,
+        help='Anchor for external BGP objects (default: %(default)s).',
+    )
+    parser.add_argument(
+        ARG_PARSER_EMBLEM_AS,
+        type=str,
+        default=EMBLEM_BGP_AS,
+        help='Emblem to use for BGP-AS objects (default: %(default)s).',
+    )
+    parser.add_argument(
+        ARG_PARSER_EMBLEM_ID,
+        type=str,
+        default=EMBLEM_BGP_ID,
+        help='Emblem to use for BGP-ID objects (default: %(default)s).',
+    )
+    parser.add_argument(
+        ARG_PARSER_MAKE_DEFAULT,
+        action='store_const', const=True,
+        default=False,
+        help='Make this topology the default (default: %(default)s).',
+    )
+    parser.add_argument(
+        ARG_PARSER_HOST,
+        type=str,
+        help="""The name of the Checkmk host to which the plugin is attached. This is set 
+automatically by Checkmk. If a site filter is active, the host name is 
+appended to the subdirectory where the topology is stored (“BGP” becomes 
+“BGP_host_name”). This way we can have more than one BGP topology without 
+overwriting each other.""",
+    )
+    site_filter = parser.add_mutually_exclusive_group()
+    site_filter.add_argument(
+        ARG_PARSER_SITES_INCLUDE,
+        type=str,
+        nargs='+',
+        help=f"""List of Checkmk site names to include in the topology creation. 
+Can not used together with {ARG_PARSER_SITES_EXCLUDE}""",
+    )
+    site_filter.add_argument(
+        ARG_PARSER_SITES_EXCLUDE,
+        type=str,
+        nargs='+',
+        help=f"""List of Checkmk site names to exclude from the topology creation. 
+Can not used together with {ARG_PARSER_SITES_INCLUDE}""",
+    )
+
+    return parser.parse_args(argv)
+
+
+def main(argv: Sequence[str] | None = None) -> int:
+    return create_bgp_topology(parse_arguments(argv=argv))
diff --git a/source/cmk_addons_plugins/bgp_topology/lib/utils.py b/source/cmk_addons_plugins/bgp_topology/lib/utils.py
new file mode 100644
index 0000000..9c93067
--- /dev/null
+++ b/source/cmk_addons_plugins/bgp_topology/lib/utils.py
@@ -0,0 +1,336 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# License: GNU General Public License v2
+#
+# Author: thl-cmk[at]outlook[dot]com
+# URL   : https://thl-cmk.hopto.org
+# Date  : 2024-07-16
+# File  : bgp_topology/lib/utils.py
+
+# 2024-07-16: copied from vsphere_topology/lib/utils.py
+
+from collections.abc import Mapping, MutableMapping, MutableSequence, Sequence
+from dataclasses import dataclass
+from json import dumps as json_dunps, loads as json_loads
+from os import environ
+from pathlib import Path
+from typing import Final, Tuple
+
+from livestatus import MultiSiteConnection, SiteConfigurations, SiteId
+from cmk_addons.plugins.bgp_topology.constants import (
+    ARG_PARSER_NONE,
+    BGP_PEER_LOCAL_ADDR,
+    BGP_PEER_LOCAL_AS,
+    BGP_PEER_LOCAL_ID,
+    BGP_PEER_REMOTE_ADDR,
+    BGP_PEER_REMOTE_AS,
+    BGP_PEER_REMOTE_ID,
+    BGP_PEER_STATE,
+    BGP_PEER_UPTIME,
+    PARAM_BGP_EXT_ANCHOR_AS,
+    PARAM_BGP_EXT_ANCHOR_BOTH,
+    PARAM_BGP_EXT_ANCHOR_ID,
+    TOPOLOGY_NAME,
+)
+
+OMD_ROOT = environ['OMD_ROOT']
+BASE_TOPO_PATH: Final[str] = f'{OMD_ROOT}/var/check_mk/topology/data'
+
+
+def get_bgp_peer_clean_key(raw_key: str) -> str | None:
+    key_map: Mapping = {
+        'Local AS': BGP_PEER_LOCAL_AS,
+        'Local address': BGP_PEER_LOCAL_ADDR,
+        'Local identifier': BGP_PEER_LOCAL_ID,
+        'Peer state': BGP_PEER_STATE,
+        'Remote AS': BGP_PEER_REMOTE_AS,
+        'Remote address': BGP_PEER_REMOTE_ADDR,
+        'Remote identifier': BGP_PEER_REMOTE_ID,
+        'Uptime': BGP_PEER_UPTIME,
+    }
+
+    return key_map.get(raw_key)
+
+
+def get_bgp_peer_clean_value(key: str, value: str) -> str | None:
+    class MatchValues:
+        local_addr = BGP_PEER_LOCAL_ADDR
+        remote_addr = BGP_PEER_REMOTE_ADDR
+        remote_id = BGP_PEER_REMOTE_ID
+
+    if value is not None:
+        value = value.strip()
+    match key:
+        case MatchValues.local_addr | MatchValues.remote_addr | MatchValues.remote_id:
+            if value.strip() in ['0.0.0.0', 'N/A']:
+                return
+    if value:
+        return value
+
+
+def get_service(host: str, servie: str) -> str:
+    return f'{servie}@{host}'
+
+
+def get_anchor(raw_anchor: str) -> str | None:
+    anchor_map = {
+        'both': PARAM_BGP_EXT_ANCHOR_BOTH,
+        'as': PARAM_BGP_EXT_ANCHOR_AS,
+        'id': PARAM_BGP_EXT_ANCHOR_ID,
+        'none': ARG_PARSER_NONE
+    }
+    return anchor_map.get(raw_anchor)
+
+
+def get_emblem(emblem: str) -> str | None:
+    if emblem != ARG_PARSER_NONE:
+        return emblem
+
+
+def save_topology(data: Mapping, sub_directory: str) -> None:
+    """
+    Save the topology as json file under $OMD_ROOT/var/check_mk/topology/data/{sub_directory}.data_{data['name']}.json
+    the filename will be changed to lower case.
+    Args:
+        data: the topology data
+        sub_directory: the subdirectory were to save the data under ~/var/check_mk/topology/data/
+
+    Returns:
+        None
+    """
+    file_name = f'data_{data["name"]}.json'.lower()
+    save_file = Path(f'{BASE_TOPO_PATH}/{sub_directory}/{file_name}')
+    save_file.parent.mkdir(exist_ok=True, parents=True)
+    save_file.write_text(json_dunps(data))
+
+
+def make_topo_default(sub_directory: str, make_default: bool) -> None:
+    """
+    Create the symlink "default" to $OMD_ROOT/var/check_mk/topology/data/{sub_directory} in
+    $OMD_ROOT/var/check_mk/topology/data/ if it don't exist or mage_default is True
+    Args:
+        sub_directory: the subdirectory under ~/var/check_mk/topology/data/ thaht become default
+        make_default: if True, create the symlink "default" with path as target
+
+    Returns:
+        None
+    """
+
+    target_path = f'{BASE_TOPO_PATH}/{sub_directory}'
+
+    if not Path(f'{BASE_TOPO_PATH}/default').exists():
+        make_default = True
+    if make_default:
+        Path(f'{BASE_TOPO_PATH}/default').unlink(missing_ok=True)
+        Path(f'{BASE_TOPO_PATH}/default').symlink_to(target=Path(target_path), target_is_directory=True)
+
+
+def get_topologies() -> Sequence[str | None]:
+    """
+    Returns a list of topology names form the default typology directory.
+
+    Returns:
+        List of str ie: ['CDP', 'LLDP']
+    """
+    path: str = f'{BASE_TOPO_PATH}/default'
+    if not Path(path).exists():
+        return []
+
+    files = [f for f in Path(path).glob('*.json') if f.is_file()]
+    return [
+        json_loads(Path(file).read_text())['name'] for file in files if
+        json_loads(Path(file).read_text()).get('name') is not None
+    ]
+
+
+def add_dummy_topologies(sub_directory: str):
+    path: str = f'{BASE_TOPO_PATH}/default'
+
+    # don't overwrite existing topology
+    if Path(path).exists() and not Path(f'{path}/data_{TOPOLOGY_NAME.lower()}.json').exists():
+        dummy_topology = {'version': 1, 'name': TOPOLOGY_NAME, 'objects': {}, 'connections': []}
+        save_topology(
+            data=dummy_topology,
+            sub_directory='default',
+        )
+
+    for topology in get_topologies():
+        if not Path(f'{BASE_TOPO_PATH}/{sub_directory}/data_{topology.lower()}.json').exists():
+            save_topology(
+                data={'version': 1, 'name': topology, 'objects': {}, 'connections': []},
+                sub_directory=sub_directory
+            )
+
+
+#
+#  live status
+#
+class LiveStatusConnection:
+    def __init__(self):
+        self.sites: SiteConfigurations = SiteConfigurations({})
+        self.sites_mk = Path(f'{OMD_ROOT}/etc/check_mk/multisite.d/sites.mk')
+        self.socket_path = f'unix:{OMD_ROOT}/tmp/run/live'
+        if self.sites_mk.exists():
+            # make eval() "secure"
+            # https://realpython.com/python-eval-function/#minimizing-the-security-issues-of-eval
+            _code = compile(self.sites_mk.read_text(), '<string>', 'eval')
+            allowed_names = ['sites', 'update']
+            for name in _code.co_names:
+                if name not in allowed_names:
+                    raise NameError(f'Use of {name} in {self.sites_mk.name} not allowed.')
+
+            sites_raw: MutableMapping = {}
+            eval(self.sites_mk.read_text(), {'__builtins__': {}}, {'sites': sites_raw})
+
+            for site, data in sites_raw.items():
+                self.sites.update({site: {
+                    'alias': data['alias'],
+                    'timeout': data['timeout'],
+                }})
+                if data['socket'] == ('local', None):
+                    self.sites[site]['socket'] = self.socket_path
+                else:
+                    protocol, socket = data['socket']
+                    address, port = socket['address']
+                    self.sites[site]['socket'] = f'{protocol}:{address}:{port}'
+                    self.sites[site]['tls'] = socket['tls']
+        else:
+            self.sites.update({SiteId('local'): {
+                'alias': 'Local site',
+                'timeout': 5,
+                'socket': self.socket_path
+            }})
+
+        self.c = MultiSiteConnection(self.sites)
+        dead_sites = [site['site']['alias'] for site in self.c.dead_sites().values()]
+        if dead_sites:
+            self.c.set_only_sites(self.c.alive_sites())
+
+    def query(self, query: str):
+        data: MutableSequence[Tuple[str, str]] = self.c.query(query=query)
+
+        if data:
+            return data
+        else:
+            return None
+
+    def filter_sites(self, include: bool, sites: Sequence[str]):
+        if include is True:
+            site_list = [site for site in self.c.sites if site in sites]
+        else:
+            site_list = [site for site in self.c.sites if site not in sites]
+
+        self.c.set_only_sites(site_list)
+
+
+class TopoObjects:
+    def __init__(self) -> None:
+        self.topo_objects: MutableMapping[str, object] = {}
+
+    def add_host(
+            self,
+            host: str,
+            emblem: str | None = None,
+            icon: str | None = None,
+            link2core: bool = True,
+            obj_id_prefix: str = '',
+    ):
+        if self.topo_objects.get(f'{obj_id_prefix}{host}') is not None:
+            return
+
+        metadata = {}
+        if emblem or icon:
+            metadata = {'images': {}}
+
+        if emblem is not None:
+            metadata['images'].update({'emblem': emblem})  # node image
+
+        if icon is not None:
+            metadata['images'].update({'icon': icon})  # node icon
+
+        self.topo_objects[f'{obj_id_prefix}{host}'] = {
+            'name': host,
+            'link': {'core': host} if link2core else {},
+            'metadata': metadata,
+        }
+
+    def add_service(
+            self,
+            host: str,
+            service: str,
+            emblem: str | None = None,
+            link2core: bool = True,
+    ):
+        obj_id = f'{service}@{host}'
+        if self.topo_objects.get(obj_id) is not None:
+            return
+
+        metadata = {}
+        if emblem is not None:
+            metadata = {
+                'images': {
+                    'emblem': emblem,  # node image
+                },
+            }
+
+        self.topo_objects[obj_id] = {
+            'name': service,
+            'link': {'core': [host, service]} if link2core else {},
+            'metadata': metadata,
+        }
+
+
+def get_connection_color(left_state: int, right_state:int) -> str:
+    state_to_color = {
+        0: 'white',
+        1: 'yellow',
+        2: 'red',
+        3: 'orange',
+    }
+
+    return state_to_color.get(max(left_state, right_state), 'orange')
+
+
+class TopoConnections:
+    def __init__(self) -> None:
+        self.topo_connections: MutableSequence = []
+        self.clean_connections: MutableSequence[Sequence[str]] = []
+
+    def add_connection(
+            self,
+            left: str,
+            right: str,
+            left_state: int = 0,
+            right_state: int = 0,
+    ):
+        connection = [left, right]
+        connection.sort()
+        if connection not in self.clean_connections:
+            self.clean_connections.append(connection)
+            self.topo_connections.append([
+                connection, {
+                    'line_config': {
+                        'css_styles': {
+                            'stroke-dasharray': 'unset'
+                        },
+                        'color': get_connection_color(left_state, right_state),
+                    }
+                }
+            ])
+
+
+# taken from https://github.com/Checkmk/checkmk/blob/master/cmk/plugins/smb/lib/check_disk_smb.py
+# Change-Id: I7426f6553a906c5ac50a8306157931a10640a526
+@dataclass
+class Metric:
+    name: str
+    value: float
+    levels: tuple[float, float] | None = None
+    boundaries: tuple[float, float] | None = None
+
+    def __str__(self) -> str:
+        l = f"{self.levels[0]};{self.levels[1]};" if self.levels else ";;"
+        b = f"{self.boundaries[0]};{self.boundaries[1]}" if self.boundaries else ";"
+        # I'm not too sure about the single quotes here, but keeping it for now
+        return f"'{self.name}'={self.value}B;{l}{b}"
\ No newline at end of file
diff --git a/source/cmk_addons_plugins/bgp_topology/libexec/check_bgp_topology b/source/cmk_addons_plugins/bgp_topology/libexec/check_bgp_topology
new file mode 100755
index 0000000..d1c8d43
--- /dev/null
+++ b/source/cmk_addons_plugins/bgp_topology/libexec/check_bgp_topology
@@ -0,0 +1,18 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# License: GNU General Public License v2
+#
+# Author: thl-cmk[at]outlook[dot]com
+# URL   : https://thl-cmk.hopto.org
+# Date  : 2024-07-20
+# File  : bgp_topology/lib_exec/bgp_topology
+
+import sys
+
+from cmk_addons.plugins.bgp_topology.lib.bgp_topology import main
+
+if __name__ == "__main__":
+    sys.exit(main())
+
+
diff --git a/source/cmk_addons_plugins/bgp_topology/rulesets/bgp_topology.py b/source/cmk_addons_plugins/bgp_topology/rulesets/bgp_topology.py
new file mode 100644
index 0000000..7038083
--- /dev/null
+++ b/source/cmk_addons_plugins/bgp_topology/rulesets/bgp_topology.py
@@ -0,0 +1,176 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# License: GNU General Public License v2
+#
+# Author: thl-cmk[at]outlook[dot]com
+# URL   : https://thl-cmk.hopto.org
+# Date  : 2024-07-06
+# File  : bgp_topology/rulesets/bgp_topology.py
+
+from collections.abc import Sequence
+
+from cmk.rulesets.v1 import Help, Label, Message, Title
+from cmk.rulesets.v1.form_specs import (
+    CascadingSingleChoice,
+    CascadingSingleChoiceElement,
+    DefaultValue,
+    DictElement,
+    Dictionary,
+    FixedValue,
+    SingleChoice,
+    SingleChoiceElement,
+    String,
+    List
+)
+
+from cmk.rulesets.v1.form_specs.validators import LengthInRange, ValidationError
+from cmk.rulesets.v1.rule_specs import ActiveCheck, Topic
+from cmk_addons.plugins.bgp_topology.constants import (
+    EMBLEM_BGP_AS,
+    EMBLEM_BGP_ID,
+    PARAM_BGP_AS,
+    PARAM_BGP_EXT_ANCHOR,
+    PARAM_BGP_EXT_ANCHOR_AS,
+    PARAM_BGP_EXT_ANCHOR_BOTH,
+    PARAM_BGP_EXT_ANCHOR_ID,
+    PARAM_BGP_EXT_ANCHOR_NONE,
+    PARAM_BGP_ID,
+    PARAM_BGP_SITES_EXCLUDE,
+    PARAM_BGP_SITES_FILTER,
+    PARAM_BGP_SITES_INCLUDE,
+    PARAM_EMBLEM_CUSTOM,
+    PARAM_EMBLEM_DEFAULT,
+    PARAM_EMBLEM_NO_EMBLEM,
+    PARAM_MAKE_DEFAULT,
+    PICTURE_TYPE_EMBLEM,
+    RULE_SET_NAME_BGP_TOPOLOGY,
+)
+
+
+class DuplicateInList:  # pylint: disable=too-few-public-methods
+    """ Custom validator that ensures the validated list has no duplicate entries. """
+
+    def __init__(
+            self,
+    ) -> None:
+        pass
+
+    @staticmethod
+    def _get_default_errmsg(_duplicates: Sequence) -> Message:
+        return Message(f"Duplicate element in list. Duplicate elements: {', '.join(_duplicates)}")
+
+    def __call__(self, value: List[str] | None) -> None:
+        if not isinstance(value, list):
+            return
+        _duplicates = [value[i] for i, x in enumerate(value) if value.count(x) > 1]
+        _duplicates = list(set(_duplicates))
+        if _duplicates:
+            raise ValidationError(message=self._get_default_errmsg(_duplicates))
+
+
+def get_emblem_element(default_emblem: str, picture_type: str) -> Sequence[CascadingSingleChoiceElement]:
+    return [
+        CascadingSingleChoiceElement(
+            name=PARAM_EMBLEM_NO_EMBLEM,
+            title=Title(f'No custom {picture_type}'),
+            parameter_form=FixedValue(
+                value=True,
+                label=Label(f'No custom {picture_type} will be used')
+            )),
+        CascadingSingleChoiceElement(
+            name=PARAM_EMBLEM_DEFAULT,
+            title=Title(f'Use default {picture_type}'),
+            parameter_form=FixedValue(
+                value=True,
+                label=Label(f'"{default_emblem}" will be used as {picture_type}')
+            )),
+        CascadingSingleChoiceElement(
+            name=PARAM_EMBLEM_CUSTOM,
+            title=Title(f'Use custom {picture_type}'),
+            parameter_form=String(
+                custom_validate=(LengthInRange(min_value=1),),
+                prefill=DefaultValue(default_emblem),
+            ))
+    ]
+
+
+def _parameter_form() -> Dictionary:
+    return Dictionary(
+        elements={
+            PARAM_BGP_AS: DictElement(
+                parameter_form=CascadingSingleChoice(
+                    title=Title('BGP AS emblem'),
+                    elements=get_emblem_element(EMBLEM_BGP_AS, PICTURE_TYPE_EMBLEM),
+                    prefill=DefaultValue(PARAM_EMBLEM_DEFAULT),
+                    help_text=Help(
+                        'Here you can change the picture for the BGP-AS object. '
+                        'If you use the built-in icons prefix the name with "icon_"'
+                    ),
+                )),
+            PARAM_BGP_ID: DictElement(
+                parameter_form=CascadingSingleChoice(
+                    title=Title('BGP ID emblem'),
+                    elements=get_emblem_element(EMBLEM_BGP_ID, PICTURE_TYPE_EMBLEM),
+                    prefill=DefaultValue(PARAM_EMBLEM_DEFAULT),
+                    help_text=Help(
+                        'Here you can change the picture for the BGP-ID object. '
+                        'If you use the built-in icons prefix the name with "icon_"'
+                    ),
+                )),
+            PARAM_BGP_EXT_ANCHOR: DictElement(
+                parameter_form=SingleChoice(
+                    title=Title('Anchor for external BGP objects'),
+                    elements=[
+                        SingleChoiceElement(name=PARAM_BGP_EXT_ANCHOR_BOTH, title=Title('Use BGP AS and ID')),
+                        SingleChoiceElement(name=PARAM_BGP_EXT_ANCHOR_AS, title=Title('Use BGP AS only')),
+                        SingleChoiceElement(name=PARAM_BGP_EXT_ANCHOR_ID, title=Title('Use BGP ID only')),
+                        SingleChoiceElement(name=PARAM_BGP_EXT_ANCHOR_NONE, title=Title(f'No anchor object'))
+                    ],
+                    prefill=DefaultValue(PARAM_BGP_EXT_ANCHOR_BOTH),
+                    help_text=Help('Select how BGP elements external to Checkmk will be created'),
+                )),
+            PARAM_MAKE_DEFAULT: DictElement(
+                parameter_form=FixedValue(
+                    title=Title('Make default'),
+                    label=Label('This will be the default topology'),
+                    help_text=Help(
+                        'Makes the topology the default topology. If there no default topology, this '
+                        'topology becomes always the default.'
+                    ),
+                    value=True
+                )),
+            PARAM_BGP_SITES_FILTER: DictElement(
+                parameter_form=CascadingSingleChoice(
+                    title=Title('Filter Checkmk sites'),
+                    elements=[
+                        CascadingSingleChoiceElement(
+                            name=PARAM_BGP_SITES_INCLUDE,
+                            title=Title('Include checkmk sites'),
+                            parameter_form=List(element_template=String(
+                                title=Title('checkmk site name'),
+                                custom_validate=(DuplicateInList(),),
+                            ))
+                        ),
+                        CascadingSingleChoiceElement(
+                            name=PARAM_BGP_SITES_EXCLUDE,
+                            title=Title('Exclude checkmk sites'),
+                            parameter_form=List(element_template=String(
+                                title=Title('Checkmk site name'),
+                                custom_validate=(DuplicateInList(),),
+                            ))
+                        )
+                    ],
+                    prefill=DefaultValue(PARAM_BGP_SITES_INCLUDE),
+                    help_text=Help(''),
+                )),
+        }
+    )
+
+
+rule_spec_bgp_topo = ActiveCheck(
+    name=RULE_SET_NAME_BGP_TOPOLOGY,
+    topic=Topic.NETWORKING,
+    parameter_form=_parameter_form,
+    title=Title('BGP Topology'),
+)
diff --git a/source/cmk_addons_plugins/bgp_topology/server_side_calls/bgp_topology.py b/source/cmk_addons_plugins/bgp_topology/server_side_calls/bgp_topology.py
new file mode 100644
index 0000000..37baa8e
--- /dev/null
+++ b/source/cmk_addons_plugins/bgp_topology/server_side_calls/bgp_topology.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# License: GNU General Public License v2
+#
+# Author: thl-cmk[at]outlook[dot]com
+# URL   : https://thl-cmk.hopto.org
+# Date  : 2024-07-20
+# File  : bgp_topology/server_side_calls/bgp_topology.py
+
+
+from collections.abc import Iterator, Mapping, Sequence
+from pydantic import BaseModel
+from typing import Literal
+
+from cmk.utils import debug
+from cmk.server_side_calls.v1 import (
+    HostConfig,
+    Secret,
+    ActiveCheckCommand,
+    ActiveCheckConfig,
+)
+
+from cmk_addons.plugins.bgp_topology.constants import (
+    ARG_PARSER_ANCHOR,
+    ARG_PARSER_ANCHOR_AS,
+    ARG_PARSER_ANCHOR_ID,
+    ARG_PARSER_EMBLEM_AS,
+    ARG_PARSER_EMBLEM_ID,
+    ARG_PARSER_HOST,
+    ARG_PARSER_MAKE_DEFAULT,
+    ARG_PARSER_NONE,
+    ARG_PARSER_SITES_EXCLUDE,
+    ARG_PARSER_SITES_INCLUDE,
+    PARAM_BGP_EXT_ANCHOR_AS,
+    PARAM_BGP_EXT_ANCHOR_BOTH,
+    PARAM_BGP_EXT_ANCHOR_ID,
+    PARAM_BGP_EXT_ANCHOR_NONE,
+    PARAM_BGP_SITES_EXCLUDE,
+    PARAM_BGP_SITES_INCLUDE,
+    PARAM_EMBLEM_CUSTOM,
+    PARAM_EMBLEM_DEFAULT,
+    PARAM_EMBLEM_NO_EMBLEM,
+    RULE_SET_NAME_BGP_TOPOLOGY,
+)
+
+EMBLEM = (
+        tuple[Literal["default_emblem"], bool] |
+        tuple[Literal['custom_emblem'], str] |
+        tuple[Literal['no_emblem'], bool] |
+        None
+)
+
+FILTER_SITES = (
+    tuple[Literal['exclude_sites'], Sequence[str]] |
+    tuple[Literal['include_sites'], Sequence[str]]
+)
+
+class BgpAnchorType:
+    anchor_both: str = PARAM_BGP_EXT_ANCHOR_BOTH
+    anchor_as: str = PARAM_BGP_EXT_ANCHOR_AS
+    anchor_id: str = PARAM_BGP_EXT_ANCHOR_ID
+    anchor_none: str = PARAM_BGP_EXT_ANCHOR_NONE
+
+
+class Emblem:
+    default: str = PARAM_EMBLEM_DEFAULT
+    custom: str = PARAM_EMBLEM_CUSTOM
+    no_emblem: str = PARAM_EMBLEM_NO_EMBLEM
+
+
+class Params(BaseModel):
+    bgp_as: EMBLEM = None
+    bgp_id: EMBLEM = None
+    bgp_ext_anchor: str | None = None
+    make_default: bool | None = None
+    bgp_filter_sites: FILTER_SITES | None = None
+
+
+__params = {
+    'bgp_as': ('custom_emblem', 'icon_cloud'),
+    'bgp_id': ('no_emblem', True),
+    'bgp_ext_anchor': 'bgp_ext_anchor_both',
+    'make_default': True
+}
+__parsed = Params(
+    bgp_as=('custom_emblem', 'icon_cloud'),
+    bgp_id=('no_emblem', True),
+    bgp_ext_anchor='bgp_ext_anchor_both',
+    make_default=True
+)
+
+
+def _commands_bgp_topology_parser(params: Mapping[str, object]) -> Params:
+    if debug.enabled():
+        print(params)
+    return Params.model_validate(params)
+
+
+def commands_bgp_topology_arguments(
+        params: Params, host_config: HostConfig
+) -> Iterator[ActiveCheckCommand]:
+    if debug.enabled():
+        pass
+        # print(host_config)
+
+    args: list[str | Secret] = []
+    args += [ARG_PARSER_HOST, host_config.name.replace(' ', '_')]
+    if params.make_default is True:
+        args.append(ARG_PARSER_MAKE_DEFAULT)
+
+    if params.bgp_ext_anchor is not None:
+        match params.bgp_ext_anchor:
+            case BgpAnchorType.anchor_both:
+                pass  # this is the default
+            case BgpAnchorType.anchor_as:
+                args += [ARG_PARSER_ANCHOR, ARG_PARSER_ANCHOR_AS]
+            case BgpAnchorType.anchor_id:
+                args += [ARG_PARSER_ANCHOR, ARG_PARSER_ANCHOR_ID]
+            case BgpAnchorType.anchor_none:
+                args += [ARG_PARSER_ANCHOR, ARG_PARSER_NONE]
+            case _:
+                pass
+
+    if params.bgp_id:
+        key, value = params.bgp_id
+        match key:
+            case Emblem.default:
+                pass  # this is the default
+            case Emblem.custom:
+                args += [ARG_PARSER_EMBLEM_ID, value]
+            case Emblem.no_emblem:
+                args += [ARG_PARSER_EMBLEM_ID, ARG_PARSER_NONE]
+            case _:
+                pass
+
+    if params.bgp_as:
+        key, value = params.bgp_as
+        match key:
+            case Emblem.default:
+                pass  # this is the default
+            case Emblem.custom:
+                args += [ARG_PARSER_EMBLEM_AS, value]
+            case Emblem.no_emblem:
+                args += [ARG_PARSER_EMBLEM_AS, ARG_PARSER_NONE]
+            case _:
+                pass
+
+    if params.bgp_filter_sites:
+        class FilterMode:
+            exclude_sites = PARAM_BGP_SITES_EXCLUDE
+            include_sites = PARAM_BGP_SITES_INCLUDE
+
+        mode, site_list = params.bgp_filter_sites
+        match mode:
+            case FilterMode.exclude_sites:
+                args.append(ARG_PARSER_SITES_EXCLUDE)
+                args += site_list
+            case FilterMode.include_sites:
+                args.append(ARG_PARSER_SITES_INCLUDE)
+                args += site_list
+            case _:
+                pass
+
+    if debug.enabled():
+        print(args)
+
+    yield ActiveCheckCommand(
+        service_description="BGP Topology",
+        command_arguments=args
+    )
+
+
+active_check_bgp_topology = ActiveCheckConfig(
+    name=RULE_SET_NAME_BGP_TOPOLOGY,
+    parameter_parser=_commands_bgp_topology_parser,
+    commands_function=commands_bgp_topology_arguments,
+)
diff --git a/source/packages/bgp_topology b/source/packages/bgp_topology
new file mode 100644
index 0000000..7ebfba5
--- /dev/null
+++ b/source/packages/bgp_topology
@@ -0,0 +1,16 @@
+{'author': 'Th.L. (thl-cmk[at]outlook[dot]com)',
+ 'description': 'Active check to create the BGP peer topology\n',
+ 'download_url': 'https://thl-cmk.hopto.org',
+ 'files': {'cmk_addons_plugins': ['bgp_topology/constants.py',
+                                  'bgp_topology/lib/bgp_topology.py',
+                                  'bgp_topology/lib/utils.py',
+                                  'bgp_topology/libexec/check_bgp_topology',
+                                  'bgp_topology/rulesets/bgp_topology.py',
+                                  'bgp_topology/server_side_calls/bgp_topology.py',
+                                  'bgp_topology/graphing/bgp_topology.py']},
+ 'name': 'bgp_topology',
+ 'title': 'BGP peer topology',
+ 'version': '0.0.1-20240722',
+ 'version.min_required': '2.3.0b1',
+ 'version.packaged': 'cmk-mkp-tool 0.2.0',
+ 'version.usable_until': '2.4.0b1'}
-- 
GitLab