From 510a2d6a9b863db643936192b473f89c3f20bcb1 Mon Sep 17 00:00:00 2001
From: "th.l" <thl-cmk@outlook.com>
Date: Wed, 10 Jul 2024 17:56:12 +0200
Subject: [PATCH] update project

---
 README.md                                     |   2 +-
 mkp/vsphere_topo-0.0.1-20240709.mkp           | Bin 0 -> 5934 bytes
 source/checkman/.gitkeep                      |   0
 source/checkman/vsphere_topo                  |  45 ---
 .../vsphere_topo/agent_based/packages.py      | 291 ++++++++++++++++++
 .../vsphere_topo/lib/utils.py                 | 164 ++++++++++
 .../vsphere_topo/rulesets/packages.py         | 169 ++++++++++
 source/packages/vsphere_topo                  |  17 +
 8 files changed, 642 insertions(+), 46 deletions(-)
 create mode 100644 mkp/vsphere_topo-0.0.1-20240709.mkp
 delete mode 100644 source/checkman/.gitkeep
 delete mode 100644 source/checkman/vsphere_topo
 create mode 100644 source/cmk_addons_plugins/vsphere_topo/agent_based/packages.py
 create mode 100644 source/cmk_addons_plugins/vsphere_topo/lib/utils.py
 create mode 100644 source/cmk_addons_plugins/vsphere_topo/rulesets/packages.py
 create mode 100644 source/packages/vsphere_topo

diff --git a/README.md b/README.md
index 9209cf4..6362bf4 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/vsphere_topo-0.0.1-20240709.mkp "vsphere_topo-0.0.1-20240709.mkp"
 # Title
 
 A short description about the plugin
diff --git a/mkp/vsphere_topo-0.0.1-20240709.mkp b/mkp/vsphere_topo-0.0.1-20240709.mkp
new file mode 100644
index 0000000000000000000000000000000000000000..488875b9d5a288c645153386c117138b2394fd94
GIT binary patch
literal 5934
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
BkQo2~

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/vsphere_topo b/source/checkman/vsphere_topo
deleted file mode 100644
index 08ef898..0000000
--- a/source/checkman/vsphere_topo
+++ /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/vsphere_topo/agent_based/packages.py b/source/cmk_addons_plugins/vsphere_topo/agent_based/packages.py
new file mode 100644
index 0000000..912b4c4
--- /dev/null
+++ b/source/cmk_addons_plugins/vsphere_topo/agent_based/packages.py
@@ -0,0 +1,291 @@
+#!/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-05
+# File  : vsphere_topo/agent_based/packages.py
+
+from collections.abc import Mapping, Sequence
+from time import time_ns
+from typing import Any
+
+from cmk.agent_based.v2 import (
+    CheckPlugin,
+    CheckResult,
+    DiscoveryResult,
+    Result,
+    Service,
+    State,
+)
+from cmk.base.check_api import host_name
+from cmk_addons.plugins.vsphere_topo.lib.utils import (
+    EMBLEM_CLUSTER,
+    EMBLEM_DATA_CENTER,
+    EMBLEM_DATA_STORE,
+    LiveStatusConnection,
+    OMD_ROOT,
+    PARAM_ADD_DUMMY_TOPOLOGIES,
+    PARAM_CLUSTER,
+    PARAM_DATA_CENTER,
+    PARAM_DATA_STORE,
+    PARAM_DATA_STORE_AS_SERVICE,
+    PARAM_DONT_ADD_VC_AS_VM,
+    PARAM_HOST_SYSTEMS,
+    PARAM_MAKE_DEFAULT,
+    PARAM_VM_NAMES,
+    RULE_SET_NAME_VSPHERE_TOPO,
+    adjust_name,
+    get_emblem,
+    save_data_to_file,
+)
+
+Section = Mapping[str, object]
+
+
+def discover_vsphere_topo(
+        section_esx_vsphere_clusters: Section | None,
+        section_esx_vsphere_virtual_machines: Sequence[Section] | None,
+        section_esx_vsphere_datastores: Section | None,
+) -> DiscoveryResult:
+    yield Service()
+
+
+def check_vsphere_topo(
+        params: Mapping[str, Any],
+        section_esx_vsphere_clusters: Section | None,
+        section_esx_vsphere_virtual_machines: Sequence[Section] | None,
+        section_esx_vsphere_datastores: Section | None,
+) -> CheckResult:
+    def add_host(host: str, emblem: str | None = None, link2core: bool = True):
+        if objects.get(host) is not None:
+            return
+
+        metadata = {}
+        if emblem is not None:
+            metadata = {
+                'images': {
+                    'emblem': emblem,  # node image
+                },
+            }
+
+        objects[host] = {
+            "name": host,
+            "link": {'core': host} if link2core else {},
+            'metadata': metadata,
+        }
+
+    def add_service(host: str, service: str, emblem: str | None = None, link2core: bool = True):
+        obj_id = f'{service}@{host}'
+        if objects.get(obj_id) is not None:
+            return
+
+        metadata = {}
+        if emblem is not None:
+            metadata = {
+                'images': {
+                    'emblem': emblem,  # node image
+                },
+            }
+
+        objects[obj_id] = {
+            "name": service,
+            "link": {'core': [host, service]} if link2core else {},
+            'metadata': metadata,
+        }
+
+    def add_connection(left: str, right: str):
+        connection = [left, right]
+        connection.sort()
+        connections.append([connection, {'line_config': {'css_styles': {'stroke-dasharray': 'unset'}}}])
+
+    start_time = time_ns()
+    objects = {}
+    connections = []
+
+    vsphere_host: str = host_name().strip()
+    add_host(host=vsphere_host)
+
+    __clusters = {
+        'HA': {
+            'datacenter': 'datacenter-21',
+            'hostsystems': 'esxi01, esxi02',
+            'vms': 'vmname01, vmname02, vmname03'
+        }
+    }
+
+    if section_esx_vsphere_clusters is None:
+        yield Result(state=State.UNKNOWN, summary='Found no vSphere data centers/clusters')
+        return
+
+    for cluster in section_esx_vsphere_clusters:
+        data_center: str = section_esx_vsphere_clusters[cluster]['datacenter'].strip()
+        if objects.get(data_center) is None:
+            add_host(
+                host=data_center,
+                emblem=get_emblem(EMBLEM_DATA_CENTER, params.get(PARAM_DATA_CENTER)),
+                link2core=False,
+            )
+            add_connection(data_center, vsphere_host)
+
+        add_host(
+            host=cluster,
+            emblem=get_emblem(EMBLEM_CLUSTER, params.get(PARAM_CLUSTER)),
+            link2core=False
+        )
+        add_connection(cluster, data_center)
+
+        host_systems: Sequence[str] = section_esx_vsphere_clusters[cluster].get('hostsystems', '').split(',')
+        for host_system in host_systems:
+            esx_name: str = adjust_name(host_system, params.get(PARAM_HOST_SYSTEMS, {}))
+            add_host(host=esx_name)
+            add_connection(cluster, esx_name)
+
+    __VMs = [
+        {
+            'vm_name': 'vmname01',
+            'hostsystem': 'esxi01',
+            'powerstate': 'poweredOff',
+            'guest_os': 'Microsoft Windows Server 2008 R2 (64-bit)',
+            'compatibility': 'vmx-10',
+            'uuid': '420354bd-bee0-88d5-053c-d4f424106b0f'
+        },
+        {
+            'vm_name': 'vmname02',
+            'hostsystem': 'esxi02',
+            'powerstate': 'poweredOn',
+            'guest_os': 'Microsoft Windows Server 2019 (64-bit)',
+            'compatibility': 'vmx-19',
+            'uuid': '422eba9f-0327-2da0-b546-f5a5e5dfdb68'
+        },
+        {
+            'vm_name': 'vmname03',
+            'hostsystem': 'esxi01',
+            'powerstate': 'poweredOn',
+            'guest_os': 'Other 3.x or later Linux (64-bit)',
+            'compatibility': 'vmx-11',
+            'uuid': '422e3863-b7de-dc55-74f8-6dc9c159b36d'
+        }
+    ]
+
+    if section_esx_vsphere_virtual_machines is not None:
+        for vm in section_esx_vsphere_virtual_machines:
+            vm_name: str = adjust_name(vm['vm_name'], params.get(PARAM_VM_NAMES, {}))
+
+            if params.get(PARAM_DONT_ADD_VC_AS_VM) is True and vm_name.lower() == vsphere_host.lower():
+                continue
+            host_system: str = adjust_name(vm['hostsystem'], params.get(PARAM_HOST_SYSTEMS, {}))
+            add_host(host=vm_name)
+            add_connection(vm_name, host_system)
+
+    service_prefix: str = ''
+    ds_services = None
+    ls_connection = LiveStatusConnection()
+
+    if section_esx_vsphere_datastores is not None:
+        data_stores_host: str = 'data_stores'  # Anchor for data stores
+        add_host(
+            host=data_stores_host,
+            link2core=False,
+            emblem=get_emblem(EMBLEM_DATA_STORE, params.get(PARAM_DATA_STORE)),
+        )
+        for ds_name in section_esx_vsphere_datastores:
+            add_host(
+                host=ds_name,
+                link2core=False,
+                emblem=get_emblem(EMBLEM_DATA_STORE, params.get(PARAM_DATA_STORE)),
+            )
+            add_connection(ds_name, data_stores_host)
+
+        if params.get(PARAM_DATA_STORE_AS_SERVICE) is True:
+            query: str = (
+                'GET services\n'
+                'Columns: service_description\n'
+                'Filter: service_check_command ~ ^check_mk-esx_vsphere_datastores(!.*)?\n'
+                f'Filter: host_name = {vsphere_host}\n'
+                'OutputFormat: python3\n'
+            )
+            if (ds_services := ls_connection.query(query=query)) is not None:
+                for ds_service in ds_services:
+                    service_name: str = ds_service[0]
+                    service_id: str = f'{service_name}@{vsphere_host}'
+                    ds_name: str = service_name.split('fs_')[-1].split('Filesystem ')[-1]
+                    add_service(
+                        host=vsphere_host,
+                        service=service_name,
+                    )
+                    add_connection(service_id, ds_name)
+                    service_prefix = service_name.split(ds_name)[0]
+
+    query: str = (
+        'GET services\n'
+        'Columns: host_name long_plugin_output\n'
+        'Filter: description = ESX Datastores\n'
+        'OutputFormat: python3\n'
+    )
+    if (vm_to_data_stores := ls_connection.query(query=query)) is not None:
+        for vm, data_stores in vm_to_data_stores:
+            vm_name: str = adjust_name(vm, params.get(PARAM_VM_NAMES, {}))
+            if params.get(PARAM_DONT_ADD_VC_AS_VM) is True and vm_name.lower() == vsphere_host.lower():
+                continue
+            if vm_name in objects:
+                data_stores = data_stores.split(',')
+                for data_store in data_stores:
+                    ds_name = data_store.strip().split(' ')[2]
+                    if ds_services is not None:
+                        service_name = f'{service_prefix}{ds_name}'
+                        service_id = f'{service_name}@{vsphere_host}'
+                        add_connection(service_id, vm_name)
+                    else:
+                        add_connection(ds_name, vm_name)
+
+    connections.sort()
+    data_set_vsphere = {
+        'version': 1,
+        'name': 'vSphere',
+        'objects': dict(sorted(objects.items())),
+        'connections': connections,
+    }
+
+    # workaround for missing topology
+    path: str = f'{OMD_ROOT}/var/check_mk/topology/data/{vsphere_host}'
+    if params.get(PARAM_ADD_DUMMY_TOPOLOGIES) is True:
+        data_sets = [
+            {'version': 1, 'name': 'cdp', 'objects': {}, 'connections': []},
+            {'version': 1, 'name': 'lldp', 'objects': {}, 'connections': []},
+            {'version': 1, 'name': 'static', 'objects': {}, 'connections': []},
+            {'version': 1, 'name': 'l3v4', 'objects': {}, 'connections': []},
+            data_set_vsphere
+        ]
+    else:
+        data_sets = [data_set_vsphere]
+    for data_set in data_sets:
+        file: str = f'data_{data_set["name"].lower()}.json'
+        save_data_to_file(
+            data=data_set,
+            file=file,
+            make_default=params.get(PARAM_MAKE_DEFAULT, False),
+            path=path,
+        )
+
+    yield Result(state=State.OK, summary=f'Objects: {len(objects)}')
+    yield Result(state=State.OK, summary=f'Connections: {len(connections)}')
+    yield Result(state=State.OK, notice=f'Written to: {path}/{file}')
+    yield Result(state=State.OK, summary=f'Time taken: {((time_ns() - start_time) / 1e9):.2}/s')
+
+
+check_plugin_vsphere_topo = CheckPlugin(
+    name="vsphere_topo",
+    service_name="vSphere Topology",
+    sections=[
+        'esx_vsphere_clusters',
+        'esx_vsphere_virtual_machines',
+        'esx_vsphere_datastores',
+    ],
+    check_function=check_vsphere_topo,
+    discovery_function=discover_vsphere_topo,
+    check_ruleset_name=RULE_SET_NAME_VSPHERE_TOPO,
+    check_default_parameters={},
+)
diff --git a/source/cmk_addons_plugins/vsphere_topo/lib/utils.py b/source/cmk_addons_plugins/vsphere_topo/lib/utils.py
new file mode 100644
index 0000000..f32e717
--- /dev/null
+++ b/source/cmk_addons_plugins/vsphere_topo/lib/utils.py
@@ -0,0 +1,164 @@
+#!/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  : vsphere_topo/lib/utils.py
+
+from collections.abc import Mapping, MutableMapping, MutableSequence
+from json import dumps as json_dunps
+from os import environ
+from pathlib import Path
+from typing import Any, Final, Tuple
+
+from livestatus import MultiSiteConnection, SiteConfigurations, SiteId
+
+EMBLEM_CLUSTER: Final[str] = 'icon_plugins_hw'
+EMBLEM_DATA_CENTER: Final[str] = 'icon_cloud'
+EMBLEM_DATA_STORE: Final[str] = 'icon_services_green'
+
+PARAM_ADD_DUMMY_TOPOLOGIES: Final[str] = 'add_dummy_topologies'
+PARAM_CHANGE_CASE: Final[str] = 'change_case'
+PARAM_CLUSTER: Final[str] = 'cluster'
+PARAM_CUSTOM_EMBLEM: Final[str] = 'custom_emblem'
+PARAM_DATA_CENTER: Final[str] = 'data_center'
+PARAM_DATA_STORE: Final[str] = 'data_store'
+PARAM_DATA_STORE_AS_SERVICE: Final[str] = 'data_sore_as_service'
+PARAM_DEFAULT_EMBLEM: Final[str] = 'default_emblem'
+PARAM_DONT_ADD_VC_AS_VM: Final[str] = 'dont_add_vc_as_vm'
+PARAM_HOST_SYSTEMS: Final[str] = 'host_systems'
+PARAM_KEEP_DOMAIN: Final[str] = 'keep_domain'
+PARAM_LOWER: Final[str] = 'lower'
+PARAM_MAKE_DEFAULT: Final[str] = 'make_default'
+PARAM_NO_CHANGE: Final[str] = 'no_change'
+PARAM_NO_EMBLEM: Final[str] = 'no_emblem'
+PARAM_PREFIX: Final[str] = 'prefix'
+PARAM_UPPER: Final[str] = 'upper'
+PARAM_VM_NAMES: Final[str] = 'vm_names'
+
+RULE_SET_NAME_VSPHERE_TOPO: Final[str] = 'vsphere_topo'
+
+OMD_ROOT = environ["OMD_ROOT"]
+
+
+def adjust_name(name: str, params: Mapping[str, Any]) -> str:
+    class Case:
+        upper: str = PARAM_UPPER
+        lower: str = PARAM_LOWER
+        no_change: str = PARAM_NO_CHANGE
+
+    if params.get(PARAM_KEEP_DOMAIN) is None:
+        name = name.split('.')[0]
+
+    match params.get(PARAM_CHANGE_CASE, PARAM_NO_CHANGE):
+        case Case.upper:
+            name = name.upper()
+        case Case.lower:
+            name = name.lower()
+        case Case.no_change | _:  # all else
+            pass
+
+    if (prefix := params.get(PARAM_PREFIX)) is not None:
+        name = f'{prefix.strip()}{name.strip()}'
+
+    return name.strip()
+
+
+def get_emblem(emblem: str, params: Tuple[str, bool | str] | None = None) -> str | None:
+    class Emblem:
+        no_emblem: str = PARAM_NO_EMBLEM
+        default_emblem: str = PARAM_DEFAULT_EMBLEM
+        custom_emblem: str = PARAM_CUSTOM_EMBLEM
+
+    match params:
+        case None:
+            return emblem
+        case (Emblem.no_emblem, True):
+            return None
+        case (Emblem.default_emblem, True):
+            return emblem
+        case _:  # custom_emblem
+            return params[1].strip()
+
+
+def save_data_to_file(data: Mapping, path: str, file: str, make_default: bool) -> None:
+    """
+    Save the data as json file.
+    Args:
+        data: the topology data
+        path: the path were to save the data
+        file: the file name to save the data in
+        make_default: if True, create the symlink "default" with path as target
+
+    Returns:
+        None
+    """
+
+    path_file = f'{path}/{file}'
+    save_file = Path(f'{path_file}')
+    save_file.parent.mkdir(exist_ok=True, parents=True)
+    save_file.write_text(json_dunps(data))
+
+    parent_path = Path(f'{path}').parent
+    if not Path(f'{parent_path}/default').exists():
+        make_default = True
+    if make_default:
+        Path(f'{parent_path}/default').unlink(missing_ok=True)
+        Path(f'{parent_path}/default').symlink_to(target=Path(path), target_is_directory=True)
+
+
+#
+#  live status
+#
+
+class LiveStatusConnection(object):
+    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
diff --git a/source/cmk_addons_plugins/vsphere_topo/rulesets/packages.py b/source/cmk_addons_plugins/vsphere_topo/rulesets/packages.py
new file mode 100644
index 0000000..2133a73
--- /dev/null
+++ b/source/cmk_addons_plugins/vsphere_topo/rulesets/packages.py
@@ -0,0 +1,169 @@
+#!/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  : vsphere_topo/rulesets/packages.py
+
+from collections.abc import Mapping, Sequence
+
+from cmk.rulesets.v1 import Label, Title
+from cmk.rulesets.v1.form_specs import (
+    CascadingSingleChoice,
+    CascadingSingleChoiceElement,
+    DefaultValue,
+    DictElement,
+    Dictionary,
+    FixedValue,
+    SingleChoice,
+    SingleChoiceElement,
+    String,
+)
+
+from cmk.rulesets.v1.form_specs.validators import LengthInRange
+from cmk.rulesets.v1.rule_specs import CheckParameters, HostCondition, Topic
+from cmk_addons.plugins.vsphere_topo.lib.utils import (
+    EMBLEM_CLUSTER,
+    EMBLEM_DATA_CENTER,
+    EMBLEM_DATA_STORE,
+    PARAM_ADD_DUMMY_TOPOLOGIES,
+    PARAM_CHANGE_CASE,
+    PARAM_CLUSTER,
+    PARAM_CUSTOM_EMBLEM,
+    PARAM_DATA_CENTER,
+    PARAM_DATA_STORE,
+    PARAM_DATA_STORE_AS_SERVICE,
+    PARAM_DEFAULT_EMBLEM,
+    PARAM_DONT_ADD_VC_AS_VM,
+    PARAM_HOST_SYSTEMS,
+    PARAM_KEEP_DOMAIN,
+    PARAM_LOWER,
+    PARAM_MAKE_DEFAULT,
+    PARAM_NO_CHANGE,
+    PARAM_NO_EMBLEM,
+    PARAM_PREFIX,
+    PARAM_UPPER,
+    PARAM_VM_NAMES,
+    RULE_SET_NAME_VSPHERE_TOPO,
+)
+
+adjust_names_elements: Mapping[str, DictElement] = {
+    PARAM_KEEP_DOMAIN: DictElement(
+        parameter_form=FixedValue(
+            title=Title('Keep domain name'),
+            label=Label('The domain name will not be cut'),
+            value=True,
+        )),
+    PARAM_CHANGE_CASE: DictElement(
+        parameter_form=SingleChoice(
+            title=Title('Change case'),
+            elements=[
+                SingleChoiceElement(name=PARAM_UPPER, title=Title('All upper case')),
+                SingleChoiceElement(name=PARAM_LOWER, title=Title('All lower case')),
+                SingleChoiceElement(name=PARAM_NO_CHANGE, title=Title('Don\'t change case'))
+            ],
+            prefill=DefaultValue(PARAM_NO_CHANGE)
+        )),
+    PARAM_PREFIX: DictElement(
+        parameter_form=String(
+            title=Title('Name prefix'),
+            custom_validate=(LengthInRange(min_value=1),)
+        )),
+}
+
+
+def get_emblem_element(default_emblem) -> Sequence[CascadingSingleChoiceElement]:
+    return [
+        CascadingSingleChoiceElement(
+            name=PARAM_NO_EMBLEM,
+            title=Title('No custom emblem'),
+            parameter_form=FixedValue(
+                value=True,
+                label=Label('No custom emblem will be used')
+            )),
+        CascadingSingleChoiceElement(
+            name=PARAM_DEFAULT_EMBLEM,
+            title=Title('Use default emblem'),
+            parameter_form=FixedValue(
+                value=True,
+                label=Label(f'Emblem "{default_emblem}" will be used')
+            )),
+        CascadingSingleChoiceElement(
+            name=PARAM_CUSTOM_EMBLEM,
+            title=Title('Use custom emblem'),
+            parameter_form=String(
+                custom_validate=(LengthInRange(min_value=1),),
+                prefill=DefaultValue(default_emblem),
+            ))
+    ]
+
+
+def _parameter_form() -> Dictionary:
+    return Dictionary(
+        elements={
+            PARAM_HOST_SYSTEMS: DictElement(
+                parameter_form=Dictionary(
+                    title=Title('Adjust ESXi host names'),
+                    elements=adjust_names_elements
+                )),
+            PARAM_VM_NAMES: DictElement(
+                parameter_form=Dictionary(
+                    title=Title('Adjust VM names'),
+                    elements=adjust_names_elements
+                )),
+            PARAM_CLUSTER: DictElement(
+                parameter_form=CascadingSingleChoice(
+                    title=Title('Cluster emblem'),
+                    elements=get_emblem_element(EMBLEM_CLUSTER),
+                    prefill=DefaultValue(PARAM_DEFAULT_EMBLEM)
+                )),
+            PARAM_DATA_CENTER: DictElement(
+                parameter_form=CascadingSingleChoice(
+                    title=Title('Datacenter emblem'),
+                    elements=get_emblem_element(EMBLEM_DATA_CENTER),
+                    prefill=DefaultValue(PARAM_DEFAULT_EMBLEM)
+                )),
+            PARAM_DATA_STORE: DictElement(
+                parameter_form=CascadingSingleChoice(
+                    title=Title('Datastore emblem'),
+                    elements=get_emblem_element(EMBLEM_DATA_STORE),
+                    prefill=DefaultValue(PARAM_DEFAULT_EMBLEM)
+                )),
+            PARAM_DATA_STORE_AS_SERVICE: DictElement(
+                parameter_form=FixedValue(
+                    title=Title('Add data store service'),
+                    label=Label('enabled'),
+                    value=True
+                )),
+            PARAM_MAKE_DEFAULT: DictElement(
+                parameter_form=FixedValue(
+                    title=Title('Make default'),
+                    label=Label('This will be the default topology'),
+                    value=True
+                )),
+            PARAM_DONT_ADD_VC_AS_VM: DictElement(
+                parameter_form=FixedValue(
+                    title=Title('Don\'t add vCenter as VM'),
+                    label=Label('The vCenter will not be added as VM'),
+                    value=True
+                )),
+            PARAM_ADD_DUMMY_TOPOLOGIES: DictElement(
+                parameter_form=FixedValue(
+                    title=Title('Add dummy topologies'),
+                    label=Label('Adds empty CDP, LLDP, L3v4 and STATIC topology'),
+                    value=True
+                )),
+        }
+    )
+
+
+rule_spec_vsphere_topo = CheckParameters(
+    name=RULE_SET_NAME_VSPHERE_TOPO,
+    topic=Topic.APPLICATIONS,
+    parameter_form=_parameter_form,
+    title=Title('vSphere Topology'),
+    condition=HostCondition(),
+)
diff --git a/source/packages/vsphere_topo b/source/packages/vsphere_topo
new file mode 100644
index 0000000..62492c4
--- /dev/null
+++ b/source/packages/vsphere_topo
@@ -0,0 +1,17 @@
+{'author': 'Th.L. (thl-cmk[at]outlook[dot]com)',
+ 'description': 'Creates the vSphere topology, based on the data from the ESXI '
+                'special agent (esx_vsphere_clusters, '
+                'esx_vsphere_virtual_machines sections).\n'
+                '\n'
+                'Topolgy is created by:  vCenter  -> data center(s) '
+                '->Cluster(s) -> ESX host(s) -> VM(s)\n',
+ 'download_url': 'https://thl-cmk.hopto.org',
+ 'files': {'cmk_addons_plugins': ['vsphere_topo/agent_based/packages.py',
+                                  'vsphere_topo/rulesets/packages.py',
+                                  'vsphere_topo/lib/utils.py']},
+ 'name': 'vsphere_topo',
+ 'title': 'vSphere Topologie',
+ 'version': '0.0.1-20240709',
+ 'version.min_required': '2.3.0b1',
+ 'version.packaged': 'cmk-mkp-tool 0.2.0',
+ 'version.usable_until': '2.4.0b1'}
-- 
GitLab