diff --git a/.github/workflows/ans-int-test-site.yaml b/.github/workflows/ans-int-test-site.yaml new file mode 100644 index 000000000..1c8f74e18 --- /dev/null +++ b/.github/workflows/ans-int-test-site.yaml @@ -0,0 +1,77 @@ +# README: +# - When changing the module name, it needs to be changed in 'env:MODULE_NAME' and in 'on:pull_requests:path'! +# +# Resources: +# - Template for this file: https://github.com/ansible-collections/collection_template/blob/main/.github/workflows/ansible-test.yml +# - About Ansible integration tests: https://docs.ansible.com/ansible/latest/dev_guide/testing_integration.html + +env: + NAMESPACE: checkmk + COLLECTION_NAME: general + MODULE_NAME: site + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +name: Ansible Integration Tests for Site Management Module +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * 0' + pull_request: + branches: + - main + - devel + paths: + - 'plugins/modules/site.py' + push: + paths: + - 'plugins/modules/site.py' + +jobs: + + integration: + runs-on: ubuntu-22.04 + name: Ⓐ${{ matrix.ansible }}+py${{ matrix.python }} + strategy: + fail-fast: false + matrix: + ansible: + - stable-2.15 + - stable-2.16 + - stable-2.17 + - devel + python: + - '3.9' + - '3.10' + - '3.11' + - '3.12' + exclude: + # Exclude unsupported sets. + - ansible: stable-2.15 + python: '3.12' + + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + path: ansible_collections/${{env.NAMESPACE}}/${{env.COLLECTION_NAME}} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install ansible-base (${{ matrix.ansible }}) + run: pip install https://github.com/ansible/ansible/archive/${{ matrix.ansible }}.tar.gz --disable-pip-version-check + + - name: Provide secrets file + run: echo "${{ secrets.CHECKMK_DOWNLOAD_PW }}" > ./tests/integration/files/.dl-secret + working-directory: ./ansible_collections/${{env.NAMESPACE}}/${{env.COLLECTION_NAME}} + env: + CHECKMK_DOWNLOAD_PW: ${{ secrets.CHECKMK_DOWNLOAD_PW }} + + - name: Run integration test + run: ansible-test integration ${{env.MODULE_NAME}} -v --color --retry-on-error --continue-on-error --diff --python ${{ matrix.python }} --docker default + working-directory: ./ansible_collections/${{env.NAMESPACE}}/${{env.COLLECTION_NAME}} diff --git a/README.md b/README.md index 891e52658..cec8c6082 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ Name | Description | Tests [checkmk.general.host](https://github.com/Checkmk/ansible-collection-checkmk.general/blob/main/plugins/modules/host.py)|Manage hosts.|[![Integration Tests for Host Module](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-host.yaml/badge.svg)](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-host.yaml) [checkmk.general.rule](https://github.com/Checkmk/ansible-collection-checkmk.general/blob/main/plugins/modules/rule.py)|Manage rules.|[![Integration Tests for Rule Module](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-rule.yaml/badge.svg)](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-rule.yaml) [checkmk.general.service_group](https://github.com/Checkmk/ansible-collection-checkmk.general/blob/main/plugins/modules/service_group.py)|Manage service groups.|[![Integration Tests for Service Group Module](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-service_group.yaml/badge.svg)](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-service_group.yaml) +[checkmk.general.site](https://github.com/Checkmk/ansible-collection-checkmk.general/blob/main/plugins/modules/site.py)|Manage sites.|[![Integration Tests for Site Module](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-site.yaml/badge.svg)](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-site.yaml) [checkmk.general.tag_group](https://github.com/Checkmk/ansible-collection-checkmk.general/blob/main/plugins/modules/tag_group.py)|Manage tag groups.|[![Integration Tests for Tag Group Module](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-tag_group.yaml/badge.svg)](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-tag_group.yaml) [checkmk.general.user](https://github.com/Checkmk/ansible-collection-checkmk.general/blob/main/plugins/modules/user.py)|Manage users.|[![Integration Tests for User Module](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-user.yaml/badge.svg)](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-user.yaml) @@ -152,7 +153,6 @@ Please do **not** consider it a concrete planning document! - Setup - Agents - BI - - Distributed Monitoring - Notification Rules - Dynamic Inventory - OMD Module diff --git a/changelogs/fragments/site.yml b/changelogs/fragments/site.yml new file mode 100644 index 000000000..159f77ce1 --- /dev/null +++ b/changelogs/fragments/site.yml @@ -0,0 +1,9 @@ +major_changes: + - Site module - Added a module for distributed monitoring. + Refer to the module documentation for further details. + +known_issues: + - Site module - To completely enable a site, the livestatus certificate + needs to be trusted. This cannot be done with the site module. + As of now, there is no automatic way to do this, so you need to log into + the site and add the certificate to the trusted certificates manually. diff --git a/playbooks/usecases/setup-distributed-monitoring.yml b/playbooks/usecases/setup-distributed-monitoring.yml new file mode 100644 index 000000000..b60480e65 --- /dev/null +++ b/playbooks/usecases/setup-distributed-monitoring.yml @@ -0,0 +1,81 @@ +--- +# This playbook uses the inventory from the 'playbooks/hosts' file and expects +# The VM 'debsible' to be running. + +- name: "Install a Central and Remote Site and connect them." + hosts: debsible + strategy: linear + vars: + checkmk_server_version: "2.3.0p14" + checkmk_server_edition: "cre" + checkmk_server_admin_pass: "password" + checkmk_server_sites: + - name: central + version: "{{ checkmk_server_version }}" + state: started + admin_pw: "{{ checkmk_server_admin_pass }}" + update_conflict_resolution: install + - name: remote + version: "{{ checkmk_server_version }}" + state: started + admin_pw: "{{ checkmk_server_admin_pass }}" + update_conflict_resolution: install + omd_auto_restart: 'true' + omd_config: + - var: LIVESTATUS_TCP + value: "on" + - var: LIVESTATUS_TCP_TLS + value: "on" + - var: LIVESTATUS_TCP_PORT + value: "6558" + tasks: + - name: "Install Checkmk and create Sites." + ansible.builtin.import_role: + name: checkmk.general.server + - name: "Connect Central Site to Remote Site." + checkmk.general.site: + server_url: "http://localhost/" + site: "central" + automation_user: "cmkadmin" + automation_secret: "{{ checkmk_server_admin_pass }}" + site_id: "remote" + site_connection: + site_config: + status_connection: + connection: + socket_type: tcp + port: 6558 + encrypted: true + host: localhost + verify: true + proxy: + use_livestatus_daemon: "direct" + connect_timeout: 2 + status_host: + status_host_set: "disabled" + url_prefix: "/remote/" + configuration_connection: + enable_replication: true + url_of_remote_site: "http://localhost/remote/check_mk/" + basic_settings: + site_id: "remote" + alias: "My Remote Site" + state: "present" + - name: "Log in to Remote Site." + checkmk.general.site: + server_url: "http://localhost/" + site: "central" + automation_user: "cmkadmin" + automation_secret: "{{ checkmk_server_admin_pass }}" + site_id: "remote" + site_connection: + authentication: + username: "cmkadmin" + password: "{{ checkmk_server_admin_pass }}" + state: "login" + - name: "Activate Changes on all Sites." + checkmk.general.activation: + server_url: "http://localhost/" + site: "central" + automation_user: "cmkadmin" + automation_secret: "{{ checkmk_server_admin_pass }}" diff --git a/plugins/doc_fragments/site_options.py b/plugins/doc_fragments/site_options.py new file mode 100644 index 000000000..8c6ce12e6 --- /dev/null +++ b/plugins/doc_fragments/site_options.py @@ -0,0 +1,326 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +class ModuleDocFragment(object): + DOCUMENTATION = r""" + options: + state: + description: + - The desired state of this site connection. + type: str + choices: ['present', 'absent', 'login', 'logout'] + default: present + site_id: + description: + - The site ID to manage. + required: true + type: str + site_connection: + description: + - The settings of the site. + type: dict + suboptions: + authentication: + description: + - The authentication data for a configuration connection. + - Only required when the O(state) is V(login). + type: dict + suboptions: + username: + description: + - A user with administrative permissions. + type: str + password: + description: + - The password for the username provided. + type: str + site_config: + description: + - A site's connection. + - Only required when that O(state) is V(present). + type: dict + suboptions: + status_connection: + description: + - A site's status connection. + type: dict + suboptions: + connection: + description: + - When connecting to sites on remote servers, please + - make sure that Livestatus over TCP is activated there. + - You can use UNIX sockets to connect to foreign sites on + - localhost. + type: dict + suboptions: + socket_type: + description: + - The connection type. + type: str + choices: ['tcp', 'tcp6', 'unix', 'local'] + port: + description: + - The Livestatus TCP port to connect to. + type: int + encrypted: + description: + - Enable encryption for the connection. + type: bool + verify: + description: + - Verify remote site's certificate. + type: bool + host: + description: + - The IP or domain name of the host + - running the remote site. + type: str + path: + description: + - When the connection name is unix, + - this is the path to the unix socket. + type: str + proxy: + description: + - The Livestatus Proxy Daemon configuration attributes. + type: dict + suboptions: + use_livestatus_daemon: + description: + - Use Livestatus daemon with direct connection + - or with Livestatus proxy. + type: str + choices: [with_proxy, direct] + global_settings: + description: + - When O(site_connection.site_config.status_connection.proxy.use_livestatus_daemon) + - is set to V(with_proxy), + - you can set this to V(true) to use global setting or + - V(false) to use custom parameters. + type: bool + tcp: + description: + - Allow access to Livestatus via TCP. + type: dict + suboptions: + port: + description: + - The TCP port to open. + type: int + only_from: + description: + - Restrict access to these IP addresses. + type: list + elements: str + tls: + description: + - Encrypt TCP Livestatus connections. + type: bool + default: false + params: + description: + - The Livestatus Proxy Daemon parameters. + type: dict + suboptions: + channels: + description: + - The number of channels to keep open. + type: int + default: 5 + heartbeat: + description: + - The heartbeat interval and timeout + - configuration. + type: dict + suboptions: + interval: + description: + - The heartbeat interval + - for the TCP connection. + type: int + default: 5 + timeout: + description: + - The heartbeat timeout + - for the TCP connection. + type: int + default: 2 + channel_timeout: + description: + - The timeout waiting for a free channel. + type: int + default: 3 + query_timeout: + description: + - The total query timeout. + type: int + default: 120 + connect_retry: + description: + - The cooling period after failed + - connect or heartbeat. + type: int + default: 4 + cache: + description: + - Enable caching of several non-status queries. + type: bool + default: true + connect_timeout: + description: + - The time that the GUI waits for a connection to the site + - to be established before the site is considered to be + - unreachable. + type: int + default: 2 + persistent_connection: + description: + - If you enable persistent connections then Multisite + - will try to keep open a number of connections + - to the remote sites. + type: bool + default: false + url_prefix: + description: + - The URL prefix will be prepended to links of addons like + - NagVis when a link to such applications points to a host + - or service on that site. + type: str + status_host: + description: + - By specifying a status host for each non-local connection + - you prevent Multisite from running into timeouts when + - remote sites do not respond. + - I(This setting can be omitted, when) + - O(site_connection.site_config.status_connection.proxy.use_livestatus_daemon) + - I(is set to) V(with_proxy)! + type: dict + suboptions: + status_host_set: + description: + - enabled for 'use the following status host' and + - disabled for 'no status host' + type: str + choices: [enabled, disabled] + default: disabled + site: + description: + - The site ID of the status host. + type: str + host: + description: + - The host name of the status host. + type: str + disable_in_status_gui: + description: + - If you disable a connection, then no data of this site will + - be shown in the status GUI. The replication is not affected + - by this, however. + type: bool + default: false + configuration_connection: + description: + - A site's configuration connection. + type: dict + suboptions: + enable_replication: + description: + - Replication allows you to manage several monitoring sites + - with a logically centralized setup. + - Remote sites receive their configuration + - from the central sites. + type: bool + default: false + url_of_remote_site: + description: + - URL of the remote site including C(/check_mk/). + - This URL can be the same as the URL prefix of the status + - connection, but with C(/check_mk/) appended. + - Here it must always be an absolute URL, though. + - Unfortunately, this field is required by the REST API, + - even if there is no configuration connection enabled. + type: str + default: http://localhost/nonexistant/check_mk/ + disable_remote_configuration: + description: + - It is a good idea to disable access to Setup completely on + - the remote site. Otherwise a user who does not now about + - the replication could make local changes that are overridden + - at the next configuration activation. + type: bool + default: true + ignore_tls_errors: + description: + - This might be needed to make the synchronization accept + - problems with SSL certificates when using an SSL secured + - connection. We encourage you to always understand TLS issues + - and fix them, though! + type: bool + default: false + direct_login_to_web_gui_allowed: + description: + - When enabled, this site is marked for synchronization every + - time a web GUI related option is changed and users are + - allowed to login to the web GUI of this site. + type: bool + default: true + user_sync: + description: + - By default the users are synchronized automatically in + - the interval configured in the connection. For example + - the LDAP connector synchronizes the users every five minutes + - by default. The interval can be changed for each connection + - individually in the connection settings. Please note that + - the synchronization is only performed on the master site + - in distributed setups by default. + type: dict + suboptions: + sync_with_ldap_connections: + description: + - Sync with ldap connections. + type: str + choices: ['ldap', 'all', 'disabled'] + default: all + ldap_connections: + description: + - A list of ldap connections to synchronize. + type: list + elements: str + replicate_event_console: + description: + - This option enables the distribution of global settings and + - rules of the Event Console to the remote site. Any change in + - the local Event Console settings will mark the site as needing + - to sync. A synchronization will automatically reload + - the Event Console of the remote site. + type: bool + default: true + replicate_extensions: + description: + - If you enable the replication of MKPs then during each + - activation of changes MKPs that are installed on your central site + - and all other files below the C($OMD_ROOT/local/) directory will be + - transferred to the remote site. All other MKPs and + - files below C($OMD_ROOT/local/) on the remote site will be removed. + type: bool + default: true + basic_settings: + description: + - A site's basic settings. + type: dict + suboptions: + alias: + description: + - The alias of the site. + type: str + customer: + description: + - The customer of the site (Managed Edition - CME only). + type: str + site_id: + description: + - The site ID. + type: str + """ diff --git a/plugins/module_utils/logger.py b/plugins/module_utils/logger.py new file mode 100644 index 000000000..96df3c70b --- /dev/null +++ b/plugins/module_utils/logger.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# -*- encoding: utf-8; py-indent-offset: 4 -*- + +# Copyright: (c) 2024, Lars Getwan +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +class Logger: + def __init__(self): + self.output = [] + self.loglevel = 0 + + def set_loglevel(self, loglevel): + self.loglevel = loglevel + + def warn(self, msg): + self.output.append("WARN: %s" % msg) + + def info(self, msg): + if self.loglevel >= 1: + self.output.append("INFO: %s" % msg) + + def debug(self, msg): + if self.loglevel >= 2: + self.output.append("DEBUG: %s" % msg) + + def trace(self, msg): + if self.loglevel >= 3: + self.output.append("TRACE: %s" % msg) + + def get_log(self): + return "\n".join(self.output) diff --git a/plugins/module_utils/site.py b/plugins/module_utils/site.py new file mode 100644 index 000000000..868b2c270 --- /dev/null +++ b/plugins/module_utils/site.py @@ -0,0 +1,384 @@ +#!/usr/bin/env python +# -*- encoding: utf-8; py-indent-offset: 4 -*- + +# Copyright: (c) 2024, Lars Getwan +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +class TargetAPI: + GET = "get" + CREATE = "create" + LOGIN = "login" + LOGOUT = "logout" + UPDATE = "update" + DELETE = "delete" + + +class SiteHTTPCodes: + # http_code: (changed, failed, "Message") + get = { + 200: (False, False, "Site connection found, nothing changed"), + 404: (False, False, "Site connection not found"), + } + + create = {200: (True, False, "Site connection created")} + update = {200: (True, False, "Site connection modified")} + delete = {204: (True, False, "Site connection deleted")} + login = {204: (True, False, "Logged in to site")} + logout = {204: (True, False, "Logged out from site")} + + +class SiteEndpoints: + default = "/objects/site_connection" + create = "/domain-types/site_connection/collections/all" + + +class SiteConnection: + """Represents a particular site connection""" + + def __init__( + self, + authentication=None, + site_config=None, + state="absent", + site_id=None, + ): + self.site_id = site_id + self.state = state + self.site_config = site_config + self.authentication = authentication + + @classmethod + def from_module_params(cls, params): + site_connection = params.get("site_connection") + state = params.get("state") + site_id = params.get("site_id") + if site_connection: + authentication = site_connection.get("authentication") + site_config = site_connection.get("site_config") + else: + authentication = None + site_config = None + + return cls( + site_config=site_config, + authentication=authentication, + state=state, + site_id=site_id, + ) + + @classmethod + def from_api(cls, api_data): + + if not api_data: + return None + + return cls( + site_config=api_data.content.get("extensions"), + site_id=api_data.content.get("id"), + state="present", + ) + + def equals(self, site_connection): + return self.site_config == site_connection.site_config + + def _diff(self, d, u): + differences = [] + for k, v in u.items(): + if isinstance(v, dict): + differences += self._diff(d.get(k, {}), v) + else: + if d.get(k) != v: + differences += [k] + return differences + + def diff(self, site_connection): + return self._diff(self.site_config, site_connection.site_config) + + def logged_in(self): + if self.site_config and self.site_config.get("secret"): + return True + + def _update(self, d, u): + for k, v in u.items(): + if isinstance(v, dict): + d[k] = self._update(d.get(k, {}), v) + else: + d[k] = v + return d + + def merge_with(self, site_connection): + self._update(self.site_config, site_connection.site_config) + + def get_api_data(self, target_api): + + t = TargetAPI() + if target_api in [t.CREATE, t.UPDATE]: + return {"site_config": self.site_config} + + if target_api in [t.LOGIN]: + return self.authentication + + +# Define available arguments/parameters a user can pass to the module +module_args = dict( + server_url=dict(type="str", required=True), + site=dict(type="str", required=True), + validate_certs=dict(type="bool", required=False, default=True), + automation_user=dict(type="str", required=True), + automation_secret=dict(type="str", required=True, no_log=True), + state=dict( + type="str", + default="present", + choices=["present", "absent", "login", "logout"], + ), + site_id=dict(type="str", required=True), + site_connection=dict( + type="dict", + mutually_exclusive=[ + ("authentication", "site_config"), + ], + options=dict( + authentication=dict( + type="dict", + options=dict( + username=dict(type="str"), + password=dict(type="str", no_log=True), + ), + ), + site_config=dict( + type="dict", + options=dict( + status_connection=dict( + type="dict", + apply_defaults=True, + options=dict( + connection=dict( + type="dict", + apply_defaults=False, + options=dict( + socket_type=dict( + type="str", + choices=["tcp", "tcp6", "unix", "local"], + ), + port=dict( + type="int", + ), + encrypted=dict( + type="bool", + ), + verify=dict( + type="bool", + ), + host=dict( + type="str", + ), + path=dict( + type="str", + ), + ), + required_if=[ + ( + "socket_type", + "tcp", + ("port", "encrypted", "host"), + ), + ( + "socket_type", + "tcp6", + ( + "port", + "encrypted", + "host", + ), + ), + ("socket_type", "unix", ("path",)), + ], + mutually_exclusive=[ + ("host", "path"), + ("port", "path"), + ("encrypted", "path"), + ("verify", "path"), + ], + ), + proxy=dict( + type="dict", + apply_defaults=False, + options=dict( + use_livestatus_daemon=dict( + type="str", + choices=["with_proxy", "direct"], + ), + global_settings=dict( + type="bool", + ), + tcp=dict( + type="dict", + options=dict( + port=dict( + type="int", + ), + only_from=dict( + type="list", + elements="str", + ), + tls=dict( + type="bool", + default=False, + ), + ), + ), + params=dict( + type="dict", + apply_defaults=False, + options=dict( + channels=dict( + type="int", + default=5, + ), + heartbeat=dict( + type="dict", + options=dict( + interval=dict( + type="int", + default=5, + ), + timeout=dict( + type="int", + default=2, + ), + ), + ), + channel_timeout=dict( + type="int", + default=3, + ), + query_timeout=dict( + type="int", + default=120, + ), + connect_retry=dict( + type="int", + default=4, + ), + cache=dict( + type="bool", + default=True, + ), + ), + ), + ), + ), + connect_timeout=dict( + type="int", + default=2, + ), + persistent_connection=dict( + type="bool", + default=False, + ), + url_prefix=dict( + type="str", + ), + status_host=dict( + type="dict", + apply_defaults=True, + options=dict( + status_host_set=dict( + type="str", + default="disabled", + choices=["enabled", "disabled"], + ), + site=dict( + type="str", + ), + host=dict( + type="str", + ), + ), + ), + disable_in_status_gui=dict( + type="bool", + default="False", + ), + ), + ), + configuration_connection=dict( + type="dict", + apply_defaults=True, + options=dict( + enable_replication=dict( + type="bool", + default=False, + ), + url_of_remote_site=dict( + type="str", + default="http://localhost/nonexistant/check_mk/", + ), + disable_remote_configuration=dict( + type="bool", + default=True, + ), + ignore_tls_errors=dict( + type="bool", + default=False, + ), + direct_login_to_web_gui_allowed=dict( + type="bool", + default=True, + ), + user_sync=dict( + type="dict", + apply_defaults=True, + required_if=[ + ( + "sync_with_ldap_connections", + "ldap", + ("ldap_connections",), + ) + ], + options=dict( + sync_with_ldap_connections=dict( + type="str", + choices=["ldap", "all", "disabled"], + default="all", + ), + ldap_connections=dict( + type="list", + elements="str", + ), + ), + ), + replicate_event_console=dict( + type="bool", + default=True, + ), + replicate_extensions=dict( + default=True, + type="bool", + ), + ), + ), + basic_settings=dict( + type="dict", + options=dict( + alias=dict( + type="str", + ), + customer=dict( + type="str", + ), + site_id=dict( + type="str", + ), + ), + ), + ), + ), + ), + ), +) diff --git a/plugins/module_utils/utils.py b/plugins/module_utils/utils.py index 5412e3df8..47f846329 100644 --- a/plugins/module_utils/utils.py +++ b/plugins/module_utils/utils.py @@ -10,6 +10,8 @@ __metaclass__ = type +from ansible_collections.checkmk.general.plugins.module_utils.types import RESULT + def result_as_dict(result): return { @@ -19,6 +21,61 @@ def result_as_dict(result): } +def merge_results(results): + """Merges two or more results. Call like this: + over_all_result = merge_results({"created": create_result, "moved": move_result})""" + + return RESULT( + http_code=list(results.values())[-1].http_code, + msg=", ".join( + ["%s (%d)" % (results[k].msg, results[k].http_code) for k in results.keys()] + ), + content=list(results.values())[-1].content, + etag=list(results.values())[-1].etag, + failed=any(r.failed for r in list(results.values())), + changed=any(r.changed for r in list(results.values())), + ) + + +def remove_null_value_keys(params): + """Takes the module.params and removes all parameters that are set to 'null'. + This unsually removes all parameters that are neither explicitly set + nor provided in the ansible task""" + + for k in list(params.keys()): + if isinstance(params[k], dict): + remove_null_value_keys(params[k]) + elif params[k] is None: + del params[k] + + +def exit_module( + module, + result=None, + http_code=0, + msg="", + content="{}", + etag="", + failed=False, + changed=False, + logger=None, +): + if not result: + result = RESULT( + http_code=http_code, + msg=msg, + content=content, + etag=etag, + failed=failed, + changed=changed, + ) + + result_as_dict = result._asdict() + if logger: + result_as_dict["debug"] = logger.get_log() + module.exit_json(**result_as_dict) + + GENERIC_HTTP_CODES = { 200: (True, False, "OK: The operation was done successfully"), 204: (True, False, "Operation done successfully. No further output."), diff --git a/plugins/modules/site.py b/plugins/modules/site.py new file mode 100644 index 000000000..ee227fa0c --- /dev/null +++ b/plugins/modules/site.py @@ -0,0 +1,335 @@ +#!/usr/bin/python +# -*- encoding: utf-8; py-indent-offset: 4 -*- + +# Copyright: (c) 2024, Lars Getwan +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = r""" +--- +module: site + +short_description: Manage distributed monitoring in Checkmk. + +# If this is part of a collection, you need to use semantic versioning, +# i.e. the version is of the form "2.5.0" and not "2.4". +version_added: "5.3.0" + +description: + - Manage distributed monitoring within Checkmk. + +extends_documentation_fragment: + - checkmk.general.common + - checkmk.general.site_options + +author: + - Lars Getwan (@lgetwan) +""" + +EXAMPLES = r""" +- name: "Add a remote site with configuration replication." + checkmk.general.site: + server_url: "http://myserver/" + site: "mysite" + automation_user: "myuser" + automation_secret: "mysecret" + site_id: "myremotesite" + site_connection: + site_config: + status_connection: + connection: + socket_type: tcp + port: 6557 + encrypted: true + host: localhost + verify: true + proxy: + use_livestatus_daemon: "direct" + connect_timeout: 2 + status_host: + status_host_set: "disabled" + url_prefix: "/myremotesite/" + configuration_connection: + enable_replication: true + url_of_remote_site: "http://localhost/myremotesite/check_mk/" + basic_settings: + site_id: "myremotesite" + customer: "provider" + alias: "My Remote Site" + state: "present" + +- name: "Log into a remote site." + checkmk.general.site: + server_url: "http://myserver/" + site: "mysite" + automation_user: "myuser" + automation_secret: "mysecret" + site_id: "myremotesite" + site_connection: + authentication: + username: "myremote_admin" + password: "highly_secret" + state: "login" + +- name: "Log out from a remote site." + checkmk.general.site: + server_url: "http://myserver/" + site: "mysite" + automation_user: "myuser" + automation_secret: "mysecret" + site_id: "myremotesite" + state: "logout" + +- name: "Delete a remote site." + checkmk.general.site: + server_url: "http://myserver/" + site: "mysite" + automation_user: "myuser" + automation_secret: "mysecret" + site_id: "myremotesite" + state: "absent" +""" + +RETURN = r""" +message: + description: The output message that the module generates. Contains the API response details in case of an error. + type: str + returned: always + sample: 'Site connection created.' +""" + +import json + +# https://docs.ansible.com/ansible/latest/dev_guide/testing/sanity/import.html +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.checkmk.general.plugins.module_utils.api import CheckmkAPI +from ansible_collections.checkmk.general.plugins.module_utils.logger import Logger +from ansible_collections.checkmk.general.plugins.module_utils.site import ( + SiteConnection, + SiteEndpoints, + SiteHTTPCodes, + TargetAPI, + module_args, +) +from ansible_collections.checkmk.general.plugins.module_utils.types import RESULT +from ansible_collections.checkmk.general.plugins.module_utils.utils import ( + exit_module, + remove_null_value_keys, +) +from ansible_collections.checkmk.general.plugins.module_utils.version import ( + CheckmkVersion, +) + + +class SiteAPI(CheckmkAPI): + def __init__(self, module): + super().__init__(module) + + self._verify_compatibility() + + self.module = module + self.params = self.module.params + self.state = self.params.get("state") + + def _get_endpoint(self, target_api, site_id=""): + if target_api == TargetAPI.CREATE: + return SiteEndpoints.create + + if target_api in [TargetAPI.GET, TargetAPI.UPDATE]: + return "%s/%s" % (SiteEndpoints.default, site_id) + + if target_api in [TargetAPI.LOGIN]: + return "%s/%s/actions/login/invoke" % ( + SiteEndpoints.default, + site_id, + ) + + if target_api in [TargetAPI.LOGOUT]: + return "%s/%s/actions/logout/invoke" % ( + SiteEndpoints.default, + site_id, + ) + + if target_api in [TargetAPI.DELETE]: + return "%s/%s/actions/delete/invoke" % ( + SiteEndpoints.default, + site_id, + ) + + def get(self, site_id): + logger.debug( + "get endpoint: %s" % self._get_endpoint(TargetAPI.GET, site_id=site_id) + ) + result = self._fetch( + code_mapping=SiteHTTPCodes.get, + endpoint=self._get_endpoint(TargetAPI.GET, site_id=site_id), + ) + + logger.debug("get data: %s" % str(result)) + + if result.http_code == 404: + return None + + result = result._replace(content=json.loads(result.content)) + return result + + def create(self, site_connection): + logger.debug("create endpoint: %s" % self._get_endpoint(TargetAPI.CREATE)) + logger.debug("create data: %s" % site_connection.get_api_data(TargetAPI.CREATE)) + return self._fetch( + code_mapping=SiteHTTPCodes.create, + endpoint=self._get_endpoint(TargetAPI.CREATE), + data=site_connection.get_api_data(TargetAPI.CREATE), + method="POST", + ) + + def update(self, site_connection, desired_site_connection): + vorher = site_connection.site_config + site_connection.merge_with(desired_site_connection) + nachher = site_connection.site_config + logger.debug("update endpoint: %s" % self._get_endpoint(TargetAPI.UPDATE)) + logger.debug("update data: %s" % site_connection.get_api_data(TargetAPI.UPDATE)) + return self._fetch( + code_mapping=SiteHTTPCodes.update, + endpoint=self._get_endpoint( + TargetAPI.UPDATE, site_id=site_connection.site_id + ), + data=site_connection.get_api_data(TargetAPI.UPDATE), + method="PUT", + ) + + def login(self, site_connection): + logger.debug( + "login endpoint: %s" + % self._get_endpoint(TargetAPI.LOGIN, site_id=site_connection.site_id) + ) + logger.debug("login data: %s" % site_connection.get_api_data(TargetAPI.LOGIN)) + return self._fetch( + code_mapping=SiteHTTPCodes.login, + endpoint=self._get_endpoint( + TargetAPI.LOGIN, site_id=site_connection.site_id + ), + data=site_connection.get_api_data(TargetAPI.LOGIN), + method="POST", + ) + + def logout(self, site_connection): + logger.debug( + "logout endpoint: %s" + % self._get_endpoint(TargetAPI.LOGOUT, site_id=site_connection.site_id) + ) + logger.debug("logout data: %s" % site_connection.get_api_data(TargetAPI.LOGOUT)) + return self._fetch( + code_mapping=SiteHTTPCodes.logout, + endpoint=self._get_endpoint( + TargetAPI.LOGOUT, site_id=site_connection.site_id + ), + method="POST", + ) + + def delete(self, site_connection): + logger.debug( + "delete endpoint: %s" + % self._get_endpoint(TargetAPI.DELETE, site_id=site_connection.site_id) + ) + logger.debug("delete data: %s" % site_connection.get_api_data(TargetAPI.DELETE)) + return self._fetch( + code_mapping=SiteHTTPCodes.delete, + endpoint=self._get_endpoint( + TargetAPI.DELETE, site_id=site_connection.site_id + ), + method="POST", + ) + + def _verify_compatibility(self): + if self.getversion() <= CheckmkVersion("2.2.0"): + exit_module( + msg="Site management is only available for Checkmk versions starting with 2.2.0.", + failed=True, + ) + + +logger = Logger() + + +def run_module(): + # define available arguments/parameters a user can pass to the module + + module = AnsibleModule(argument_spec=module_args, supports_check_mode=True) + logger.set_loglevel(module._verbosity) + remove_null_value_keys(module.params) + site_id = module.params.get("site_id") + + site_api = SiteAPI(module) + desired_site_connection = SiteConnection.from_module_params(module.params) + existing_site_connection = SiteConnection.from_api(site_api.get(site_id)) + + if desired_site_connection.state == "present": + if existing_site_connection and existing_site_connection.state == "present": + differences = existing_site_connection.diff(desired_site_connection) + if differences: + result = site_api.update( + existing_site_connection, desired_site_connection + ) + + result = result._replace( + msg="%s\nUpdated: %s" % (result.msg, ", ".join(differences)) + ) + else: + result = RESULT( + http_code=0, + msg="Site connection already exists with the desired parameters.", + content="", + etag="", + failed=False, + changed=False, + ) + + else: + result = site_api.create(desired_site_connection) + + exit_module(module, result=result) + + elif desired_site_connection.state == "absent": + if existing_site_connection and existing_site_connection.state == "present": + result = site_api.delete(existing_site_connection) + exit_module(module, result=result) + else: + exit_module(module, msg="Site connection already absent.") + + elif desired_site_connection.state == "login": + if not existing_site_connection: + exit_module(module, msg="Site does not exist", failed=True) + + if not existing_site_connection.logged_in(): + result = site_api.login(desired_site_connection) + exit_module(module, result=result) + else: + exit_module(module, msg="Already logged in to site.") + + elif desired_site_connection.state == "logout": + if not existing_site_connection: + exit_module(module, msg="Site does not exist", failed=True) + + if existing_site_connection.logged_in(): + result = site_api.logout(desired_site_connection) + exit_module(module, result=result) + else: + exit_module(module, msg="Already logged out from site.") + + else: + exit_module( + module, + msg="Unexpected target state %s" % desired_site_connection.state, + failed=True, + ) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/site/tasks/main.yml b/tests/integration/targets/site/tasks/main.yml new file mode 100644 index 000000000..45a262645 --- /dev/null +++ b/tests/integration/targets/site/tasks/main.yml @@ -0,0 +1,14 @@ +--- +- name: "Include Global Variables." + ansible.builtin.include_vars: /root/ansible_collections/checkmk/general/tests/integration/files/includes/vars/global.yml + +- name: "Run Preparations." + ansible.builtin.include_tasks: /root/ansible_collections/checkmk/general/tests/integration/files/includes/tasks/prep.yml + +- name: "Testing." + ansible.builtin.include_tasks: test.yml + loop: "{{ test_sites }}" + loop_control: + loop_var: outer_item + label: "{{ outer_item.site }}" + when: (download_pass is defined and download_pass | length) or outer_item.edition == "cre" diff --git a/tests/integration/targets/site/tasks/test.yml b/tests/integration/targets/site/tasks/test.yml new file mode 100644 index 000000000..49a0070d1 --- /dev/null +++ b/tests/integration/targets/site/tasks/test.yml @@ -0,0 +1,213 @@ +--- +- name: "{{ outer_item.version }} - {{ outer_item.edition | upper }} - Set customer when needed." + ansible.builtin.set_fact: + customer: "provider" + when: (outer_item.edition == "cme") or (outer_item.edition == "cce") + +- name: "{{ outer_item.version }} - {{ outer_item.edition | upper }} - Unset customer when needed." + ansible.builtin.set_fact: + customer: null + when: not ((outer_item.edition == "cme") or (outer_item.edition == "cce")) + +- name: "{{ outer_item.version }} - {{ outer_item.edition | upper }} - Create remote sites." + ansible.builtin.command: "omd -V {{ outer_item.version }}.{{ outer_item.edition }} create --no-tmpfs --admin-password {{ checkmk_var_automation_secret }} {{ item.site_id }}" + args: + creates: "/omd/sites/{{ item.site_id }}" + when: (download_pass is defined and download_pass | length) or outer_item.edition == "cre" + loop: "{{ outer_item.remote_sites }}" + loop_control: + label: "{{ item.site_id }}" + +- name: "{{ outer_item.version }} - {{ outer_item.edition | upper }} - Configure Sites." # noqa no-changed-when + become: true + ansible.builtin.shell: | + set -o pipefail + omd config {{ item.site_id }} set LIVEPROXYD on + omd config {{ item.site_id }} set LIVESTATUS_TCP_PORT {{ item.site_config.status_connection.connection.port }} + omd config {{ item.site_id }} set LIVESTATUS_TCP_TLS off + args: + executable: /bin/bash + when: (download_pass is defined and download_pass | length) or outer_item.edition == "cre" + loop: "{{ outer_item.remote_sites }}" + loop_control: + label: "{{ item.site_id }}" + +- name: "{{ outer_item.version }} - {{ outer_item.edition | upper }} - Start Sites." + ansible.builtin.shell: "omd status -b {{ item.site_id }} || omd start {{ item.site_id }}" + register: site_status + changed_when: site_status.rc == "0" + when: (download_pass is defined and download_pass | length) or outer_item.edition == "cre" + loop: "{{ outer_item.remote_sites }}" + loop_control: + label: "{{ item.site_id }}" + +- name: "{{ outer_item.version }} - {{ outer_item.edition | upper }} - Wait for site to be ready." + ansible.builtin.pause: + seconds: 5 + when: | + ((download_pass is defined and download_pass | length) or outer_item.edition == 'cre') + and (outer_item.stdout_lines is defined and 'OVERALL 1' in outer_item.stdout_lines) + +- name: "{{ outer_item.version }} - {{ outer_item.edition | upper }} - Create site connection." + site: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + automation_user: "{{ checkmk_var_automation_user }}" + automation_secret: "{{ checkmk_var_automation_secret }}" + site_id: "{{ item.site_id }}" + site_connection: + site_config: "{{ item.site_config }}" + state: "present" + delegate_to: localhost + loop: "{{ outer_item.remote_sites }}" + loop_control: + label: "{{ item.site_id }}" + run_once: true # noqa run-once[task] + +- name: "{{ outer_item.version }} - {{ outer_item.edition | upper }} - Create site connection again." + site: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + automation_user: "{{ checkmk_var_automation_user }}" + automation_secret: "{{ checkmk_var_automation_secret }}" + site_id: "{{ item.site_id }}" + site_connection: + site_config: "{{ item.site_config }}" + state: "present" + delegate_to: localhost + loop: "{{ outer_item.remote_sites }}" + loop_control: + label: "{{ item.site_id }}" + run_once: true # noqa run-once[task] + +- name: "{{ outer_item.version }} - {{ outer_item.edition | upper }} - Log in to remote site." + site: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + automation_user: "{{ checkmk_var_automation_user }}" + automation_secret: "{{ checkmk_var_automation_secret }}" + site_id: "{{ item.site_id }}" + site_connection: + authentication: "{{ item.authentication }}" + state: "login" + delegate_to: localhost + loop: "{{ outer_item.remote_sites }}" + loop_control: + label: "{{ item.site_id }}" + run_once: true # noqa run-once[task] + +- name: "{{ outer_item.version }} - {{ outer_item.edition | upper }} - Log in to remote site again." + site: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + automation_user: "{{ checkmk_var_automation_user }}" + automation_secret: "{{ checkmk_var_automation_secret }}" + site_id: "{{ item.site_id }}" + site_connection: + authentication: "{{ item.authentication }}" + state: "login" + delegate_to: localhost + loop: "{{ outer_item.remote_sites }}" + loop_control: + label: "{{ item.site_id }}" + run_once: true # noqa run-once[task] + register: result + failed_when: result.changed | bool + +- name: "{{ outer_item.version }} - {{ outer_item.edition | upper }} - Update remote site." + site: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + automation_user: "{{ checkmk_var_automation_user }}" + automation_secret: "{{ checkmk_var_automation_secret }}" + site_id: "{{ item.site_id }}" + site_connection: + site_config: + basic_settings: + alias: "{{ item.site_id }} with new alias" + state: "present" + delegate_to: localhost + loop: "{{ outer_item.remote_sites }}" + loop_control: + label: "{{ item.site_id }}" + run_once: true # noqa run-once[task] + +- name: "{{ outer_item.version }} - {{ outer_item.edition | upper }} - Update remote site again. " + site: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + automation_user: "{{ checkmk_var_automation_user }}" + automation_secret: "{{ checkmk_var_automation_secret }}" + site_id: "{{ item.site_id }}" + site_connection: + site_config: + basic_settings: + alias: "{{ item.site_id }} with new alias" + state: "present" + delegate_to: localhost + loop: "{{ outer_item.remote_sites }}" + loop_control: + label: "{{ item.site_id }}" + run_once: true # noqa run-once[task] + register: result + failed_when: result.changed | bool + +- name: "{{ outer_item.version }} - {{ outer_item.edition | upper }} - Log out from remote site." + site: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + automation_user: "{{ checkmk_var_automation_user }}" + automation_secret: "{{ checkmk_var_automation_secret }}" + site_id: "{{ item.site_id }}" + state: "logout" + delegate_to: localhost + loop: "{{ outer_item.remote_sites }}" + loop_control: + label: "{{ item.site_id }}" + run_once: true # noqa run-once[task] + +- name: "{{ outer_item.version }} - {{ outer_item.edition | upper }} - Log out from remote site again." + site: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + automation_user: "{{ checkmk_var_automation_user }}" + automation_secret: "{{ checkmk_var_automation_secret }}" + site_id: "{{ item.site_id }}" + state: "logout" + delegate_to: localhost + loop: "{{ outer_item.remote_sites }}" + loop_control: + label: "{{ item.site_id }}" + run_once: true # noqa run-once[task] + register: result + failed_when: result.changed | bool + +- name: "{{ outer_item.version }} - {{ outer_item.edition | upper }} - Delete remote site." + site: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + automation_user: "{{ checkmk_var_automation_user }}" + automation_secret: "{{ checkmk_var_automation_secret }}" + site_id: "{{ item.site_id }}" + state: "absent" + delegate_to: localhost + loop: "{{ outer_item.remote_sites }}" + loop_control: + label: "{{ item.site_id }}" + run_once: true # noqa run-once[task] + +- name: "{{ outer_item.version }} - {{ outer_item.edition | upper }} - Delete remote site again." + site: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + automation_user: "{{ checkmk_var_automation_user }}" + automation_secret: "{{ checkmk_var_automation_secret }}" + site_id: "{{ item.site_id }}" + state: "absent" + delegate_to: localhost + loop: "{{ outer_item.remote_sites }}" + loop_control: + label: "{{ item.site_id }}" + run_once: true # noqa run-once[task] + register: result + failed_when: result.changed | bool diff --git a/tests/integration/targets/site/vars/main.yml b/tests/integration/targets/site/vars/main.yml new file mode 100644 index 000000000..eac9a4ff4 --- /dev/null +++ b/tests/integration/targets/site/vars/main.yml @@ -0,0 +1,207 @@ +--- +test_sites: + - version: "2.3.0p12" + edition: "cme" + site: "stable_cme" + remote_sites: + - site_id: "stable_cme_r1" + site_config: + status_connection: + connection: + socket_type: "tcp" + port: 6561 + encrypted: true + host: "localhost" + verify: false + proxy: + use_livestatus_daemon: "direct" + connect_timeout: 2 + status_host: + status_host_set: "disabled" + url_prefix: "/stable_cme_r1/" + configuration_connection: + enable_replication: true + url_of_remote_site: "http://localhost/stable_cme_r1/check_mk/" + basic_settings: + site_id: "stable_cme_r1" + customer: "provider" + alias: "stable_cme remote site 1" + authentication: + username: "cmkadmin" + password: "{{ checkmk_var_automation_secret }}" + - site_id: "stable_cme_r2" + site_config: + status_connection: + connection: + socket_type: "tcp" + port: 6562 + encrypted: true + host: "localhost" + verify: false + proxy: + use_livestatus_daemon: "with_proxy" + global_settings: true + connect_timeout: 2 + status_host: + status_host_set: "disabled" + url_prefix: "/stable_cme_r2/" + configuration_connection: + enable_replication: true + url_of_remote_site: "http://localhost/stable_cme_r2/check_mk/" + basic_settings: + site_id: "stable_cme_r2" + customer: "provider" + alias: "stable_cme remote site 2" + authentication: + username: "cmkadmin" + password: "{{ checkmk_var_automation_secret }}" + - site_id: "stable_cme_r3" + site_config: + status_connection: + connection: + socket_type: "tcp" + port: 6563 + encrypted: true + host: "localhost" + verify: false + proxy: + use_livestatus_daemon: "with_proxy" + global_settings: false + tcp: + port: 6663 + only_from: [] + tls: true + params: + channels: 6 + heartbeat: + interval: 10 + timeout: 4 + channel_timeout: 6 + query_timeout: 240 + connect_retry: 5 + cache: true + connect_timeout: 2 + status_host: + status_host_set: "disabled" + url_prefix: "/stable_cme_r3/" + configuration_connection: + enable_replication: true + url_of_remote_site: "http://localhost/stable_cme_r3/check_mk/" + disable_remote_configuration: true + ignore_tls_errors: false + direct_login_to_web_gui_allowed: false + replicate_event_console: true + replicate_extensions: false + basic_settings: + site_id: "stable_cme_r3" + customer: "provider" + alias: "stable_cme remote site 3" + authentication: + username: "cmkadmin" + password: "{{ checkmk_var_automation_secret }}" + - version: "2.3.0p12" + edition: "cee" + site: "stable_cee" + remote_sites: + - site_id: "stable_cee_r" + site_config: + status_connection: + connection: + socket_type: "tcp" + port: 6559 + encrypted: true + host: "localhost" + verify: "true" + proxy: + use_livestatus_daemon: "direct" + connect_timeout: 2 + status_host: + status_host_set: "disabled" + url_prefix: "/stable_cee_r/" + configuration_connection: + enable_replication: true + url_of_remote_site: "http://localhost/stable_cee_r/check_mk/" + basic_settings: + site_id: "stable_cee_r" + alias: "stable_cee remote site" + authentication: + username: "cmkadmin" + password: "{{ checkmk_var_automation_secret }}" + - version: "2.3.0p12" + edition: "cre" + site: "stable_cre" + remote_sites: + - site_id: "stable_cre_r1" + site_config: + status_connection: + connection: + socket_type: "tcp" + port: 6564 + encrypted: true + host: "localhost" + verify: false + proxy: + use_livestatus_daemon: "with_proxy" + global_settings: false + tcp: + port: 6664 + only_from: [] + tls: true + params: + channels: 6 + heartbeat: + interval: 10 + timeout: 4 + channel_timeout: 6 + query_timeout: 240 + connect_retry: 5 + cache: true + connect_timeout: 2 + status_host: + status_host_set: "disabled" + url_prefix: "/stable_cre_r1/" + configuration_connection: + enable_replication: true + url_of_remote_site: "http://localhost/stable_cre_r1/check_mk/" + disable_remote_configuration: true + ignore_tls_errors: false + direct_login_to_web_gui_allowed: false + replicate_event_console: true + replicate_extensions: false + basic_settings: + site_id: "stable_cre_r1" + alias: "stable_cre remote site 1" + authentication: + username: "cmkadmin" + password: "{{ checkmk_var_automation_secret }}" + - version: "2.2.0p32" + edition: "cre" + site: "old_cre" + remote_sites: + - site_id: "old_cre_r" + site_config: + status_connection: + connection: + socket_type: "tcp" + port: 6559 + encrypted: true + host: "localhost" + verify: "true" + proxy: + use_livestatus_daemon: "direct" + connect_timeout: 2 + status_host: + status_host_set: "disabled" + url_prefix: "/old_cre_r/" + configuration_connection: + enable_replication: true + url_of_remote_site: "http://localhost/old_cre_r/check_mk/" + basic_settings: + site_id: "old_cre_r" + alias: "old_cre remote site" + authentication: + username: "cmkadmin" + password: "{{ checkmk_var_automation_secret }}" + # - version: "2.1.0p46" + # edition: "cre" + # site: "ancient_cre"