Restructured libraries

This commit is contained in:
Kevin Veen-Birkenbach 2025-07-17 16:38:20 +02:00
parent 6d4b7227ce
commit 562603a8cd
No known key found for this signature in database
GPG Key ID: 44D8F11FD62F878E
35 changed files with 162 additions and 85 deletions

View File

@ -5,7 +5,7 @@ import sys
import time import time
from pathlib import Path from pathlib import Path
# Ensure project root on PYTHONPATH so utils is importable # Ensure project root on PYTHONPATH so module_utils is importable
repo_root = Path(__file__).resolve().parent.parent.parent.parent repo_root = Path(__file__).resolve().parent.parent.parent.parent
sys.path.insert(0, str(repo_root)) sys.path.insert(0, str(repo_root))
@ -13,7 +13,7 @@ sys.path.insert(0, str(repo_root))
plugin_path = repo_root / "lookup_plugins" plugin_path = repo_root / "lookup_plugins"
sys.path.insert(0, str(plugin_path)) sys.path.insert(0, str(plugin_path))
from utils.dict_renderer import DictRenderer from module_utils.dict_renderer import DictRenderer
from application_gid import LookupModule from application_gid import LookupModule
def load_yaml_file(path: Path) -> dict: def load_yaml_file(path: Path) -> dict:

View File

@ -4,9 +4,9 @@ import sys
from pathlib import Path from pathlib import Path
import yaml import yaml
from typing import Dict, Any from typing import Dict, Any
from utils.manager.inventory import InventoryManager from module_utils.manager.inventory import InventoryManager
from utils.handler.vault import VaultHandler, VaultScalar from module_utils.handler.vault import VaultHandler, VaultScalar
from utils.handler.yaml import YamlHandler from module_utils.handler.yaml import YamlHandler
from yaml.dumper import SafeDumper from yaml.dumper import SafeDumper

View File

@ -88,7 +88,7 @@ def validate_application_ids(inventory, app_ids):
""" """
Abort the script if any application IDs are invalid, with detailed reasons. Abort the script if any application IDs are invalid, with detailed reasons.
""" """
from utils.valid_deploy_id import ValidDeployId from module_utils.valid_deploy_id import ValidDeployId
validator = ValidDeployId() validator = ValidDeployId()
invalid = validator.validate(inventory, app_ids) invalid = validator.validate(inventory, app_ids)
if invalid: if invalid:

View File

@ -4,8 +4,8 @@ import sys
from pathlib import Path from pathlib import Path
import yaml import yaml
from typing import Dict, Any from typing import Dict, Any
from utils.handler.vault import VaultHandler, VaultScalar from module_utils.handler.vault import VaultHandler, VaultScalar
from utils.handler.yaml import YamlHandler from module_utils.handler.yaml import YamlHandler
from yaml.dumper import SafeDumper from yaml.dumper import SafeDumper
def ask_for_confirmation(key: str) -> bool: def ask_for_confirmation(key: str) -> bool:

27
filter_plugins/README.md Normal file
View File

@ -0,0 +1,27 @@
# Custom Filter Plugins for CyMaIS
This directory contains custom **Ansible filter plugins** used within the CyMaIS project.
## When to Use a Filter Plugin
- **Transform values:** Use filters to transform, extract, reformat, or compute values from existing variables or facts.
- **Inline data manipulation:** Filters are designed for inline use in Jinja2 expressions (in templates, tasks, vars, etc.).
- **No external lookups:** Filters only operate on data you explicitly pass to them and cannot access external files, the Ansible inventory, or runtime context.
### Examples
```jinja2
{{ role_name | get_entity_name }}
{{ my_list | unique }}
{{ user_email | regex_replace('^(.+)@.*$', '\\1') }}
````
## When *not* to Use a Filter Plugin
* If you need to **load data from an external source** (e.g., file, environment, API), use a lookup plugin instead.
* If your logic requires **access to inventory, facts, or host-level information** that is not passed as a parameter.
## Further Reading
* [Ansible Filter Plugins Documentation](https://docs.ansible.com/ansible/latest/plugins/filter.html)
* [Developing Ansible Filter Plugins](https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#developing-filter-plugins)

View File

@ -1,5 +1,3 @@
# filter_plugins/get_application_id.py
import os import os
import re import re
import yaml import yaml

View File

@ -1,4 +1,4 @@
def get_docker_compose(path_docker_compose_instances: str, application_id: str) -> dict: def get_docker_paths(path_docker_compose_instances: str, application_id: str) -> dict:
""" """
Build the docker_compose dict based on Build the docker_compose dict based on
path_docker_compose_instances and application_id. path_docker_compose_instances and application_id.
@ -23,5 +23,5 @@ def get_docker_compose(path_docker_compose_instances: str, application_id: str)
class FilterModule(object): class FilterModule(object):
def filters(self): def filters(self):
return { return {
'get_docker_compose': get_docker_compose, 'get_docker_paths': get_docker_paths,
} }

View File

@ -1,17 +0,0 @@
class FilterModule(object):
def filters(self):
return {
'get_public_id': self.get_public_id
}
def get_public_id(self, value):
"""
Extract the substring after the last hyphen in the input string.
Example:
'service-user-abc123' => 'abc123'
"""
if not isinstance(value, str):
raise ValueError("Expected a string")
if '-' not in value:
raise ValueError("No hyphen found in input string")
return value.rsplit('-', 1)[-1]

View File

@ -1,5 +1,3 @@
# filter_plugins/role_path_by_app_id.py
import os import os
import glob import glob
import yaml import yaml

View File

@ -1,4 +1,3 @@
# file: filter_plugins/safe_join.py
""" """
Ansible filter plugin that joins a base string and a tail path safely. Ansible filter plugin that joins a base string and a tail path safely.
If the base is falsy (None, empty, etc.), returns an empty string. If the base is falsy (None, empty, etc.), returns an empty string.

View File

@ -1,5 +1,3 @@
# filter_plugins/text_filters.py
from ansible.errors import AnsibleFilterError from ansible.errors import AnsibleFilterError
import re import re

40
library/README.md Normal file
View File

@ -0,0 +1,40 @@
# Custom Modules (`library/`) for CyMaIS
This directory contains **custom Ansible modules** developed specifically for the CyMaIS project.
## When to Use the `library/` Directory
- **Place custom Ansible modules here:**
Use this directory for any Python modules you have written yourself that are not part of the official Ansible distribution.
- **Extend automation capabilities:**
Custom modules allow you to implement logic, workflows, or integrations that are not available through built-in Ansible modules or existing community collections.
- **Project-specific functionality:**
Use for project- or infrastructure-specific tasks, such as managing custom APIs, provisioning special infrastructure resources, or integrating with internal systems.
### Examples
- Managing a special internal API for your company.
- Automating a resource that has no official Ansible module.
- Creating a highly customized deployment step for your environment.
## Usage Example
In your playbook, call your custom module as you would any other Ansible module:
```yaml
- name: Use custom CyMaIS module
cymais_my_custom_module:
option1: value1
option2: value2
````
Ansible automatically looks in the `library/` directory for custom modules during execution.
## When *not* to Use the `library/` Directory
* Do **not** place shared utility code here—put that in `module_utils/` for use across multiple modules or plugins.
* Do **not** put filter or lookup plugins here—those belong in `filter_plugins/` or `lookup_plugins/` respectively.
## Further Reading
* [Developing Ansible Modules](https://docs.ansible.com/ansible/latest/dev_guide/developing_modules.html)
* [Best Practices: Organizing Custom Modules](https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_documenting.html)

28
lookup_plugins/README.md Normal file
View File

@ -0,0 +1,28 @@
# Custom Lookup Plugins for CyMaIS
This directory contains custom **Ansible lookup plugins** used within the CyMaIS project.
## When to Use a Lookup Plugin
- **Load external data:** Use lookups to retrieve data from files, APIs, databases, environment variables, or other external sources.
- **Context-aware data access:** Lookups can access the full Ansible context, including inventory, facts, and runtime variables.
- **Generate dynamic lists:** Lookups are often used to build inventories, secrets, or host lists dynamically.
### Examples
```yaml
# Load the contents of a file as a variable
my_secret: "{{ lookup('file', '/path/to/secret.txt') }}"
# Retrieve a list of hostnames from an external source
host_list: "{{ lookup('cymais_inventory_hosts', 'group_name') }}"
````
## When *not* to Use a Lookup Plugin
* If you only need to **manipulate or transform data already available** in your playbook, prefer a filter plugin instead.
## Further Reading
* [Ansible Lookup Plugins Documentation](https://docs.ansible.com/ansible/latest/plugins/lookup.html)
* [Developing Ansible Lookup Plugins](https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#developing-lookup-plugins)

View File

@ -35,7 +35,7 @@ if _IN_DOCKER:
Sound = Quiet Sound = Quiet
else: else:
from utils.sounds import Sound from module_utils.sounds import Sound
def color_text(text, color): def color_text(text, color):

32
module_utils/README.md Normal file
View File

@ -0,0 +1,32 @@
# Shared Utility Code (`module_utils/`) for CyMaIS
This directory contains shared Python utility code (also known as "library code") for use by custom Ansible modules, plugins, or roles in the CyMaIS project.
## When to Use `module_utils`
- **Shared logic:** Use `module_utils` to define functions, classes, or helpers that are shared across multiple custom modules, plugins, or filter/lookups in your project.
- **Reduce duplication:** Centralize code such as API clients, input validation, complex calculations, or protocol helpers.
- **Maintainability:** If you find yourself repeating code in different custom modules/plugins, refactor it into `module_utils/`.
### Examples
- Shared HTTP(S) connection handler for multiple modules.
- Common validation or transformation functions for user input.
- Utility functions for interacting with Docker, LDAP, etc.
## Usage Example
In a custom Ansible module or plugin:
```python
from ansible.module_utils.cymais_utils import my_shared_function
````
## When *not* to Use `module_utils`
* Do not place standalone Ansible modules or plugins here—those go into `library/`, `filter_plugins/`, or `lookup_plugins/` respectively.
* Only use for code that will be **imported** by other plugins or modules.
## Further Reading
* [Ansible Module Utilities Documentation](https://docs.ansible.com/ansible/latest/dev_guide/developing_module_utilities.html)
* [Best Practices: Reusing Code with module\_utils](https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#sharing-code-among-plugins)

View File

@ -1,7 +1,7 @@
import yaml import yaml
from yaml.loader import SafeLoader from yaml.loader import SafeLoader
from typing import Any, Dict from typing import Any, Dict
from utils.handler.vault import VaultScalar from module_utils.handler.vault import VaultScalar
class YamlHandler: class YamlHandler:
@staticmethod @staticmethod

View File

@ -3,8 +3,8 @@ import hashlib
import bcrypt import bcrypt
from pathlib import Path from pathlib import Path
from typing import Dict from typing import Dict
from utils.handler.yaml import YamlHandler from module_utils.handler.yaml import YamlHandler
from utils.handler.vault import VaultHandler, VaultScalar from module_utils.handler.vault import VaultHandler, VaultScalar
import string import string
import sys import sys
import base64 import base64

View File

@ -1,4 +1,4 @@
# File: utils/valid_deploy_id.py # File: module_utils/valid_deploy_id.py
""" """
Utility for validating deployment application IDs against defined roles and inventory. Utility for validating deployment application IDs against defined roles and inventory.
""" """

View File

@ -1,17 +1,17 @@
# Helper variables # Helper variables
_database_id: "svc-db-{{ database_type }}" _database_id: "svc-db-{{ database_type }}"
_database_central_name: "{{ applications | get_app_conf( _database_id, 'docker.services.' ~ database_type ~ '.name') }}" _database_central_name: "{{ applications | get_app_conf( _database_id, 'docker.services.' ~ database_type ~ '.name') }}"
_database_consumer_public_id: "{{ database_application_id | get_public_id }}" _database_consumer_entity_name: "{{ database_application_id | get_entity_name }}"
_database_central_enabled: "{{ applications | get_app_conf(database_application_id, 'features.central_database', False) }}" _database_central_enabled: "{{ applications | get_app_conf(database_application_id, 'features.central_database', False) }}"
# Definition # Definition
database_name: "{{ applications | get_app_conf( database_application_id, 'database.name', false, _database_consumer_public_id ) }}" # The overwritte configuration is needed by bigbluebutton database_name: "{{ applications | get_app_conf( database_application_id, 'database.name', false, _database_consumer_entity_name ) }}" # The overwritte configuration is needed by bigbluebutton
database_instance: "{{ _database_central_name if _database_central_enabled else database_name }}" # This could lead to bugs at dedicated database @todo cleanup database_instance: "{{ _database_central_name if _database_central_enabled else database_name }}" # This could lead to bugs at dedicated database @todo cleanup
database_host: "{{ _database_central_name if _database_central_enabled else 'database' }}" # This could lead to bugs at dedicated database @todo cleanup database_host: "{{ _database_central_name if _database_central_enabled else 'database' }}" # This could lead to bugs at dedicated database @todo cleanup
database_username: "{{ applications | get_app_conf(database_application_id, 'database.username', false, _database_consumer_public_id)}}" # The overwritte configuration is needed by bigbluebutton database_username: "{{ applications | get_app_conf(database_application_id, 'database.username', false, _database_consumer_entity_name)}}" # The overwritte configuration is needed by bigbluebutton
database_password: "{{ applications | get_app_conf(database_application_id, 'credentials.database_password', true) }}" database_password: "{{ applications | get_app_conf(database_application_id, 'credentials.database_password', true) }}"
database_port: "{{ ports.localhost.database[ _database_id ] }}" database_port: "{{ ports.localhost.database[ _database_id ] }}"
database_env: "{{docker_compose.directories.env}}{{database_type}}.env" database_env: "{{docker_compose.directories.env}}{{database_type}}.env"
database_url_jdbc: "jdbc:{{ database_type if database_type == 'mariadb' else 'postgresql' }}://{{ database_host }}:{{ database_port }}/{{ database_name }}" database_url_jdbc: "jdbc:{{ database_type if database_type == 'mariadb' else 'postgresql' }}://{{ database_host }}:{{ database_port }}/{{ database_name }}"
database_url_full: "{{database_type}}://{{database_username}}:{{database_password}}@{{database_host}}:{{database_port}}/{{ database_name }}" database_url_full: "{{database_type}}://{{database_username}}:{{database_password}}@{{database_host}}:{{database_port}}/{{ database_name }}"
database_volume: "{{ _database_consumer_public_id ~ '_' if not _database_central_enabled }}{{ database_host }}" database_volume: "{{ _database_consumer_entity_name ~ '_' if not _database_central_enabled }}{{ database_host }}"

View File

@ -1,2 +1,2 @@
# @See https://chatgpt.com/share/67a23d18-fb54-800f-983c-d6d00752b0b4 # @See https://chatgpt.com/share/67a23d18-fb54-800f-983c-d6d00752b0b4
docker_compose: "{{ path_docker_compose_instances | get_docker_compose(application_id) }}" docker_compose: "{{ path_docker_compose_instances | get_docker_paths(application_id) }}"

View File

@ -3,7 +3,7 @@
{% set redis_version = applications | get_app_conf('svc-db-redis', 'docker.services.redis.version')%} {% set redis_version = applications | get_app_conf('svc-db-redis', 'docker.services.redis.version')%}
redis: redis:
image: "{{ redis_image }}:{{ redis_version }}" image: "{{ redis_image }}:{{ redis_version }}"
container_name: {{ application_id | get_public_id }}-redis container_name: {{ application_id | get_entity_name }}-redis
restart: {{ docker_restart_policy }} restart: {{ docker_restart_policy }}
logging: logging:
driver: journald driver: journald

View File

@ -1,6 +1,6 @@
- name: "Transfering oauth2-proxy-keycloak.cfg.j2 to {{(path_docker_compose_instances | get_docker_compose(application_id)).directories.volumes}}" - name: "Transfering oauth2-proxy-keycloak.cfg.j2 to {{(path_docker_compose_instances | get_docker_paths(application_id)).directories.volumes}}"
template: template:
src: "{{ playbook_dir }}/roles/web-app-oauth2-proxy/templates/oauth2-proxy-keycloak.cfg.j2" src: "{{ playbook_dir }}/roles/web-app-oauth2-proxy/templates/oauth2-proxy-keycloak.cfg.j2"
dest: "{{(path_docker_compose_instances | get_docker_compose(application_id)).directories.volumes}}{{applications | get_app_conf('oauth2-proxy','configuration_file')}}" dest: "{{(path_docker_compose_instances | get_docker_paths(application_id)).directories.volumes}}{{applications | get_app_conf('oauth2-proxy','configuration_file')}}"
notify: notify:
- docker compose up - docker compose up

View File

@ -11,7 +11,7 @@ sys.path.insert(0, dir_path)
# Import functions and classes to test # Import functions and classes to test
from cli.create.credentials import ask_for_confirmation, main from cli.create.credentials import ask_for_confirmation, main
from utils.handler.vault import VaultHandler, VaultScalar from module_utils.handler.vault import VaultHandler, VaultScalar
import subprocess import subprocess
import tempfile import tempfile
import yaml import yaml

View File

@ -14,9 +14,9 @@ sys.path.insert(
), ),
) )
from utils.handler.yaml import YamlHandler from module_utils.handler.yaml import YamlHandler
from utils.handler.vault import VaultHandler, VaultScalar from module_utils.handler.vault import VaultHandler, VaultScalar
from utils.manager.inventory import InventoryManager from module_utils.manager.inventory import InventoryManager
class TestInventoryManager(unittest.TestCase): class TestInventoryManager(unittest.TestCase):

View File

@ -34,10 +34,10 @@ class TestGetAllInvokableApps(unittest.TestCase):
"invokable": True "invokable": True
}, },
"util": { "util": {
"title": "Utils", "title": "module_utils",
"invokable": False, "invokable": False,
"desk": { "desk": {
"title": "Desktop Utils", "title": "Desktop module_utils",
"invokable": True "invokable": True
} }
} }

View File

@ -1,26 +0,0 @@
import unittest
from filter_plugins.get_public_id import FilterModule
class TestGetPublicId(unittest.TestCase):
def setUp(self):
self.filter = FilterModule().filters()['get_public_id']
def test_extract_public_id(self):
self.assertEqual(self.filter("svc-user-abc123"), "abc123")
self.assertEqual(self.filter("something-simple-xyz"), "xyz")
self.assertEqual(self.filter("a-b-c-d-e"), "e")
def test_no_hyphen(self):
with self.assertRaises(ValueError):
self.filter("nohyphenhere")
def test_non_string_input(self):
with self.assertRaises(ValueError):
self.filter(12345)
def test_empty_string(self):
with self.assertRaises(ValueError):
self.filter("")
if __name__ == '__main__':
unittest.main()

View File

@ -1,5 +1,5 @@
import unittest import unittest
from utils.dict_renderer import DictRenderer from module_utils.dict_renderer import DictRenderer
class TestDictRenderer(unittest.TestCase): class TestDictRenderer(unittest.TestCase):
def setUp(self): def setUp(self):

View File

@ -1,9 +1,9 @@
# File: tests/unit/utils/test_valid_deploy_id.py # File: tests/unit/module_utils/test_valid_deploy_id.py
import os import os
import tempfile import tempfile
import unittest import unittest
import yaml import yaml
from utils.valid_deploy_id import ValidDeployId from module_utils.valid_deploy_id import ValidDeployId
class TestValidDeployId(unittest.TestCase): class TestValidDeployId(unittest.TestCase):
def setUp(self): def setUp(self):