Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/labels-issues.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ inventory:
module:activation:
- 'Component Name: activation'

module:aux_tag:
- 'Component Name: aux_tag'

module:bakery:
- 'Component Name: bakery'

Expand Down
5 changes: 5 additions & 0 deletions .github/labels-prs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ module:activation:
- changed-files:
- any-glob-to-any-file: 'plugins/modules/activation.py'

module:aux_tag:
- any:
- changed-files:
- any-glob-to-any-file: 'plugins/modules/aux_tag.py'

module:bakery:
- any:
- changed-files:
Expand Down
116 changes: 116 additions & 0 deletions .github/workflows/ans-int-test-aux_tag.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# 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: aux_tag

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

name: Ansible Integration Tests for Aux Tag Module
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * 0'
pull_request:
branches:
- main
- devel
paths:
- 'plugins/modules/aux_tag.py'
push:
paths:
- '.github/workflows/ans-int-test-aux_tag.yaml'
- 'plugins/modules/aux_tag.py'
- 'tests/integration/files/includes/'
- 'tests/integration/targets/aux_tag/'

jobs:

integration:
runs-on: ubuntu-24.04
name: Ⓐ${{ matrix.ansible }}+py${{ matrix.python }}
strategy:
fail-fast: false
matrix:
ansible:
- stable-2.17
- stable-2.18
- stable-2.19
- devel
python:
- '3.11'
- '3.12'
exclude:
# Exclude unsupported sets.
- ansible: devel
python: '3.11'

services:
ancient_cre:
image: checkmk/check-mk-raw:2.2.0p45
ports:
- 5022:5000
env:
CMK_SITE_ID: "ancient_cre"
CMK_PASSWORD: "Sup3rSec4et!"
old_cre:
image: checkmk/check-mk-raw:2.3.0p36
ports:
- 5023:5000
env:
CMK_SITE_ID: "old_cre"
CMK_PASSWORD: "Sup3rSec4et!"
old_cme:
image: checkmk/check-mk-managed:2.3.0p36
ports:
- 5323:5000
env:
CMK_SITE_ID: "old_cme"
CMK_PASSWORD: "Sup3rSec4et!"
stable_cre:
image: checkmk/check-mk-raw:2.4.0p10
ports:
- 5024:5000
env:
CMK_SITE_ID: "stable_cre"
CMK_PASSWORD: "Sup3rSec4et!"
stable_cme:
image: checkmk/check-mk-managed:2.4.0p10
ports:
- 5324:5000
env:
CMK_SITE_ID: "stable_cme"
CMK_PASSWORD: "Sup3rSec4et!"

steps:
- name: Check out code
uses: actions/checkout@v5
with:
path: ansible_collections/${{env.NAMESPACE}}/${{env.COLLECTION_NAME}}

- name: "Install uv and set the python version."
uses: astral-sh/setup-uv@v6
with:
python-version: ${{ matrix.python }}
enable-cache: true
working-directory: ./ansible_collections/${{env.NAMESPACE}}/${{env.COLLECTION_NAME}}/

- name: "Setup uv venv."
run: uv venv
working-directory: ./ansible_collections/${{env.NAMESPACE}}/${{env.COLLECTION_NAME}}

- name: Install ansible-base (${{ matrix.ansible }})
run: uv pip install https://github.com/ansible/ansible/archive/${{ matrix.ansible }}.tar.gz
working-directory: ./ansible_collections/${{env.NAMESPACE}}/${{env.COLLECTION_NAME}}

- name: Run integration test
run: uv run ansible-test integration ${{env.MODULE_NAME}} -v --color --continue-on-error --diff --python ${{ matrix.python }}
working-directory: ./ansible_collections/${{env.NAMESPACE}}/${{env.COLLECTION_NAME}}
3 changes: 3 additions & 0 deletions changelogs/fragments/aux_tag_module.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
minor_changes:
- aux_tag module - Add new module to manage auxiliary tags in Checkmk.
Auxiliary tags can be created, updated, and deleted independently, making it easier to manage tag hierarchies and dependencies.
244 changes: 244 additions & 0 deletions plugins/modules/aux_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
#!/usr/bin/python
# -*- encoding: utf-8; py-indent-offset: 4 -*-

# Copyright: (c) 2025, Nicolas Brainez <[email protected]>
# 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: aux_tag

short_description: Manage auxiliary tags in Checkmk.

version_added: "6.3.0"

description:
- Manage auxiliary tags in Checkmk.

extends_documentation_fragment: [checkmk.general.common]

options:
name:
description: The ID of the auxiliary tag.
required: true
type: str
aliases: ["id"]

title:
description: The title of the auxiliary tag.
required: false
type: str

topic:
description: The topic or category of the auxiliary tag.
required: false
type: str

help:
description: Help text describing the auxiliary tag.
required: false
type: str

state:
description: The desired state.
required: true
choices: ["present", "absent"]
type: str

author:
- Nicolas Brainez (@nicoske)
"""

EXAMPLES = r"""
# Create an auxiliary tag
- name: "Create auxiliary tag for HTTPS"
checkmk.general.aux_tag:
server_url: "http://myserver/"
site: "mysite"
automation_user: "myuser"
automation_secret: "mysecret"
name: https
title: Web Server HTTPS
topic: Services
help: "Host provides HTTPS services"
state: "present"

# Update an auxiliary tag
- name: "Update auxiliary tag"
checkmk.general.aux_tag:
server_url: "http://myserver/"
site: "mysite"
automation_user: "myuser"
automation_secret: "mysecret"
name: https
title: Web Server HTTPS/TLS
topic: Services
state: "present"

# Delete an auxiliary tag
- name: "Delete auxiliary tag"
checkmk.general.aux_tag:
server_url: "http://myserver/"
site: "mysite"
automation_user: "myuser"
automation_secret: "mysecret"
name: https
state: "absent"
"""

RETURN = r"""
http_code:
description: The HTTP code the Checkmk API returns.
type: int
returned: always
sample: '200'
message:
description: The output message that the module generates.
type: str
returned: always
sample: 'Done.'
"""

import time

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.types import RESULT
from ansible_collections.checkmk.general.plugins.module_utils.utils import (
base_argument_spec,
result_as_dict,
)

# We count 404 not as failed, because we want to know if the aux tag exists or not.
HTTP_CODES_GET = {
# http_code: (changed, failed, "Message")
404: (False, False, "Not Found: The requested object has not been found."),
}

HTTP_CODES_DELETE = {
# http_code: (changed, failed, "Message")
404: (False, False, "Not Found: The requested object has not been found."),
}


class AuxTagCreateAPI(CheckmkAPI):
def post(self): # Create aux tag
data = {
"aux_tag_id": self.params.get("name", ""),
"title": self.params.get("title", ""),
"topic": self.params.get("topic", ""),
"help": self.params.get("help", ""),
}

# Remove all keys without value, as otherwise they would be None.
data = {key: val for key, val in data.items() if val}

return self._fetch(
endpoint="/domain-types/aux_tag/collections/all",
data=data,
method="POST",
)


class AuxTagUpdateAPI(CheckmkAPI):
def put(self): # Update aux tag
data = {
"title": self.params.get("title", ""),
"topic": self.params.get("topic", ""),
"help": self.params.get("help", ""),
}

# Remove all keys without value, as they would be emptied.
data = {key: val for key, val in data.items() if val}

return self._fetch(
endpoint="/objects/aux_tag/%s" % self.params.get("name"),
data=data,
method="PUT",
)


class AuxTagDeleteAPI(CheckmkAPI):
def delete(self): # Remove aux tag
return self._fetch(
code_mapping=HTTP_CODES_DELETE,
endpoint="/objects/aux_tag/%s" % self.params.get("name"),
method="DELETE",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fails in my 2.4.0p14 environment, as deleting is done with a POST on the endpoint /objects/aux_tag/<aux_tag_name>/actions/delete/invoke.

)


class AuxTagGetAPI(CheckmkAPI):
def get(self):
return self._fetch(
code_mapping=HTTP_CODES_GET,
endpoint="/objects/aux_tag/%s" % self.params.get("name"),
method="GET",
)


Comment on lines +164 to +181
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I don't get the idea, but why have you created four ChecckmkAPI classes? I usually created only one, which has a put, get and delete method.
That way, you could also handle getting the etag and putting it into the header directly inside the class.

def run_module():
argument_spec = base_argument_spec()
argument_spec.update(
name=dict(type="str", required=True, aliases=["id"]),
title=dict(type="str", required=False),
topic=dict(type="str", required=False),
help=dict(type="str", required=False),
state=dict(
type="str",
choices=["present", "absent"],
required=True,
),
)

module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False)

result = RESULT(
http_code=0,
msg="Nothing to be done",
content="",
etag="",
failed=False,
changed=False,
)

if module.params.get("state") == "present":
auxtagget = AuxTagGetAPI(module)
result = auxtagget.get()

if result.http_code == 200:
auxtagupdate = AuxTagUpdateAPI(module)
auxtagupdate.headers["If-Match"] = result.etag
result = auxtagupdate.put()
Comment on lines +212 to +214
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be cool if you only update the aux tag if something has changed. (-> Idempotency).


time.sleep(3)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the sleep needed to avoid race conditions?
However, I'd rather not do a sleep in the module.


elif result.http_code == 404:
auxtagcreate = AuxTagCreateAPI(module)
result = auxtagcreate.post()

time.sleep(3)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.


if module.params.get("state") == "absent":
# Only delete if the aux tag exists
auxtagget = AuxTagGetAPI(module)
result = auxtagget.get()

if result.http_code == 200:
auxtagdelete = AuxTagDeleteAPI(module)
auxtagdelete.headers["If-Match"] = result.etag
result = auxtagdelete.delete()

time.sleep(3)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.


module.exit_json(**result_as_dict(result))


def main():
run_module()


if __name__ == "__main__":
main()
Loading
Loading