Compare commits

...

6 Commits

92 changed files with 943 additions and 232 deletions

View File

@@ -25,10 +25,17 @@ clean:
@echo "Removing ignored git files" @echo "Removing ignored git files"
git clean -fdX git clean -fdX
list:
@echo Generating the roles list
python3 main.py build roles_list
tree: tree:
@echo Generating Tree @echo Generating Tree
python3 main.py build tree -D 2 --no-signal python3 main.py build tree -D 2 --no-signal
mig: list tree
@echo Creating meta data for meta infinity graph
dockerignore: dockerignore:
@echo Create dockerignore @echo Create dockerignore
cat .gitignore > .dockerignore cat .gitignore > .dockerignore
@@ -61,9 +68,9 @@ build: clean dockerignore
install: build install: build
@echo "⚙️ Install complete." @echo "⚙️ Install complete."
partial-test: messy-test:
@echo "🧪 Running Python tests…" @echo "🧪 Running Python tests…"
python -m unittest discover -s tests PYTHONPATH=. python -m unittest discover -s tests
@echo "📑 Checking Ansible syntax…" @echo "📑 Checking Ansible syntax…"
ansible-playbook playbook.yml --syntax-check ansible-playbook playbook.yml --syntax-check

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:

113
cli/build/inventory/full.py Normal file
View File

@@ -0,0 +1,113 @@
#!/usr/bin/env python3
# cli/build/inventory/full.py
import argparse
import sys
import os
try:
from filter_plugins.get_all_invokable_apps import get_all_invokable_apps
except ImportError:
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')))
from filter_plugins.get_all_invokable_apps import get_all_invokable_apps
import yaml
import json
def build_group_inventory(apps, host):
"""
Builds a group-based Ansible inventory: each app is a group containing the host.
"""
groups = {app: {"hosts": [host]} for app in apps}
inventory = {
"all": {
"hosts": [host],
"children": {app: {} for app in apps},
},
**groups
}
return inventory
def build_hostvar_inventory(apps, host):
"""
Alternative: Builds an inventory where all invokables are set as hostvars (as a list).
"""
return {
"all": {
"hosts": [host],
},
"_meta": {
"hostvars": {
host: {
"invokable_applications": apps
}
}
}
}
def main():
parser = argparse.ArgumentParser(
description='Build a dynamic Ansible inventory for a given host with all invokable applications.'
)
parser.add_argument(
'--host',
required=True,
help='Hostname to assign to all invokable application groups'
)
parser.add_argument(
'-f', '--format',
choices=['json', 'yaml'],
default='yaml',
help='Output format (yaml [default], json)'
)
parser.add_argument(
'--inventory-style',
choices=['group', 'hostvars'],
default='group',
help='Inventory style: group (default, one group per app) or hostvars (list as hostvar)'
)
parser.add_argument(
'-c', '--categories-file',
default=os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'roles', 'categories.yml')),
help='Path to roles/categories.yml (default: roles/categories.yml at project root)'
)
parser.add_argument(
'-r', '--roles-dir',
default=os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'roles')),
help='Path to roles/ directory (default: roles/ at project root)'
)
parser.add_argument(
'-o', '--output',
help='Write output to file instead of stdout'
)
args = parser.parse_args()
try:
apps = get_all_invokable_apps(
categories_file=args.categories_file,
roles_dir=args.roles_dir
)
except Exception as e:
sys.stderr.write(f"Error: {e}\n")
sys.exit(1)
# Select inventory style
if args.inventory_style == 'group':
inventory = build_group_inventory(apps, args.host)
else:
inventory = build_hostvar_inventory(apps, args.host)
# Output in chosen format
if args.format == 'json':
output = json.dumps(inventory, indent=2)
else:
output = yaml.safe_dump(inventory, default_flow_style=False)
if args.output:
with open(args.output, 'w') as f:
f.write(output)
else:
print(output)
if __name__ == '__main__':
main()

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:

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env python3
# cli/meta/applications/invokable.py
import argparse
import sys
import os
# Import filter plugin for get_all_invokable_apps
try:
from filter_plugins.get_all_invokable_apps import get_all_invokable_apps
except ImportError:
# Try to adjust sys.path if running outside Ansible
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')))
try:
from filter_plugins.get_all_invokable_apps import get_all_invokable_apps
except ImportError:
sys.stderr.write("Could not import filter_plugins.get_all_invokable_apps. Check your PYTHONPATH.\n")
sys.exit(1)
def main():
parser = argparse.ArgumentParser(
description='List all invokable applications (application_ids) based on invokable paths from categories.yml and available roles.'
)
parser.add_argument(
'-c', '--categories-file',
default=os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'roles', 'categories.yml')),
help='Path to roles/categories.yml (default: roles/categories.yml at project root)'
)
parser.add_argument(
'-r', '--roles-dir',
default=os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'roles')),
help='Path to roles/ directory (default: roles/ at project root)'
)
args = parser.parse_args()
try:
result = get_all_invokable_apps(
categories_file=args.categories_file,
roles_dir=args.roles_dir
)
except Exception as e:
sys.stderr.write(f"Error: {e}\n")
sys.exit(1)
for app_id in result:
print(app_id)
if __name__ == '__main__':
main()

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

@@ -0,0 +1,54 @@
import os
import yaml
def get_all_invokable_apps(
categories_file=None,
roles_dir=None
):
"""
Return all application_ids (or role names) for roles whose directory names match invokable paths from categories.yml.
:param categories_file: Path to categories.yml (default: roles/categories.yml at project root)
:param roles_dir: Path to roles directory (default: roles/ at project root)
:return: List of application_ids (or role names)
"""
# Resolve defaults
here = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.abspath(os.path.join(here, '..'))
if not categories_file:
categories_file = os.path.join(project_root, 'roles', 'categories.yml')
if not roles_dir:
roles_dir = os.path.join(project_root, 'roles')
# Get invokable paths
from filter_plugins.invokable_paths import get_invokable_paths
invokable_paths = get_invokable_paths(categories_file)
if not invokable_paths:
return []
result = []
if not os.path.isdir(roles_dir):
return []
for role in sorted(os.listdir(roles_dir)):
role_path = os.path.join(roles_dir, role)
if not os.path.isdir(role_path):
continue
if any(role == p or role.startswith(p + '-') for p in invokable_paths):
vars_file = os.path.join(role_path, 'vars', 'main.yml')
if os.path.isfile(vars_file):
try:
with open(vars_file, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f) or {}
app_id = data.get('application_id', role)
except Exception:
app_id = role
else:
app_id = role
result.append(app_id)
return sorted(result)
class FilterModule(object):
def filters(self):
return {
'get_all_invokable_apps': get_all_invokable_apps
}

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,9 +1,15 @@
def get_docker_compose(path_docker_compose_instances: str, application_id: str) -> dict: import sys, os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from module_utils.entity_name_utils import get_entity_name
def get_docker_paths(application_id: str, path_docker_compose_instances: 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.
Uses get_entity_name to extract the entity name from application_id.
""" """
base = f"{path_docker_compose_instances}{application_id}/" entity = get_entity_name(application_id)
base = f"{path_docker_compose_instances}{entity}/"
return { return {
'directories': { 'directories': {
@@ -23,5 +29,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

@@ -0,0 +1,9 @@
import sys, os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from module_utils.entity_name_utils import get_entity_name
class FilterModule(object):
def filters(self):
return {
'get_entity_name': get_entity_name,
}

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

View File

@@ -27,7 +27,7 @@ ports:
web-app-mediawiki: 8004 web-app-mediawiki: 8004
web-app-mybb: 8005 web-app-mybb: 8005
yourls: 8006 yourls: 8006
mailu: 8007 web-app-mailu: 8007
web-app-elk: 8008 web-app-elk: 8008
web-app-mastodon: 8009 web-app-mastodon: 8009
web-app-pixelfed: 8010 web-app-pixelfed: 8010
@@ -67,7 +67,7 @@ ports:
simpleicons: 8044 simpleicons: 8044
libretranslate: 8055 libretranslate: 8055
pretix: 8056 pretix: 8056
bigbluebutton: 48087 # This port is predefined by bbb. @todo Try to change this to a 8XXX port web-app-bigbluebutton: 48087 # This port is predefined by bbb. @todo Try to change this to a 8XXX port
# Ports which are exposed to the World Wide Web # Ports which are exposed to the World Wide Web
public: public:
@@ -78,8 +78,8 @@ ports:
ldaps: ldaps:
svc-db-openldap: 636 svc-db-openldap: 636
stun: stun:
bigbluebutton: 3478 # Not sure if it's right placed here or if it should be moved to localhost section web-app-bigbluebutton: 3478 # Not sure if it's right placed here or if it should be moved to localhost section
web-app-nextcloud: 3479 web-app-nextcloud: 3479
turn: turn:
bigbluebutton: 5349 # Not sure if it's right placed here or if it should be moved to localhost section web-app-bigbluebutton: 5349 # Not sure if it's right placed here or if it should be moved to localhost section
web-app-nextcloud: 5350 # Not used yet web-app-nextcloud: 5350 # Not used yet

View File

@@ -42,7 +42,7 @@ defaults_networks:
subnet: 192.168.101.240/28 subnet: 192.168.101.240/28
web-app-matrix: web-app-matrix:
subnet: 192.168.102.0/28 subnet: 192.168.102.0/28
mailu: web-app-mailu:
# Use one of the last container ips for dns resolving so that it isn't used # Use one of the last container ips for dns resolving so that it isn't used
dns: 192.168.102.29 dns: 192.168.102.29
subnet: 192.168.102.16/28 subnet: 192.168.102.16/28
@@ -94,7 +94,7 @@ defaults_networks:
subnet: 192.168.103.144/28 subnet: 192.168.103.144/28
# /24 Networks / 254 Usable Clients # /24 Networks / 254 Usable Clients
bigbluebutton: web-app-bigbluebutton:
subnet: 10.7.7.0/24 # This variable does not have an impact. It's just there for documentation reasons, because this network is used in bbb subnet: 10.7.7.0/24 # This variable does not have an impact. It's just there for documentation reasons, because this network is used in bbb
svc-db-postgres: svc-db-postgres:
subnet: 192.168.200.0/24 subnet: 192.168.200.0/24

View File

@@ -19,7 +19,7 @@ defaults_service_provider:
bluesky: >- bluesky: >-
{{ ('@' ~ users.contact.username ~ '.' ~ domains.bluesky.api) {{ ('@' ~ users.contact.username ~ '.' ~ domains.bluesky.api)
if 'bluesky' in group_names else '' }} if 'bluesky' in group_names else '' }}
email: "{{ users.contact.username ~ '@' ~ primary_domain if 'mailu' in group_names else '' }}" email: "{{ users.contact.username ~ '@' ~ primary_domain if 'web-app-mailu' in group_names else '' }}"
mastodon: "{{ '@' ~ users.contact.username ~ '@' ~ domains | get_domain('web-app-mastodon') if 'web-app-mastodon' in group_names else '' }}" mastodon: "{{ '@' ~ users.contact.username ~ '@' ~ domains | get_domain('web-app-mastodon') if 'web-app-mastodon' in group_names else '' }}"
matrix: "{{ '@' ~ users.contact.username ~ ':' ~ domains['web-app-matrix'].synapse if 'web-app-matrix' in group_names else '' }}" matrix: "{{ '@' ~ users.contact.username ~ ':' ~ domains['web-app-matrix'].synapse if 'web-app-matrix' in group_names else '' }}"
peertube: "{{ '@' ~ users.contact.username ~ '@' ~ domains | get_domain('web-app-peertube') if 'web-app-peertube' in group_names else '' }}" peertube: "{{ '@' ~ users.contact.username ~ '@' ~ domains | get_domain('web-app-peertube') if 'web-app-peertube' in group_names else '' }}"

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

@@ -0,0 +1,49 @@
import os
import yaml
def load_categories_tree(categories_file):
with open(categories_file, 'r', encoding='utf-8') as f:
categories = yaml.safe_load(f)['roles']
return categories
def flatten_categories(tree, prefix=''):
"""Flattens nested category tree to all possible category paths."""
result = []
for k, v in tree.items():
current = f"{prefix}-{k}" if prefix else k
result.append(current)
if isinstance(v, dict):
for sk, sv in v.items():
if isinstance(sv, dict):
result.extend(flatten_categories({sk: sv}, current))
return result
def get_entity_name(role_name):
"""
Get the entity name from a role name by removing the
longest matching category path from categories.yml.
"""
possible_locations = [
os.path.join(os.getcwd(), 'roles', 'categories.yml'),
os.path.join(os.path.dirname(__file__), '..', 'roles', 'categories.yml'),
'roles/categories.yml',
]
categories_file = None
for loc in possible_locations:
if os.path.exists(loc):
categories_file = loc
break
if not categories_file:
return role_name
categories_tree = load_categories_tree(categories_file)
all_category_paths = flatten_categories(categories_tree)
role_name_lc = role_name.lower()
all_category_paths = [cat.lower() for cat in all_category_paths]
for cat in sorted(all_category_paths, key=len, reverse=True):
if role_name_lc.startswith(cat + "-"):
return role_name[len(cat) + 1:]
if role_name_lc == cat:
return ""
return role_name

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

@@ -13,9 +13,9 @@
- database:/var/lib/mysql - database:/var/lib/mysql
healthcheck: healthcheck:
test: [ "CMD", "sh", "-c", "/usr/bin/mariadb --user=$$MYSQL_USER --password=$$MYSQL_PASSWORD --execute 'SHOW DATABASES;'" ] test: [ "CMD", "sh", "-c", "/usr/bin/mariadb --user=$$MYSQL_USER --password=$$MYSQL_PASSWORD --execute 'SHOW DATABASES;'" ]
interval: 3s interval: 10s
timeout: 1s timeout: 5s
retries: 5 retries: 18
networks: networks:
- default - default
{% endif %} {% endif %}

View File

@@ -1,14 +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_entity_name: "{{ database_application_id | get_entity_name }}"
_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_application_id | get_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 applications | get_app_conf(database_application_id, 'features.central_database', False) 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 applications | get_app_conf(database_application_id, 'features.central_database', False) 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_application_id | get_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_entity_name ~ '_' if not _database_central_enabled }}{{ database_host }}"

View File

@@ -5,8 +5,8 @@
- docker-compose - docker-compose
state: present state: present
- name: Adding user {{users.client.username}} to relevant docker usergroup - name: "Adding user {{ users[desktop_username].username }} to relevant docker usergroup"
user: user:
name: "{{users.client.username}}" name: "{{ users[desktop_username].username }}"
groups: docker groups: docker
append: yes append: yes

View File

@@ -1,2 +1,2 @@
auto_start_directory: /home/{{users.client.username}}/.config/autostart/ auto_start_directory: /home/{{ users[desktop_username].username }}/.config/autostart/
application_id: desk-gnome-caffeine application_id: desk-gnome-caffeine

View File

@@ -5,8 +5,8 @@
- gnome-terminal - gnome-terminal
state: present state: present
- name: "Set zsh as default shell for {{users.client.username}}" - name: "Set zsh as default shell for {{ users[desktop_username].username }}"
user: user:
name: "{{users.client.username}}" name: "{{ users[desktop_username].username }}"
shell: /usr/bin/zsh shell: /usr/bin/zsh
become: true become: true

View File

@@ -7,8 +7,8 @@
ansible.builtin.file: ansible.builtin.file:
src: "{{nextcloud_cloud_directory}}{{item}}" src: "{{nextcloud_cloud_directory}}{{item}}"
dest: "{{nextcloud_user_home_directory}}{{item}}" dest: "{{nextcloud_user_home_directory}}{{item}}"
owner: "{{users.client.username}}" owner: "{{ users[desktop_username].username }}"
group: "{{users.client.username}}" group: "{{ users[desktop_username].username }}"
state: link state: link
force: yes force: yes
ignore_errors: true # Just temporary @todo remove ignore_errors: true # Just temporary @todo remove
@@ -29,6 +29,6 @@
ansible.builtin.file: ansible.builtin.file:
src: "{{nextcloud_cloud_directory}}InstantUpload" src: "{{nextcloud_cloud_directory}}InstantUpload"
dest: "{{nextcloud_user_home_directory}}Dump" dest: "{{nextcloud_user_home_directory}}Dump"
owner: "{{users.client.username}}" owner: "{{ users[desktop_username].username }}"
group: "{{users.client.username}}" group: "{{ users[desktop_username].username }}"
state: link state: link

View File

@@ -1,4 +1,4 @@
application_id: desk-nextcloud application_id: desk-nextcloud
nextcloud_user_home_directory: "/home/{{users.client.username}}/" nextcloud_user_home_directory: "/home/{{ users[desktop_username].username }}/"
nextcloud_cloud_fqdn: "{{ applications | get_app_conf(application_id, 'cloud_fqdn') }}" nextcloud_cloud_fqdn: "{{ applications | get_app_conf(application_id, 'cloud_fqdn') }}"
nextcloud_cloud_directory: '{{nextcloud_user_home_directory}}Clouds/{{nextcloud_cloud_fqdn}}/{{users.client.username}}/' nextcloud_cloud_directory: '{{nextcloud_user_home_directory}}Clouds/{{nextcloud_cloud_fqdn}}/{{ users[desktop_username].username }}/'

View File

@@ -19,7 +19,7 @@
- docker compose restart - docker compose restart
- name: docker compose up - name: docker compose up
shell: docker-compose -p {{ application_id }} up -d --force-recreate --remove-orphans --build shell: docker-compose -p {{ application_id | get_entity_name }} up -d --force-recreate --remove-orphans --build
args: args:
chdir: "{{ docker_compose.directories.instance }}" chdir: "{{ docker_compose.directories.instance }}"
executable: /bin/bash executable: /bin/bash

View File

@@ -18,7 +18,7 @@ networks:
application_id in networks.local and application_id in networks.local and
networks.local[application_id].subnet is defined networks.local[application_id].subnet is defined
%} %}
name: {{ application_id }} name: {{ application_id | get_entity_name }}
driver: bridge driver: bridge
ipam: ipam:
driver: default driver: default

View File

@@ -2,5 +2,6 @@
{% if not applications | get_app_conf(application_id, 'features.central_database', False)%} {% if not applications | get_app_conf(application_id, 'features.central_database', False)%}
volumes: volumes:
database: database:
name: {{ database_volume }}
{% endif %} {% endif %}
{{ "\n" }} {{ "\n" }}

View File

@@ -1,6 +1,7 @@
{# This template needs to be included in docker-compose.yml which contain a database and additional volumes #} {# This template needs to be included in docker-compose.yml which contain a database and additional volumes #}
volumes: volumes:
{% if not applications | get_app_conf(application_id, 'features.central_database', False)%} {% if not applications | get_app_conf(application_id, 'features.central_database', False) and applications | get_app_conf(application_id, 'docker.services.database.enabled', False) %}
database: database:
name: {{ database_volume }}
{% endif %} {% endif %}
{{ "\n" }} {{ "\n" }}

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: "{{ application_id | get_docker_paths(path_docker_compose_instances) }}"

View File

@@ -0,0 +1,2 @@
# Todos
- Add cleanup service for docker system prune -f

View File

@@ -24,9 +24,9 @@
restart_policy: "{{ docker_restart_policy }}" restart_policy: "{{ docker_restart_policy }}"
healthcheck: healthcheck:
test: "/usr/bin/mariadb --user=root --password={{ mariadb_root_pwd }} --execute \"SHOW DATABASES;\"" test: "/usr/bin/mariadb --user=root --password={{ mariadb_root_pwd }} --execute \"SHOW DATABASES;\""
interval: 3s interval: 10s
timeout: 1s timeout: 5s
retries: 5 retries: 18
when: run_once_docker_mariadb is not defined when: run_once_docker_mariadb is not defined
register: setup_mariadb_container_result register: setup_mariadb_container_result

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 }}-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

@@ -43,7 +43,7 @@ for filename in os.listdir(config_path):
url = f"{{ web_protocol }}://{domain}" url = f"{{ web_protocol }}://{domain}"
redirected_domains = [domain['source'] for domain in {{ current_play_domain_mappings_redirect}}] redirected_domains = [domain['source'] for domain in {{ current_play_domain_mappings_redirect}}]
redirected_domains.append("{{domains | get_domain('mailu')}}") redirected_domains.append("{{domains | get_domain('web-app-mailu')}}")
expected_statuses = get_expected_statuses(domain, parts, redirected_domains) expected_statuses = get_expected_statuses(domain, parts, redirected_domains)

View File

@@ -43,7 +43,7 @@ if __name__ == "__main__":
if os.path.isfile(docker_compose_file): if os.path.isfile(docker_compose_file):
print(f"Found docker-compose.yml in {dir_path}.") print(f"Found docker-compose.yml in {dir_path}.")
if dir_name == "mailu": if dir_name == "web-app-mailu":
print(f"Directory {dir_name} detected. Performing hard restart...") print(f"Directory {dir_name} detected. Performing hard restart...")
hard_restart_docker_services(dir_path) hard_restart_docker_services(dir_path)
else: else:

View File

@@ -5,7 +5,7 @@
- arduino-docs - arduino-docs
state: present state: present
- name: Adding user {{users.client.username}} to relevant arduino usergroups - name: Adding user {{ users[desktop_username].username }} to relevant arduino usergroups
user: name={{users.client.username}} user: name={{ users[desktop_username].username }}
groups=uucp lock groups=uucp lock
append=yes append=yes

View File

@@ -10,7 +10,7 @@
domain: "{{ item }}" domain: "{{ item }}"
http_port: "{{ ports.localhost.http[application_id] }}" http_port: "{{ ports.localhost.http[application_id] }}"
loop: loop:
- "{{ domains | get_domain('mailu') }}" - "{{ domains | get_domain('web-app-mailu') }}"
- "{{ domain }}" - "{{ domain }}"
- name: "For '{{ application_id }}': configure {{domains | get_domain(application_id)}}.conf" - name: "For '{{ application_id }}': configure {{domains | get_domain(application_id)}}.conf"

View File

@@ -59,7 +59,7 @@
- name: Wait for BigBlueButton - name: Wait for BigBlueButton
wait_for: wait_for:
host: "{{ domains | get_domain('bigbluebutton') }}" host: "{{ domains | get_domain('web-app-bigbluebutton') }}"
port: 80 port: 80
delay: 5 delay: 5
timeout: 600 timeout: 600
@@ -68,11 +68,11 @@
command: command:
cmd: docker compose exec greenlight bundle exec rake admin:create cmd: docker compose exec greenlight bundle exec rake admin:create
chdir: "{{ docker_compose.directories.instance }}" chdir: "{{ docker_compose.directories.instance }}"
when: applications.bigbluebutton.setup | bool when: bigbluebutton_setup
ignore_errors: true ignore_errors: true
register: admin_creation_result register: admin_creation_result
- name: print admin user data - name: print admin user data
debug: debug:
msg: "{{ admin_creation_result.stdout }}" msg: "{{ admin_creation_result.stdout }}"
when: applications.bigbluebutton.setup | bool when: bigbluebutton_setup

View File

@@ -1,11 +1,11 @@
application_id: "bigbluebutton" application_id: "web-app-bigbluebutton"
bbb_repository_directory: "{{ docker_compose.directories.services }}" bbb_repository_directory: "{{ docker_compose.directories.services }}"
docker_compose_file_origine: "{{ docker_compose.directories.services }}docker-compose.yml" docker_compose_file_origine: "{{ docker_compose.directories.services }}docker-compose.yml"
docker_compose_file_final: "{{ docker_compose.directories.instance }}docker-compose.yml" docker_compose_file_final: "{{ docker_compose.directories.instance }}docker-compose.yml"
# Database configuration # Database configuration
database_type: "postgres" database_type: "postgres"
database_password: "{{ applications.bigbluebutton.credentials.postgresql_secret }}" database_password: "{{ applications | get_app_conf(application_id, 'credentials.postgresql_secret') }}"
domain: "{{ domains | get_domain(application_id) }}" domain: "{{ domains | get_domain(application_id) }}"
http_port: "{{ ports.localhost.http[application_id] }}" http_port: "{{ ports.localhost.http[application_id] }}"
@@ -13,3 +13,10 @@ bbb_env_file_link: "{{ docker_compose.directories.instance }}.env"
bbb_env_file_origine: "{{ bbb_repository_directory }}.env" bbb_env_file_origine: "{{ bbb_repository_directory }}.env"
docker_compose_skipp_file_creation: true # Skipp creation of docker-compose.yml file docker_compose_skipp_file_creation: true # Skipp creation of docker-compose.yml file
# Setup
bigbluebutton_setup: "{{ applications | get_app_conf(application_id, 'setup') }}"
# Credentials
bigbluebutton_shared_secret: "{{ applications | get_app_conf(application_id, 'credentials.shared_secret') }}"
bigbluebutton_api_suffix: "{{ applications | get_app_conf(application_id, 'api_suffix') }}"

View File

@@ -0,0 +1,32 @@
- name: Check if config.php exists in EspoCRM
command: docker exec --user root {{ espocrm_name }} test -f {{ espocrm_config_file }}
register: config_file_exists
changed_when: false
failed_when: false
- name: Patch EspoCRM config.php with updated DB credentials
when: config_file_exists.rc == 0
block:
- name: Update DB host
command: >
docker exec --user root {{ espocrm_name }}
sed -i "s/'host' => .*/'host' => '{{ database_host }}',/" {{ espocrm_config_file }}
notify: docker compose up
- name: Update DB name
command: >
docker exec --user root {{ espocrm_name }}
sed -i "s/'dbname' => .*/'dbname' => '{{ database_name }}',/" {{ espocrm_config_file }}
notify: docker compose up
- name: Update DB user
command: >
docker exec --user root {{ espocrm_name }}
sed -i "s/'user' => .*/'user' => '{{ database_username }}',/" {{ espocrm_config_file }}
notify: docker compose up
- name: Update DB password
command: >
docker exec --user root {{ espocrm_name }}
sed -i "s/'password' => .*/'password' => '{{ database_password }}',/" {{ espocrm_config_file }}
notify: docker compose up

View File

@@ -3,6 +3,13 @@
include_role: include_role:
name: cmp-db-docker-proxy name: cmp-db-docker-proxy
- name: Update database credentials
include_tasks: database.yml
- name: Flush handlers to make DB available before password reset
meta: flush_handlers
when: docker_compose_flush_handlers | bool
- name: Set OIDC scopes in EspoCRM config (inside web container) - name: Set OIDC scopes in EspoCRM config (inside web container)
ansible.builtin.shell: | ansible.builtin.shell: |
docker compose exec -T web php -r ' docker compose exec -T web php -r '

View File

@@ -9,3 +9,4 @@ espocrm_version: "{{ applications | get_app_conf(application_id,
espocrm_image: "{{ applications | get_app_conf(application_id, 'docker.services.espocrm.image', True) }}" espocrm_image: "{{ applications | get_app_conf(application_id, 'docker.services.espocrm.image', True) }}"
espocrm_name: "{{ applications | get_app_conf(application_id, 'docker.services.espocrm.name', True) }}" espocrm_name: "{{ applications | get_app_conf(application_id, 'docker.services.espocrm.name', True) }}"
espocrm_volume: "{{ applications | get_app_conf(application_id, 'docker.volumes.data', True) }}" espocrm_volume: "{{ applications | get_app_conf(application_id, 'docker.volumes.data', True) }}"
espocrm_config_file: "/var/www/html/data/config-internal.php"

View File

@@ -0,0 +1,2 @@
# Todos
- Implement hard restart into Backup for mailu

View File

@@ -1,4 +1,3 @@
version: "2024.06" # Docker Image Version
oidc: oidc:
email_by_username: true # If true, then the mail is set by the username. If wrong then the OIDC user email is used email_by_username: true # If true, then the mail is set by the username. If wrong then the OIDC user email is used
enable_user_creation: true # Users will be created if not existing enable_user_creation: true # Users will be created if not existing
@@ -31,3 +30,6 @@ docker:
enabled: true enabled: true
database: database:
enabled: true enabled: true
mailu:
version: "2024.06" # Docker Image Version
name: mailu

View File

@@ -19,7 +19,7 @@
mailu_compose_dir: "{{ docker_compose.directories.instance }}" mailu_compose_dir: "{{ docker_compose.directories.instance }}"
mailu_domain: "{{ primary_domain }}" mailu_domain: "{{ primary_domain }}"
mailu_api_base_url: "http://127.0.0.1:8080/api/v1" mailu_api_base_url: "http://127.0.0.1:8080/api/v1"
mailu_global_api_token: "{{ applications.mailu.credentials.api_token }}" mailu_global_api_token: "{{ applications | get_app_conf(application_id, 'credentials.api_token') }}"
mailu_action: >- mailu_action: >-
{{ {{
( (

View File

@@ -2,13 +2,15 @@
# Core services # Core services
resolver: resolver:
image: {{docker_source}}/unbound:{{applications.mailu.version}} image: {{docker_source}}/unbound:{{ mailu_version }}
container_name: {{mailu_name}}_resolver
{% include 'roles/docker-container/templates/base.yml.j2' %} {% include 'roles/docker-container/templates/base.yml.j2' %}
{% include 'roles/docker-container/templates/networks.yml.j2' %} {% include 'roles/docker-container/templates/networks.yml.j2' %}
ipv4_address: {{networks.local.mailu.dns}} ipv4_address: {{networks.local['web-app-mailu'].dns}}
front: front:
image: {{docker_source}}/nginx:{{applications.mailu.version}} container_name: {{mailu_name}}_front
image: {{docker_source}}/nginx:{{ mailu_version }}
{% include 'roles/docker-container/templates/base.yml.j2' %} {% include 'roles/docker-container/templates/base.yml.j2' %}
ports: ports:
- "127.0.0.1:{{ports.localhost.http[application_id]}}:80" - "127.0.0.1:{{ports.localhost.http[application_id]}}:80"
@@ -30,10 +32,11 @@
webmail: webmail:
radicale: radicale:
dns: dns:
- {{networks.local.mailu.dns}} - {{networks.local['web-app-mailu'].dns}}
admin: admin:
image: {{docker_source}}/admin:{{applications.mailu.version}} container_name: {{mailu_name}}_admin
image: {{docker_source}}/admin:{{ mailu_version }}
{% include 'roles/docker-container/templates/base.yml.j2' %} {% include 'roles/docker-container/templates/base.yml.j2' %}
volumes: volumes:
- "admin_data:/data" - "admin_data:/data"
@@ -44,11 +47,12 @@
front: front:
condition: service_started condition: service_started
dns: dns:
- {{networks.local.mailu.dns}} - {{networks.local['web-app-mailu'].dns}}
{% include 'roles/docker-container/templates/networks.yml.j2' %} {% include 'roles/docker-container/templates/networks.yml.j2' %}
imap: imap:
image: {{docker_source}}/dovecot:{{applications.mailu.version}} container_name: {{mailu_name}}_imap
image: {{docker_source}}/dovecot:{{ mailu_version }}
{% include 'roles/docker-container/templates/base.yml.j2' %} {% include 'roles/docker-container/templates/base.yml.j2' %}
volumes: volumes:
- "dovecot_mail:/mail" - "dovecot_mail:/mail"
@@ -57,11 +61,12 @@
- front - front
- resolver - resolver
dns: dns:
- {{networks.local.mailu.dns}} - {{networks.local['web-app-mailu'].dns}}
{% include 'roles/docker-container/templates/networks.yml.j2' %} {% include 'roles/docker-container/templates/networks.yml.j2' %}
smtp: smtp:
image: {{docker_source}}/postfix:{{applications.mailu.version}} container_name: {{mailu_name}}_smtp
image: {{docker_source}}/postfix:{{ mailu_version }}
{% include 'roles/docker-container/templates/base.yml.j2' %} {% include 'roles/docker-container/templates/base.yml.j2' %}
volumes: volumes:
- "{{docker_compose.directories.volumes}}overrides:/overrides:ro" - "{{docker_compose.directories.volumes}}overrides:/overrides:ro"
@@ -70,22 +75,24 @@
- front - front
- resolver - resolver
dns: dns:
- {{networks.local.mailu.dns}} - {{networks.local['web-app-mailu'].dns}}
{% include 'roles/docker-container/templates/networks.yml.j2' %} {% include 'roles/docker-container/templates/networks.yml.j2' %}
oletools: oletools:
image: {{docker_source}}/oletools:{{applications.mailu.version}} container_name: {{mailu_name}}_oletools
image: {{docker_source}}/oletools:{{ mailu_version }}
hostname: oletools hostname: oletools
restart: {{docker_restart_policy}} restart: {{docker_restart_policy}}
depends_on: depends_on:
- resolver - resolver
dns: dns:
- {{networks.local.mailu.dns}} - {{networks.local['web-app-mailu'].dns}}
{% include 'roles/docker-container/templates/networks.yml.j2' %} {% include 'roles/docker-container/templates/networks.yml.j2' %}
noinet: noinet:
antispam: antispam:
image: {{docker_source}}/rspamd:{{applications.mailu.version}} container_name: {{mailu_name}}_antispam
image: {{docker_source}}/rspamd:{{ mailu_version }}
{% include 'roles/docker-container/templates/base.yml.j2' %} {% include 'roles/docker-container/templates/base.yml.j2' %}
volumes: volumes:
- "filter:/var/lib/rspamd" - "filter:/var/lib/rspamd"
@@ -97,13 +104,14 @@
- antivirus - antivirus
- resolver - resolver
dns: dns:
- {{networks.local.mailu.dns}} - {{networks.local['web-app-mailu'].dns}}
{% include 'roles/docker-container/templates/networks.yml.j2' %} {% include 'roles/docker-container/templates/networks.yml.j2' %}
noinet: noinet:
# Optional services # Optional services
antivirus: antivirus:
container_name: {{mailu_name}}_antivirus
image: clamav/clamav-debian:latest image: clamav/clamav-debian:latest
{% include 'roles/docker-container/templates/base.yml.j2' %} {% include 'roles/docker-container/templates/base.yml.j2' %}
volumes: volumes:
@@ -111,23 +119,25 @@
depends_on: depends_on:
- resolver - resolver
dns: dns:
- {{networks.local.mailu.dns}} - {{networks.local['web-app-mailu'].dns}}
{% include 'roles/docker-container/templates/networks.yml.j2' %} {% include 'roles/docker-container/templates/networks.yml.j2' %}
webdav: webdav:
image: {{docker_source}}/radicale:{{applications.mailu.version}} container_name: {{mailu_name}}_webdav
image: {{docker_source}}/radicale:{{ mailu_version }}
{% include 'roles/docker-container/templates/base.yml.j2' %} {% include 'roles/docker-container/templates/base.yml.j2' %}
volumes: volumes:
- "webdav_data:/data" - "webdav_data:/data"
depends_on: depends_on:
- resolver - resolver
dns: dns:
- {{networks.local.mailu.dns}} - {{networks.local['web-app-mailu'].dns}}
{% include 'roles/docker-container/templates/networks.yml.j2' %} {% include 'roles/docker-container/templates/networks.yml.j2' %}
radicale: radicale:
fetchmail: fetchmail:
image: {{docker_source}}/fetchmail:{{applications.mailu.version}} container_name: {{mailu_name}}_fetchmail
image: {{docker_source}}/fetchmail:{{ mailu_version }}
volumes: volumes:
- "admin_data:/data" - "admin_data:/data"
{% include 'roles/docker-container/templates/base.yml.j2' %} {% include 'roles/docker-container/templates/base.yml.j2' %}
@@ -137,11 +147,12 @@
- imap - imap
- resolver - resolver
dns: dns:
- {{networks.local.mailu.dns}} - {{networks.local['web-app-mailu'].dns}}
{% include 'roles/docker-container/templates/networks.yml.j2' %} {% include 'roles/docker-container/templates/networks.yml.j2' %}
webmail: webmail:
image: {{docker_source}}/webmail:{{applications.mailu.version}} container_name: {{mailu_name}}_webmail
image: {{docker_source}}/webmail:{{ mailu_version }}
{% include 'roles/docker-container/templates/base.yml.j2' %} {% include 'roles/docker-container/templates/base.yml.j2' %}
volumes: volumes:
- "webmail_data:/data" - "webmail_data:/data"
@@ -151,19 +162,27 @@
- front - front
- resolver - resolver
dns: dns:
- {{networks.local.mailu.dns}} - {{networks.local['web-app-mailu'].dns}}
{% include 'roles/docker-container/templates/networks.yml.j2' %} {% include 'roles/docker-container/templates/networks.yml.j2' %}
webmail: webmail:
{% include 'roles/docker-compose/templates/volumes.yml.j2' %} {% include 'roles/docker-compose/templates/volumes.yml.j2' %}
smtp_queue: smtp_queue:
name: {{ mailu_smtp_queue }}
admin_data: admin_data:
name: {{ mailu_admin_data }}
webdav_data: webdav_data:
name: {{ mailu_webdav_data }}
webmail_data: webmail_data:
name: {{ mailu_webmail_data }}
filter: filter:
name: {{ mailu_filter }}
dkim: dkim:
name: {{ mailu_dkim }}
dovecot_mail: dovecot_mail:
name: {{ mailu_dovecot_mail }}
redis: redis:
name: {{ mailu_redis }}
{% include 'roles/docker-compose/templates/networks.yml.j2' %} {% include 'roles/docker-compose/templates/networks.yml.j2' %}
radicale: radicale:

View File

@@ -11,13 +11,13 @@
LD_PRELOAD=/usr/lib/libhardened_malloc.so LD_PRELOAD=/usr/lib/libhardened_malloc.so
# Set to a randomly generated 16 bytes string # Set to a randomly generated 16 bytes string
SECRET_KEY={{applications.mailu.credentials.secret_key}} SECRET_KEY={{applications | get_app_conf(application_id,'credentials.secret_key')}}
# Subnet of the docker network. This should not conflict with any networks to which your system is connected. (Internal and external!) # Subnet of the docker network. This should not conflict with any networks to which your system is connected. (Internal and external!)
SUBNET={{networks.local.mailu.subnet}} SUBNET={{networks.local['web-app-mailu'].subnet}}
# Main mail domain # Main mail domain
DOMAIN={{applications.mailu.domain}} DOMAIN={{ applications | get_app_conf(application_id,'domain') }}
# Hostnames for this server, separated with comas # Hostnames for this server, separated with comas
HOSTNAMES={{domains | get_domain(application_id)}} HOSTNAMES={{domains | get_domain(application_id)}}
@@ -151,7 +151,7 @@ SQLALCHEMY_DATABASE_URI=mysql+mysqlconnector://{{database_username}}:{{database_
API=true API=true
WEB_API=/api WEB_API=/api
# Configures the authentication token. The minimum length is 3 characters. This token must be passed as request header to the API as authentication token. This is a mandatory setting for using the RESTful API. # Configures the authentication token. The minimum length is 3 characters. This token must be passed as request header to the API as authentication token. This is a mandatory setting for using the RESTful API.
API_TOKEN={{applications.mailu.credentials.api_token}} API_TOKEN={{ applications | get_app_conf(application_id, 'credentials.api_token')}}
# Activated https://mailu.io/master/configuration.html#advanced-settings # Activated https://mailu.io/master/configuration.html#advanced-settings

View File

@@ -1,7 +1,7 @@
application_id: "mailu" application_id: "web-app-mailu"
# Database Configuration # Database Configuration
database_password: "{{applications.mailu.credentials.database_password}}" database_password: "{{ applications | get_app_conf(application_id, ' credentials.database_password') }}"
database_type: "mariadb" database_type: "mariadb"
cert_mount_directory: "{{docker_compose.directories.volumes}}certs/" cert_mount_directory: "{{docker_compose.directories.volumes}}certs/"
@@ -13,3 +13,13 @@ docker_source: "{{ 'ghcr.io/heviat' if applications | get_app_conf(a
domain: "{{ domains | get_domain(application_id) }}" domain: "{{ domains | get_domain(application_id) }}"
http_port: "{{ ports.localhost.http[application_id] }}" http_port: "{{ ports.localhost.http[application_id] }}"
proxy_extra_configuration: "client_max_body_size 31M;" proxy_extra_configuration: "client_max_body_size 31M;"
mailu_version: "{{ applications | get_app_conf(application_id, 'docker.services.mailu.version', True) }}"
mailu_name: "{{ applications | get_app_conf(application_id, 'docker.services.mailu.name', True) }}"
mailu_smtp_queue: "mailu_smtp_queue"
mailu_admin_data: "mailu_admin_data"
mailu_webdav_data: "mailu_webdav_data"
mailu_webmail_data: "mailu_webmail_data"
mailu_filter: "mailu_filter"
mailu_dkim: "mailu_dkim"
mailu_dovecot_mail: "mailu_dovecot_mail"
mailu_redis: "mailu_redis"

View File

@@ -1,5 +1,5 @@
single_user_mode: false # Set true for initial setup single_user_mode: false # Set true for initial setup
setup: false # Set true in inventory file to execute the setup and initializing procedures, don't know if this is still necessary @todo test it setup: true # Set true in inventory file to execute the setup and initializing procedures, don't know if this is still necessary @todo test it
features: features:
matomo: true matomo: true
css: true css: true

View File

@@ -0,0 +1,10 @@
- name: flush docker service
meta: flush_handlers
- name: "Execute migration for '{{ application_id }}'"
command:
cmd: "docker-compose run --rm web bundle exec rails db:migrate"
chdir: "{{docker_compose.directories.instance}}"
- name: "Include administrator routines for '{{ application_id }}'"
include_tasks: 02_administrator.yml

View File

@@ -16,15 +16,6 @@
client_max_body_size: "80m" client_max_body_size: "80m"
vhost_flavour: "ws_generic" vhost_flavour: "ws_generic"
- name: flush docker service - name: "start setup procedures for mastodon"
meta: flush_handlers include_tasks: 01_setup.yml
when: mastodon_setup |bool when: mastodon_setup |bool
- name: setup routine for mastodon
command:
cmd: "docker-compose run --rm web bundle exec rails db:migrate"
chdir: "{{docker_compose.directories.instance}}"
when: mastodon_setup |bool
- name: "include create-administrator.yml for mastodon"
include_tasks: create-administrator.yml

View File

@@ -2,7 +2,7 @@
include_role: include_role:
name: cmp-db-docker-proxy name: cmp-db-docker-proxy
- name: "Update database credentials" - name: "Patch Matomo config.ini.php with updated DB credentials"
include_tasks: database.yml include_tasks: database.yml
- name: flush docker service - name: flush docker service

View File

@@ -1,24 +1,15 @@
- name: Backup config.ini.php before patching
command: >
docker cp {{ matomo_name }}:{{ matomo_config }} {{ matomo_backup_file }}
- name: Patch Matomo config.ini.php with updated DB credentials
block:
- name: Update DB host - name: Update DB host
command: > command: >
docker exec --user root {{ matomo_name }} docker exec --user root {{ matomo_name }}
sed -i "s/^host *=.*/host = {{ database_host }}/" {{ matomo_config }} sed -i "s/^host *=.*/host = {{ database_host }}/" {{ matomo_config }}
- name: Update DB name - name: Update DB name
command: > command: >
docker exec --user root {{ matomo_name }} docker exec --user root {{ matomo_name }}
sed -i "s/^dbname *=.*/dbname = {{ database_name }}/" {{ matomo_config }} sed -i "s/^dbname *=.*/dbname = {{ database_name }}/" {{ matomo_config }}
- name: Update DB user - name: Update DB user
command: > command: >
docker exec --user root {{ matomo_name }} docker exec --user root {{ matomo_name }}
sed -i "s/^username *=.*/username = {{ database_username }}/" {{ matomo_config }} sed -i "s/^username *=.*/username = {{ database_username }}/" {{ matomo_config }}
- name: Update DB password - name: Update DB password
command: > command: >
docker exec --user root {{ matomo_name }} docker exec --user root {{ matomo_name }}

View File

@@ -8,7 +8,6 @@ matomo_version: "{{ applications | get_app_conf(application_id, 'docker.se
matomo_image: "{{ applications | get_app_conf(application_id, 'docker.services.matomo.image', True) }}" matomo_image: "{{ applications | get_app_conf(application_id, 'docker.services.matomo.image', True) }}"
matomo_name: "{{ applications | get_app_conf(application_id, 'docker.services.matomo.name', True) }}" matomo_name: "{{ applications | get_app_conf(application_id, 'docker.services.matomo.name', True) }}"
matomo_data: "{{ applications | get_app_conf(application_id, 'docker.volumes.data', True) }}" matomo_data: "{{ applications | get_app_conf(application_id, 'docker.volumes.data', True) }}"
matomo_backup_file: "{{ docker_compose.directories.instance }}/config.ini.php.bak"
matomo_config: "/var/www/html/config/config.ini.php" matomo_config: "/var/www/html/config/config.ini.php"
# I don't know if this is still necessary # I don't know if this is still necessary

View File

@@ -13,10 +13,6 @@
state: directory state: directory
mode: "0755" mode: "0755"
- name: Copy config.php from container to host
command: >
docker cp {{ moodle_container }}:{{ moodle_config }} {{ moodle_backup_file }}
- name: Check if config.php exists - name: Check if config.php exists
command: docker exec --user root {{ moodle_container }} test -f {{ moodle_config }} command: docker exec --user root {{ moodle_container }} test -f {{ moodle_config }}
register: config_file_exists register: config_file_exists

View File

@@ -10,7 +10,6 @@ bitnami_user_group: "{{ bitnami_user }}:{{ bitnami_user }}"
docker_compose_flush_handlers: false # Wait for env update docker_compose_flush_handlers: false # Wait for env update
moodle_backup_file: "{{ docker_compose.directories.instance }}/config.ini.php.bak"
moodle_config: "/bitnami/moodle/config.php" moodle_config: "/bitnami/moodle/config.php"
moodle_version: "{{ applications | get_app_conf(application_id, 'docker.services.moodle.version', True) }}" moodle_version: "{{ applications | get_app_conf(application_id, 'docker.services.moodle.version', True) }}"
moodle_image: "{{ applications | get_app_conf(application_id, 'docker.services.moodle.image', True) }}" moodle_image: "{{ applications | get_app_conf(application_id, 'docker.services.moodle.image', True) }}"

View File

@@ -10,9 +10,12 @@ csp:
- "data:" - "data:"
domains: domains:
canonical: canonical:
nextcloud: "cloud.{{ primary_domain }}" - "cloud.{{ primary_domain }}"
# nextcloud: "cloud.{{ primary_domain }}"
# talk: "talk.{{ primary_domain }}" @todo needs to be activated # talk: "talk.{{ primary_domain }}" @todo needs to be activated
docker: docker:
volumes:
data: nextcloud_data
services: services:
redis: redis:
enabled: true enabled: true
@@ -21,7 +24,7 @@ docker:
nextcloud: nextcloud:
name: "nextcloud" name: "nextcloud"
image: "nextcloud" image: "nextcloud"
version: "latest-fpm-alpine" version: "production-fpm-alpine"
backup: backup:
no_stop_required: true no_stop_required: true
proxy: proxy:
@@ -73,7 +76,7 @@ plugins:
enabled: true enabled: true
bbb: bbb:
# Nextcloud BigBlueButton integration: enables video conferencing using BigBlueButton (https://apps.nextcloud.com/apps/bbb) # Nextcloud BigBlueButton integration: enables video conferencing using BigBlueButton (https://apps.nextcloud.com/apps/bbb)
enabled: "{{ 'bigbluebutton' in group_names | lower }}" enabled: "{{ 'web-app-bigbluebutton' in group_names | lower }}"
#- bookmarks #- bookmarks
# # Nextcloud Bookmarks: manage and share your bookmarks easily (https://apps.nextcloud.com/apps/bookmarks) # # Nextcloud Bookmarks: manage and share your bookmarks easily (https://apps.nextcloud.com/apps/bookmarks)
# enabled: false # enabled: false

View File

@@ -6,13 +6,24 @@
dest: "{{ nextcloud_host_include_instructions_file }}" dest: "{{ nextcloud_host_include_instructions_file }}"
notify: docker compose restart notify: docker compose restart
- name: Flush handlers so Nextcloud container is restarted and ready
meta: flush_handlers
- name: "Wait until Nextcloud is reachable on port {{ports.localhost.http[application_id]}}"
wait_for:
host: 127.0.0.1
port: "{{ports.localhost.http[application_id]}}"
timeout: 120
delay: 2
state: started
- name: Copy include instructions to the container - name: Copy include instructions to the container
command: > command: >
docker cp {{ nextcloud_host_include_instructions_file }} {{ nextcloud_name }}:{{nextcloud_docker_include_instructions_file}} docker cp {{ nextcloud_host_include_instructions_file }} {{ nextcloud_container }}:{{ nextcloud_docker_include_instructions_file }}
- name: Append generated config to config.php only if not present - name: Append generated config to config.php only if not present
command: > command: >
docker exec -u {{nextcloud_docker_user}} {{ nextcloud_name }} sh -c " docker exec -u {{ nextcloud_docker_user }} {{ nextcloud_container }} sh -c "
grep -q '{{ nextcloud_docker_config_additives_directory }}' {{ nextcloud_docker_config_file }} || grep -q '{{ nextcloud_docker_config_additives_directory }}' {{ nextcloud_docker_config_file }} ||
cat {{ nextcloud_docker_include_instructions_file }} >> {{ nextcloud_docker_config_file }}" cat {{ nextcloud_docker_include_instructions_file }} >> {{ nextcloud_docker_config_file }}"
notify: docker compose restart notify: docker compose restart

View File

@@ -65,7 +65,7 @@
- name: Ensure Nextcloud administrator is in the 'admin' group - name: Ensure Nextcloud administrator is in the 'admin' group
command: > command: >
docker exec -u {{ nextcloud_docker_user }} {{ nextcloud_name }} docker exec -u {{ nextcloud_docker_user }} {{ nextcloud_container }}
php occ group:adduser admin {{ nextcloud_administrator_username }} php occ group:adduser admin {{ nextcloud_administrator_username }}
register: add_admin_to_group register: add_admin_to_group
changed_when: "'Added user' in add_admin_to_group.stdout" changed_when: "'Added user' in add_admin_to_group.stdout"

View File

@@ -2,7 +2,7 @@
application: application:
image: "{{ nextcloud_image }}:{{ nextcloud_version }}" image: "{{ nextcloud_image }}:{{ nextcloud_version }}"
container_name: {{ nextcloud_name }} container_name: {{ nextcloud_container }}
volumes: volumes:
- data:{{nextcloud_docker_work_directory}} - data:{{nextcloud_docker_work_directory}}
- {{nextcloud_host_config_additives_directory}}:{{nextcloud_docker_config_additives_directory}}:ro - {{nextcloud_host_config_additives_directory}}:{{nextcloud_docker_config_additives_directory}}:ro
@@ -70,6 +70,7 @@
{% include 'roles/docker-compose/templates/volumes.yml.j2' %} {% include 'roles/docker-compose/templates/volumes.yml.j2' %}
data: data:
name: {{ nextcloud_volume }}
redis: redis:
{% include 'roles/docker-compose/templates/networks.yml.j2' %} {% include 'roles/docker-compose/templates/networks.yml.j2' %}

View File

@@ -23,13 +23,15 @@ nextcloud_control_node_plugin_tasks_directory: "{{role_path}}/tasks/plugins/"
nextcloud_host_config_additives_directory: "{{ docker_compose.directories.volumes }}cymais/" # This folder is the path to which the additive configurations will be copied nextcloud_host_config_additives_directory: "{{ docker_compose.directories.volumes }}cymais/" # This folder is the path to which the additive configurations will be copied
nextcloud_host_include_instructions_file: "{{ docker_compose.directories.volumes }}includes.php" # Path to the instruction file on the host. Responsible for loading the additional configurations nextcloud_host_include_instructions_file: "{{ docker_compose.directories.volumes }}includes.php" # Path to the instruction file on the host. Responsible for loading the additional configurations
nextcloud_domains: "{{ domains[application_id].nextcloud }}" nextcloud_domains: "{{ domains | get_domain(application_id) }}" # This is wrong and should be optimized @todo implement support for multiple domains
# Docker # Docker
nextcloud_volume: "{{ applications | get_app_conf(application_id, 'docker.volumes.data', True) }}"
nextcloud_version: "{{ applications | get_app_conf(application_id, 'docker.services.nextcloud.version', True) }}" nextcloud_version: "{{ applications | get_app_conf(application_id, 'docker.services.nextcloud.version', True) }}"
nextcloud_image: "{{ applications | get_app_conf(application_id, 'docker.services.nextcloud.image', True) }}" nextcloud_image: "{{ applications | get_app_conf(application_id, 'docker.services.nextcloud.image', True) }}"
nextcloud_name: "{{ applications | get_app_conf(application_id, 'docker.services.nextcloud.name', True) }}" nextcloud_container: "{{ applications | get_app_conf(application_id, 'docker.services.nextcloud.name', True) }}"
nextcloud_proxy_name: "{{ applications | get_app_conf(application_id, 'docker.services.proxy.name', True) }}" nextcloud_proxy_name: "{{ applications | get_app_conf(application_id, 'docker.services.proxy.name', True) }}"
nextcloud_proxy_image: "{{ applications | get_app_conf(application_id, 'docker.services.proxy.image', True) }}" nextcloud_proxy_image: "{{ applications | get_app_conf(application_id, 'docker.services.proxy.image', True) }}"
@@ -58,5 +60,5 @@ nextcloud_docker_config_additives_directory: "{{nextcloud_docker_config_direc
nextcloud_docker_include_instructions_file: "/tmp/includes.php" # Path to the temporary file which will be included to the config.php to load the additional configurations nextcloud_docker_include_instructions_file: "/tmp/includes.php" # Path to the temporary file which will be included to the config.php to load the additional configurations
## Execution ## Execution
nextcloud_docker_exec: "docker exec -u {{ nextcloud_docker_user }} {{ nextcloud_name }}" # General execute composition nextcloud_docker_exec: "docker exec -u {{ nextcloud_docker_user }} {{ nextcloud_container }}" # General execute composition
nextcloud_docker_exec_occ: "{{nextcloud_docker_exec}} {{ nextcloud_docker_work_directory }}occ" # Execute docker occ command nextcloud_docker_exec_occ: "{{nextcloud_docker_exec}} {{ nextcloud_docker_work_directory }}occ" # Execute docker occ command

View File

@@ -1,7 +1,7 @@
plugin_configuration: plugin_configuration:
- appid: "bbb" - appid: "bbb"
configkey: "api.secret" configkey: "api.secret"
configvalue: "{{ applications.bigbluebutton.credentials.shared_secret }}" configvalue: "{{ bigbluebutton_shared_secret }}"
- appid: "bbb" - appid: "bbb"
configkey: "api.url" configkey: "api.url"
configvalue: "{{ domains | get_url('bigbluebutton', web_protocol) }}{{applications.bigbluebutton.api_suffix}}" configvalue: "{{ domains | get_url('web-app-bigbluebutton', web_protocol) }}{{ bigbluebutton_api_suffix }}"

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

@@ -3,7 +3,7 @@ followus:
description: Follow us to stay up to recieve the newest CyMaIS updates description: Follow us to stay up to recieve the newest CyMaIS updates
icon: icon:
class: fas fa-newspaper class: fas fa-newspaper
{% if ["mastodon", "bluesky"] | any_in(group_names) %} {% if ["web-app-mastodon", "web-app-bluesky"] | any_in(group_names) %}
children: children:
{% if service_provider.contact.mastodon is defined and service_provider.contact.mastodon != "" %} {% if service_provider.contact.mastodon is defined and service_provider.contact.mastodon != "" %}
- name: Mastodon - name: Mastodon

View File

@@ -6,7 +6,7 @@
- name: "Include role srv-proxy-6-6-domain for {{ application_id }}" - name: "Include role srv-proxy-6-6-domain for {{ application_id }}"
include_role: include_role:
name: srv-proxy-6-6-domain name: srv-proxy-6-6-domain
loop: "{{ applications | get_app_conf(application_id, 'domain', True)s.canonical }}" loop: "{{ applications | get_app_conf(application_id, 'domains.canonical', True) }}"
loop_control: loop_control:
loop_var: domain loop_var: domain
vars: vars:

View File

@@ -1,6 +1,16 @@
# This file is just used for internal configurations by the developer.
# All configuration possibilities are available in the config/main.yml file.
# General
application_id: {{ application_id }} # ID of the application, should be the name of the role folder application_id: {{ application_id }} # ID of the application, should be the name of the role folder
# Database
database_type: 0 # Database type [postgres, mariadb] database_type: 0 # Database type [postgres, mariadb]
# Docker
docker_compose_flush_handlers: true # When this is set to true an auto-flush after the docker-compose.yml, and env deploy is triggered, otherwise you have todo it manual. docker_compose_flush_handlers: true # When this is set to true an auto-flush after the docker-compose.yml, and env deploy is triggered, otherwise you have todo it manual.
docker_compose_skipp_file_creation: false # Skipp creation of docker-compose.yml file
# The following variable mapping is optional, but imt makes it easier to read the code. # The following variable mapping is optional, but imt makes it easier to read the code.
# I recommend, to use this mappings, but you can skipp it and access the config entries direct via get_app_conf # I recommend, to use this mappings, but you can skipp it and access the config entries direct via get_app_conf

View File

@@ -0,0 +1,36 @@
import os
import unittest
import yaml
import warnings
# Dynamically determine the path to the roles directory
ROLES_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'roles'))
class TestApplicationIdDeprecation(unittest.TestCase):
def test_application_id_matches_role_name(self):
"""
Deprecation: application_id in vars/main.yml must match the role name.
"""
for role in os.listdir(ROLES_DIR):
role_path = os.path.join(ROLES_DIR, role)
vars_main_yml = os.path.join(role_path, 'vars', 'main.yml')
if not os.path.isfile(vars_main_yml):
continue
with open(vars_main_yml, 'r', encoding='utf-8') as f:
try:
data = yaml.safe_load(f)
except Exception as e:
self.fail(f"Could not parse {vars_main_yml}: {e}")
if not isinstance(data, dict):
continue
app_id = data.get('application_id')
if app_id is not None and app_id != role:
warnings.warn(
f"[DEPRECATION WARNING] application_id '{app_id}' in {vars_main_yml} "
f"does not match its role directory '{role}'.\n"
f"Please update 'application_id' to match the role name for future compatibility.",
DeprecationWarning
)
if __name__ == "__main__":
unittest.main()

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

@@ -0,0 +1,118 @@
import os
import shutil
import tempfile
import yaml
import unittest
from filter_plugins.get_all_invokable_apps import get_all_invokable_apps
class TestGetAllInvokableApps(unittest.TestCase):
def setUp(self):
"""Create a temporary roles/ directory with categories.yml and some example roles."""
self.test_dir = tempfile.mkdtemp(prefix="invokable_apps_test_")
self.roles_dir = os.path.join(self.test_dir, "roles")
os.makedirs(self.roles_dir, exist_ok=True)
self.categories_file = os.path.join(self.roles_dir, "categories.yml")
# Write a categories.yml with nested invokable/non-invokable paths
categories = {
"roles": {
"web": {
"title": "Web",
"invokable": False,
"app": {
"title": "Applications",
"invokable": True
},
"svc": {
"title": "Services",
"invokable": False
}
},
"update": {
"title": "Update",
"invokable": True
},
"util": {
"title": "module_utils",
"invokable": False,
"desk": {
"title": "Desktop module_utils",
"invokable": True
}
}
}
}
with open(self.categories_file, 'w') as f:
yaml.safe_dump(categories, f)
# Create roles: some should match invokable paths, some shouldn't
roles = [
('web-app-nextcloud', 'web-app-nextcloud'),
('web-app-matomo', 'matomo-app'), # application_id differs
('web-svc-nginx', None), # should NOT match any invokable path
('update', None), # exact match to invokable path
('util-desk-custom', None) # matches util-desk
]
for rolename, appid in roles:
role_dir = os.path.join(self.roles_dir, rolename)
os.makedirs(os.path.join(role_dir, 'vars'), exist_ok=True)
vars_path = os.path.join(role_dir, 'vars', 'main.yml')
data = {}
if appid:
data['application_id'] = appid
with open(vars_path, 'w') as f:
yaml.safe_dump(data, f)
def tearDown(self):
"""Clean up the temporary test directory after each test."""
shutil.rmtree(self.test_dir)
def test_get_all_invokable_apps(self):
"""Should return only applications whose role paths match invokable paths."""
result = get_all_invokable_apps(
categories_file=self.categories_file,
roles_dir=self.roles_dir
)
expected = sorted([
'web-app-nextcloud', # application_id from role
'matomo-app', # application_id from role
'update', # role directory name
'util-desk-custom' # role directory name
])
self.assertEqual(sorted(result), expected)
def test_empty_when_no_invokable(self):
"""Should return an empty list if there are no invokable paths in categories.yml."""
with open(self.categories_file, 'w') as f:
yaml.safe_dump({"roles": {"foo": {"invokable": False}}}, f)
result = get_all_invokable_apps(
categories_file=self.categories_file,
roles_dir=self.roles_dir
)
self.assertEqual(result, [])
def test_empty_when_no_roles(self):
"""Should return an empty list if there are no roles, but categories.yml exists."""
shutil.rmtree(self.roles_dir)
os.makedirs(self.roles_dir, exist_ok=True)
# Recreate categories.yml after removing roles_dir
with open(self.categories_file, 'w') as f:
yaml.safe_dump({"roles": {"web": {"app": {"invokable": True}}}}, f)
result = get_all_invokable_apps(
categories_file=self.categories_file,
roles_dir=self.roles_dir
)
self.assertEqual(result, [])
def test_error_when_no_categories_file(self):
"""Should raise FileNotFoundError if categories.yml is missing."""
os.remove(self.categories_file)
with self.assertRaises(FileNotFoundError):
get_all_invokable_apps(
categories_file=self.categories_file,
roles_dir=self.roles_dir
)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,93 @@
import unittest
import tempfile
import shutil
import os
import sys
import yaml
class TestGetEntityNameFilter(unittest.TestCase):
def setUp(self):
# Create a temporary directory for roles and categories.yml
self.temp_dir = tempfile.mkdtemp()
self.roles_dir = os.path.join(self.temp_dir, 'roles')
os.makedirs(self.roles_dir)
self.categories_file = os.path.join(self.roles_dir, 'categories.yml')
# Minimal categories.yml for tests
categories = {
'roles': {
'web': {
'app': {
'title': "Applications",
'invokable': True
},
'svc': {
'title': "Services",
'invokable': True
}
},
'util': {
'desk': {
'dev': {
'title': "Dev Utilities",
'invokable': True
}
}
},
'sys': {
'bkp': {
'title': "Backup",
'invokable': True
},
'hlth': {
'title': "Health",
'invokable': True
}
}
}
}
with open(self.categories_file, 'w', encoding='utf-8') as f:
yaml.safe_dump(categories, f, default_flow_style=False)
# Patch working directory so plugin finds the test categories.yml
self._cwd = os.getcwd()
os.chdir(self.temp_dir)
# Make sure filter_plugins directory is on sys.path
plugin_path = os.path.join(self._cwd, "filter_plugins")
if plugin_path not in sys.path and os.path.isdir(plugin_path):
sys.path.insert(0, plugin_path)
# Import plugin fresh each time
global get_entity_name
from filter_plugins.get_entity_name import get_entity_name
self.get_entity_name = get_entity_name
def tearDown(self):
os.chdir(self._cwd)
shutil.rmtree(self.temp_dir)
def test_entity_name_web_app(self):
self.assertEqual(self.get_entity_name("web-app-snipe-it"), "snipe-it")
self.assertEqual(self.get_entity_name("web-app-nextcloud"), "nextcloud")
self.assertEqual(self.get_entity_name("web-svc-file"), "file")
def test_entity_name_util_desk_dev(self):
self.assertEqual(self.get_entity_name("util-desk-dev-arduino"), "arduino")
self.assertEqual(self.get_entity_name("util-desk-dev-shell"), "shell")
def test_entity_name_sys_bkp(self):
self.assertEqual(self.get_entity_name("sys-bkp-directory-validator"), "directory-validator")
def test_entity_name_sys_hlth(self):
self.assertEqual(self.get_entity_name("sys-hlth-btrfs"), "btrfs")
def test_no_category_match(self):
# Unknown category, should return input
self.assertEqual(self.get_entity_name("foobar-role"), "foobar-role")
def test_exact_category_match(self):
self.assertEqual(self.get_entity_name("web-app"), "")
if __name__ == "__main__":
unittest.main()

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):