Compare commits

..

13 Commits

72 changed files with 609 additions and 327 deletions

View File

@ -1,8 +1,8 @@
ROLES_DIR := ./roles ROLES_DIR := ./roles
APPLICATIONS_OUT := ./group_vars/all/11_applications.yml APPLICATIONS_OUT := ./group_vars/all/11_applications.yml
APPLICATIONS_SCRIPT := ./cli/generate_defaults_applications.py APPLICATIONS_SCRIPT := ./cli/generate-applications-defaults.py
INCLUDES_OUT := ./tasks/include-docker-roles.yml INCLUDES_OUT := ./tasks/include-docker-roles.yml
INCLUDES_SCRIPT := ./cli/generate_role_includes.py INCLUDES_SCRIPT := ./cli/generate-role-includes.py
.PHONY: build install test .PHONY: build install test

83
cli/deploy.py Normal file
View File

@ -0,0 +1,83 @@
#!/usr/bin/env python3
import argparse
import subprocess
import os
import datetime
def run_ansible_playbook(inventory, playbook, modes, limit=None, password_file=None, verbose=0, skip_tests=False):
start_time = datetime.datetime.now().isoformat()
print(f"\n▶️ Script started at: {start_time}\n")
print("\n🛠️ Building project (make build)...\n")
subprocess.run(["make", "build"], check=True)
if not skip_tests:
print("\n🧪 Running tests (make test)...\n")
subprocess.run(["make", "test"], check=True)
cmd = ["ansible-playbook", "-i", inventory, playbook]
if limit:
cmd.extend(["--limit", limit])
for key, value in modes.items():
val = str(value).lower() if isinstance(value, bool) else str(value)
cmd.extend(["-e", f"{key}={val}"])
if password_file:
cmd.extend(["--vault-password-file", password_file])
else:
cmd.extend(["--ask-vault-pass"])
if verbose:
cmd.append("-" + "v" * verbose)
print("\n🚀 Launching Ansible Playbook...\n")
subprocess.run(cmd, check=True)
end_time = datetime.datetime.now().isoformat()
print(f"\n✅ Script ended at: {end_time}\n")
def main():
script_dir = os.path.dirname(os.path.realpath(__file__))
parser = argparse.ArgumentParser(description="Run Ansible Playbooks")
parser.add_argument("inventory", help="Path to the inventory file")
parser.add_argument("--limit", help="Limit execution to a specific server")
parser.add_argument("--host-type", choices=["server", "personal-computer"], default="server")
parser.add_argument("--reset", action="store_true")
parser.add_argument("--test", action="store_true")
parser.add_argument("--update", action="store_true")
parser.add_argument("--backup", action="store_true")
parser.add_argument("--cleanup", action="store_true")
parser.add_argument("--debug", action="store_true")
parser.add_argument("--password-file")
parser.add_argument("--skip-tests", action="store_true")
parser.add_argument("-v", "--verbose", action="count", default=0)
args = parser.parse_args()
modes = {
"mode_reset": args.reset,
"mode_test": args.test,
"mode_update": args.update,
"mode_backup": args.backup,
"mode_cleanup": args.cleanup,
"enable_debug": args.debug,
"host_type": args.host_type
}
playbook_file = os.path.join(os.path.dirname(script_dir), "playbook.yml")
run_ansible_playbook(
inventory=args.inventory,
playbook=playbook_file,
modes=modes,
limit=args.limit,
password_file=args.password_file,
verbose=args.verbose,
skip_tests=args.skip_tests
)
if __name__ == "__main__":
main()

57
cli/fix-tabs.py Normal file
View File

@ -0,0 +1,57 @@
#!/usr/bin/env python3
import os
import argparse
from pathlib import Path
FILES_FIXED = []
def fix_tabs_in_file(file_path):
"""Replaces tab characters with two spaces in the specified file."""
with open(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
if any('\t' in line for line in lines):
fixed_lines = [line.replace('\t', ' ') for line in lines]
with open(file_path, "w", encoding="utf-8") as f:
f.writelines(fixed_lines)
FILES_FIXED.append(str(file_path))
def find_yml_files(path):
"""Yield all .yml files under a given path recursively."""
for file in path.rglob("*.yml"):
if file.is_file():
yield file
def main():
parser = argparse.ArgumentParser(
description="Fix tab characters in all .yml files under a given path (recursively)."
)
parser.add_argument(
"path",
nargs="?",
default="./",
help="Base path to search for .yml files (default: ./)"
)
args = parser.parse_args()
base_path = Path(args.path).resolve()
if not base_path.exists():
print(f"❌ Path does not exist: {base_path}")
exit(1)
print(f"🔍 Searching for .yml files under: {base_path}\n")
for yml_file in find_yml_files(base_path):
fix_tabs_in_file(yml_file)
if FILES_FIXED:
print("✅ Fixed tab characters in the following files:")
for f in FILES_FIXED:
print(f" - {f}")
else:
print("✅ No tabs found in any .yml files.")
if __name__ == "__main__":
main()

View File

@ -1,36 +0,0 @@
#!/usr/bin/env python3
import os
from pathlib import Path
ROLES_DIR = Path("roles") # Adjust this if needed
FILES_FIXED = []
def fix_tabs_in_file(file_path):
with open(file_path, "r") as f:
lines = f.readlines()
if any('\t' in line for line in lines):
fixed_lines = [line.replace('\t', ' ') for line in lines]
with open(file_path, "w") as f:
f.writelines(fixed_lines)
FILES_FIXED.append(str(file_path))
def main():
for role_dir in sorted(ROLES_DIR.iterdir()):
if not role_dir.is_dir():
continue
vars_main = role_dir / "vars" / "main.yml"
if vars_main.exists():
fix_tabs_in_file(vars_main)
if FILES_FIXED:
print("✅ Fixed tab characters in the following files:")
for f in FILES_FIXED:
print(f" - {f}")
else:
print("✅ No tabs found in any vars/main.yml files.")
if __name__ == "__main__":
main()

20
cli/vault.py Normal file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env python3
import argparse
import subprocess
def run_ansible_vault(action, filename, password_file):
cmd = ["ansible-vault", action, filename, "--vault-password-file", password_file]
subprocess.run(cmd, check=True)
def main():
parser = argparse.ArgumentParser(description="Manage Ansible Vault")
parser.add_argument("action", choices=["edit", "decrypt", "encrypt"], help="Vault action")
parser.add_argument("filename", help="File to process")
parser.add_argument("--password-file", required=True, help="Path to the Vault password file")
args = parser.parse_args()
run_ansible_vault(args.action, args.filename, args.password_file)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,29 @@
from ansible.errors import AnsibleFilterError
class FilterModule(object):
"""
Custom filters for conditional domain assignments
"""
def filters(self):
return {
"add_domain_if_group": self.add_domain_if_group,
}
@staticmethod
def add_domain_if_group(domains_dict, domain_key, domain_value, group_names):
"""
Add {domain_key: domain_value} to domains_dict
only if domain_key is in group_names.
Usage in Jinja:
{{ {}
| add_domain_if_group('akaunting', 'akaunting.' ~ primary_domain, group_names) }}
"""
try:
result = dict(domains_dict)
if domain_key in group_names:
result[domain_key] = domain_value
return result
except Exception as exc:
raise AnsibleFilterError(f"add_domain_if_group failed: {exc}")

View File

@ -1,58 +1,48 @@
# Domains defaults_domains: >-
{{ {}
| add_domain_if_group('akaunting', 'accounting.' ~ primary_domain, group_names)
| add_domain_if_group('attendize', 'tickets.' ~ primary_domain, group_names)
| add_domain_if_group('baserow', 'baserow.' ~ primary_domain, group_names)
| add_domain_if_group('bigbluebutton', 'meet.' ~ primary_domain, group_names)
| add_domain_if_group('bluesky', {'web': 'bskyweb.' ~ primary_domain,'api':'bluesky.' ~ primary_domain}, group_names)
| add_domain_if_group('discourse', 'forum.' ~ primary_domain, group_names)
| add_domain_if_group('elk', 'elk.' ~ primary_domain, group_names)
| add_domain_if_group('espocrm', 'espocrm.' ~ primary_domain, group_names)
| add_domain_if_group('file_server', 'files.' ~ primary_domain, group_names)
| add_domain_if_group('friendica', 'friendica.' ~ primary_domain, group_names)
| add_domain_if_group('funkwhale', 'music.' ~ primary_domain, group_names)
| add_domain_if_group('gitea', 'git.' ~ primary_domain, group_names)
| add_domain_if_group('gitlab', 'gitlab.' ~ primary_domain, group_names)
| add_domain_if_group('html_server', 'html.' ~ primary_domain, group_names)
| add_domain_if_group('keycloak', 'auth.' ~ primary_domain, group_names)
| add_domain_if_group('lam', 'lam.' ~ primary_domain, group_names)
| add_domain_if_group('ldap', 'ldap.' ~ primary_domain, group_names)
| add_domain_if_group('listmonk', 'newsletter.' ~ primary_domain, group_names)
| add_domain_if_group('mailu', 'mail.' ~ primary_domain, group_names)
| add_domain_if_group('mastodon', ['microblog.' ~ primary_domain], group_names)
| add_domain_if_group('matomo', 'matomo.' ~ primary_domain, group_names)
| add_domain_if_group('matrix', 'matrix.' ~ primary_domain, group_names)
| add_domain_if_group('matrix', 'element.' ~ primary_domain, group_names)
| add_domain_if_group('moodle', 'academy.' ~ primary_domain, group_names)
| add_domain_if_group('mediawiki', 'wiki.' ~ primary_domain, group_names)
| add_domain_if_group('nextcloud', 'cloud.' ~ primary_domain, group_names)
| add_domain_if_group('openproject', 'project.' ~ primary_domain, group_names)
| add_domain_if_group('peertube', ['video.' ~ primary_domain], group_names)
| add_domain_if_group('pgadmin', 'pgadmin.' ~ primary_domain, group_names)
| add_domain_if_group('phpmyadmin', 'phpmyadmin.' ~ primary_domain, group_names)
| add_domain_if_group('phpmyldapadmin', 'phpmyldap.' ~ primary_domain, group_names)
| add_domain_if_group('pixelfed', 'picture.' ~ primary_domain, group_names)
| add_domain_if_group('portfolio', primary_domain, group_names)
| add_domain_if_group('presentation', 'slides.' ~ primary_domain, group_names)
| add_domain_if_group('roulette-wheel', 'roulette.' ~ primary_domain, group_names)
| add_domain_if_group('snipe_it', 'inventory.' ~ primary_domain, group_names)
| add_domain_if_group('sphinx', 'docs.' ~ primary_domain, group_names)
| add_domain_if_group('syncope', 'syncope.' ~ primary_domain, group_names)
| add_domain_if_group('taiga', 'kanban.' ~ primary_domain, group_names)
| add_domain_if_group('yourls', 's.' ~ primary_domain, group_names)
| add_domain_if_group('wordpress', ['blog.' ~ primary_domain], group_names)
}}
## Service Domains
defaults_domains:
akaunting: "accounting.{{primary_domain}}"
attendize: "tickets.{{primary_domain}}"
baserow: "baserow.{{primary_domain}}"
bigbluebutton: "meet.{{primary_domain}}"
bluesky_api: "bluesky.{{primary_domain}}"
bluesky_web: "bskyweb.{{primary_domain}}"
discourse: "forum.{{primary_domain}}"
elk: "elk.{{primary_domain}}"
espocrm: "espocrm.{{primary_domain}}"
file_server: "files.{{primary_domain}}"
friendica: "friendica.{{primary_domain}}"
funkwhale: "music.{{primary_domain}}"
gitea: "git.{{primary_domain}}"
gitlab: "gitlab.{{primary_domain}}"
html_server: "html.{{primary_domain}}"
keycloak: "auth.{{primary_domain}}"
lam: "lam.{{primary_domain}}"
ldap: "ldap.{{primary_domain}}"
listmonk: "newsletter.{{primary_domain}}"
mailu: "mail.{{primary_domain}}"
mastodon: "microblog.{{primary_domain}}"
# ATTENTION: Will be owerwritten by the values in domains. Not merged.
mastodon_alternates:
- "mastodon.{{primary_domain}}"
matomo: "matomo.{{primary_domain}}"
synapse: "matrix.{{primary_domain}}"
element: "element.{{primary_domain}}"
moodle: "academy.{{primary_domain}}"
mediawiki: "wiki.{{primary_domain}}"
nextcloud: "cloud.{{primary_domain}}"
openproject: "project.{{primary_domain}}"
peertube: "video.{{primary_domain}}"
# ATTENTION: Will be owerwritten by the values in domains. Not merged.
peertube_alternates: []
pgadmin: "pgadmin.{{primary_domain}}"
phpmyadmin: "phpmyadmin.{{primary_domain}}"
phpmyldap: "phpmyldap.{{primary_domain}}"
pixelfed: "picture.{{primary_domain}}"
portfolio: "{{primary_domain}}"
presentation: "slides.{{primary_domain}}"
roulette-wheel: "roulette.{{primary_domain}}"
snipe_it: "inventory.{{primary_domain}}"
sphinx: "docs.{{primary_domain}}"
syncope: "syncope.{{primary_domain}}"
taiga: "kanban.{{primary_domain}}"
yourls: "s.{{primary_domain}}"
# ATTENTION: Will be owerwritten by the values in domains. Not merged.
wordpress:
- "blog.{{primary_domain}}"
## Domain Redirects
defaults_redirect_domain_mappings: >- defaults_redirect_domain_mappings: >-
{{ [] {{ []
| add_redirect_if_group('akaunting', "akaunting." ~ primary_domain, domains.akaunting, group_names) | add_redirect_if_group('akaunting', "akaunting." ~ primary_domain, domains.akaunting, group_names)
@ -63,19 +53,20 @@ defaults_redirect_domain_mappings: >-
| add_redirect_if_group('gitea', "gitea." ~ primary_domain, domains.gitea, group_names) | add_redirect_if_group('gitea', "gitea." ~ primary_domain, domains.gitea, group_names)
| add_redirect_if_group('keycloak', "keycloak." ~ primary_domain, domains.keycloak, group_names) | add_redirect_if_group('keycloak', "keycloak." ~ primary_domain, domains.keycloak, group_names)
| add_redirect_if_group('lam', domains.ldap, domains.lam, group_names) | add_redirect_if_group('lam', domains.ldap, domains.lam, group_names)
| add_redirect_if_group('phpmyldapadmin', domains.ldap, domains.phpmyldap, group_names) | add_redirect_if_group('phpmyldapadmin', domains.ldap, domains.phpmyldapadmin,group_names)
| add_redirect_if_group('listmonk', "listmonk." ~ primary_domain, domains.listmonk, group_names) | add_redirect_if_group('listmonk', "listmonk." ~ primary_domain, domains.listmonk, group_names)
| add_redirect_if_group('mailu', "mailu." ~ primary_domain, domains.mailu, group_names) | add_redirect_if_group('mailu', "mailu." ~ primary_domain, domains.mailu, group_names)
| add_redirect_if_group('mastodon', "mastodon." ~ primary_domain, domains.mastodon[0], group_names)
| add_redirect_if_group('moodle', "moodle." ~ primary_domain, domains.moodle, group_names) | add_redirect_if_group('moodle', "moodle." ~ primary_domain, domains.moodle, group_names)
| add_redirect_if_group('nextcloud', "nextcloud." ~ primary_domain, domains.nextcloud, group_names) | add_redirect_if_group('nextcloud', "nextcloud." ~ primary_domain, domains.nextcloud, group_names)
| add_redirect_if_group('openproject', "openproject." ~ primary_domain, domains.openproject, group_names) | add_redirect_if_group('openproject', "openproject." ~ primary_domain, domains.openproject, group_names)
| add_redirect_if_group('peertube', "peertube." ~ primary_domain, domains.peertube, group_names) | add_redirect_if_group('peertube', "peertube." ~ primary_domain, domains.peertube[0], group_names)
| add_redirect_if_group('pixelfed', "pictures." ~ primary_domain, domains.pixelfed, group_names) | add_redirect_if_group('pixelfed', "pictures." ~ primary_domain, domains.pixelfed, group_names)
| add_redirect_if_group('pixelfed', "pixelfed." ~ primary_domain, domains.pixelfed, group_names) | add_redirect_if_group('pixelfed', "pixelfed." ~ primary_domain, domains.pixelfed, group_names)
| add_redirect_if_group('yourls', "short." ~ primary_domain, domains.yourls, group_names) | add_redirect_if_group('yourls', "short." ~ primary_domain, domains.yourls, group_names)
| add_redirect_if_group('snipe-it', "snipe-it." ~ primary_domain, domains.snipe_it, group_names) | add_redirect_if_group('snipe-it', "snipe-it." ~ primary_domain, domains.snipe_it, group_names)
| add_redirect_if_group('taiga', "taiga." ~ primary_domain, domains.taiga, group_names) | add_redirect_if_group('taiga', "taiga." ~ primary_domain, domains.taiga, group_names)
| add_redirect_if_group('peertube', "videos." ~ primary_domain, domains.peertube, group_names) | add_redirect_if_group('peertube', "videos." ~ primary_domain, domains.peertube[0], group_names)
| add_redirect_if_group('wordpress', "wordpress." ~ primary_domain, domains.wordpress[0], group_names) | add_redirect_if_group('wordpress', "wordpress." ~ primary_domain, domains.wordpress[0], group_names)
}} }}

View File

@ -16,11 +16,11 @@ defaults_service_provider:
logo: "{{applications.assets_server.url}}/img/logo.png" logo: "{{applications.assets_server.url}}/img/logo.png"
favicon: "{{applications.assets_server.url}}/img/favicon.ico" favicon: "{{applications.assets_server.url}}/img/favicon.ico"
contact: contact:
bluesky: "{{ '@' ~ users.administrator.username ~ '.' ~ domains.bluesky_api if 'bluesky' in group_names else '' }}" bluesky: "{{ '@' ~ users.administrator.username ~ '.' ~ domains.[application_id]['api'] if 'bluesky' in group_names else '' }}"
email: "contact@{{ primary_domain }}" email: "contact@{{ primary_domain }}"
mastodon: "{{ '@' ~ users.administrator.username ~ '@' ~ domains.mastodon if 'mastodon' in group_names else '' }}" mastodon: "{{ '@' ~ users.administrator.username ~ '@' ~ domains.mastodon if 'mastodon' in group_names else '' }}"
matrix: "{{ '@' ~ users.administrator.username ~ ':' ~ domains.synapse if 'matrix' in group_names else '' }}" matrix: "{{ '@' ~ users.administrator.username ~ ':' ~ domains.synapse if 'matrix' in group_names else '' }}"
peertube: "{{ '@' ~ users.administrator.username ~ '@' ~ domains.peertube if 'peertube' in group_names else '' }}" peertube: "{{ '@' ~ users.administrator.username ~ '@' ~ domains.peertube[0] if 'peertube' in group_names else '' }}"
pixelfed: "{{ '@' ~ users.administrator.username ~ '@' ~ domains.pixelfed if 'pixelfed' in group_names else '' }}" pixelfed: "{{ '@' ~ users.administrator.username ~ '@' ~ domains.pixelfed if 'pixelfed' in group_names else '' }}"
phone: "+0 000 000 404" phone: "+0 000 000 404"
wordpress: "{{ '@' ~ users.administrator.username ~ '@' ~ domains.wordpress[0] if 'wordpress' in group_names else '' }}" wordpress: "{{ '@' ~ users.administrator.username ~ '@' ~ domains.wordpress[0] if 'wordpress' in group_names else '' }}"

149
main.py
View File

@ -1,105 +1,86 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse import argparse
import subprocess
import os import os
import datetime import subprocess
import sys
import textwrap
def run_ansible_vault(action, filename, password_file): def format_command_help(name, description, indent=2, col_width=36, width=80):
"""Execute an ansible-vault command with the specified action on a file.""" prefix = " " * indent + f"{name:<{col_width - indent}}"
cmd = ["ansible-vault", action, filename, "--vault-password-file", password_file] wrapper = textwrap.TextWrapper(
subprocess.run(cmd, check=True) width=width,
initial_indent=prefix,
subsequent_indent=" " * col_width
)
return wrapper.fill(description)
def run_ansible_playbook(inventory: str, playbook: str, modes: dict, limit: str = None, password_file: str = None, verbose: int = 0, skip_tests: bool = False): def list_cli_commands(cli_dir):
start_time = datetime.datetime.now().isoformat() return sorted(
print(f"\n▶️ Script started at: {start_time}\n") os.path.splitext(f.name)[0] for f in os.scandir(cli_dir)
print("\n🛠️ Building project (make build)...\n") if f.is_file() and f.name.endswith(".py") and not f.name.startswith("__")
subprocess.run(["make", "build"], check=True) )
if not skip_tests: def extract_description_via_help(cli_script_path):
print("\n🧪 Running tests (make test)...\n") """Run `script --help` and extract the first non-usage line after usage block."""
subprocess.run(["make", "test"], check=True) try:
result = subprocess.run(
[sys.executable, cli_script_path, "--help"],
capture_output=True,
text=True,
check=True
)
lines = result.stdout.splitlines()
"""Execute an ansible-playbook command with optional parameters.""" # Skip until first empty line after usage block
cmd = ["ansible-playbook", "-i", inventory, playbook] for i, line in enumerate(lines):
if line.strip().startswith("usage:"):
continue
if line.strip() == "":
# description usually comes after usage and empty line
for j in range(i+1, len(lines)):
desc = lines[j].strip()
if desc:
return desc
return "-"
except Exception:
return "-"
if limit:
cmd.extend(["--limit", limit])
if modes:
for key, value in modes.items():
arg_value = f"{str(value).lower()}" if isinstance(value, bool) else f"{value}"
cmd.extend(["-e", f"{key}={arg_value}"])
if password_file:
cmd.extend(["--vault-password-file", password_file])
else:
cmd.extend(["--ask-vault-pass"])
if verbose:
cmd.append("-" + "v" * verbose)
print("\n🚀 Launching Ansible Playbook...\n")
subprocess.run(cmd, check=True)
end_time = datetime.datetime.now().isoformat()
print(f"\n✅ Script ended at: {end_time}\n")
def main(): def main():
# Change to script dir to execute all folders relative to their
script_dir = os.path.dirname(os.path.realpath(__file__)) script_dir = os.path.dirname(os.path.realpath(__file__))
cli_dir = os.path.join(script_dir, "cli")
os.chdir(script_dir) os.chdir(script_dir)
parser = argparse.ArgumentParser(description="CyMaIS Ansible Deployment and Vault Management") available_cli_commands = list_cli_commands(cli_dir)
subparsers = parser.add_subparsers(dest="command", required=True)
# Vault subcommand parser # Special case: user ran `cymais playbook --help`
vault_parser = subparsers.add_parser("vault", help="Manage Ansible Vault") if len(sys.argv) >= 3 and sys.argv[1] in available_cli_commands and sys.argv[2] == "--help":
vault_parser.add_argument("action", choices=["edit", "decrypt", "encrypt"], help="Vault action") cli_script_path = os.path.join(cli_dir, f"{sys.argv[1]}.py")
vault_parser.add_argument("filename", help="File to process") subprocess.run([sys.executable, cli_script_path, "--help"])
vault_parser.add_argument("--password-file", required=True, help="Path to the Vault password file") sys.exit(0)
# Playbook subcommand parser # Global --help
playbook_parser = subparsers.add_parser("playbook", help="Run Ansible Playbooks") if "--help" in sys.argv or "-h" in sys.argv or len(sys.argv) == 1:
playbook_parser.add_argument("inventory", help="Path to the inventory file") print("CyMaIS CLI proxy to tools in ./cli/\n")
playbook_parser.add_argument("--limit", help="Limit execution to a specific server") print("Usage:")
playbook_parser.add_argument("--host-type", choices=["server", "personal-computer"], default="server", print(" cymais <command> [options]\n")
help="Host type to run the playbook on; defaults to 'server'") print("Available commands:")
playbook_parser.add_argument("--reset", action="store_true", help="Enable reset mode") for cmd in available_cli_commands:
playbook_parser.add_argument("--test", action="store_true", help="Enable test mode") path = os.path.join(cli_dir, f"{cmd}.py")
playbook_parser.add_argument("--update", action="store_true", help="Enable update mode") desc = extract_description_via_help(path)
playbook_parser.add_argument("--backup", action="store_true", help="Enable backup mode") print(format_command_help(cmd, desc))
playbook_parser.add_argument("--cleanup", action="store_true", help="Enable cleanup mode") print("\nUse 'cymais <command> --help' for details on each command.")
playbook_parser.add_argument("--debug", action="store_true", help="Enable debugging output") sys.exit(0)
playbook_parser.add_argument("--password-file", help="Path to the Vault password file")
playbook_parser.add_argument("--skip-tests", action="store_true", help="Skip running make test before executing the playbook")
playbook_parser.add_argument("-v", "--verbose", action="count", default=0,
help=("Increase verbosity. This option can be specified multiple times "
"to increase the verbosity level (e.g., -vvv for more detailed debug output)."))
# Default flow
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument("cli_command", choices=available_cli_commands)
parser.add_argument("cli_args", nargs=argparse.REMAINDER)
args = parser.parse_args() args = parser.parse_args()
if args.command == "vault": cli_script_path = os.path.join(cli_dir, f"{args.cli_command}.py")
run_ansible_vault(args.action, args.filename, args.password_file) full_cmd = [sys.executable, cli_script_path] + args.cli_args
elif args.command == "playbook": subprocess.run(full_cmd, check=True)
modes = {
"mode_reset": args.reset,
"mode_test": args.test,
"mode_update": args.update,
"mode_backup": args.backup,
"mode_cleanup": args.cleanup,
"enable_debug": args.debug,
"host_type": args.host_type
}
run_ansible_playbook(
inventory=args.inventory,
playbook=f"{script_dir}/playbook.yml",
modes=modes,
limit=args.limit,
password_file=args.password_file,
verbose=args.verbose,
skip_tests=args.skip_tests
)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -10,8 +10,8 @@
domain: "{{ item.domain }}" domain: "{{ item.domain }}"
http_port: "{{ item.http_port }}" http_port: "{{ item.http_port }}"
loop: loop:
- { domain: domains.bluesky_api, http_port: ports.localhost.http.bluesky_api } - { domain: domains.[application_id]['api'], http_port: ports.localhost.http.bluesky_api }
- { domain: domains.bluesky_web, http_port: ports.localhost.http.bluesky_web } - { domain: domains.[application_id]['web'], http_port: ports.localhost.http.bluesky_web }
# The following lines should be removed when the following issue is closed: # The following lines should be removed when the following issue is closed:
# https://github.com/bluesky-social/pds/issues/52 # https://github.com/bluesky-social/pds/issues/52

View File

@ -22,8 +22,8 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
# It doesn't compile yet with this parameters. @todo Fix it # It doesn't compile yet with this parameters. @todo Fix it
args: args:
REACT_APP_PDS_URL: "{{ web_protocol }}://{{domains.bluesky_api}}" # URL des PDS REACT_APP_PDS_URL: "{{ web_protocol }}://{{domains.[application_id]['api']}}" # URL des PDS
REACT_APP_API_URL: "{{ web_protocol }}://{{domains.bluesky_api}}" # API-URL des PDS REACT_APP_API_URL: "{{ web_protocol }}://{{domains.[application_id]['api']}}" # API-URL des PDS
REACT_APP_SITE_NAME: "{{primary_domain | upper}} - Bluesky" REACT_APP_SITE_NAME: "{{primary_domain | upper}} - Bluesky"
REACT_APP_SITE_DESCRIPTION: "Decentral Social " REACT_APP_SITE_DESCRIPTION: "Decentral Social "
ports: ports:

View File

@ -1,6 +1,6 @@
PDS_HOSTNAME="{{domains.bluesky_api}}" PDS_HOSTNAME="{{domains.[application_id]['api']}}"
PDS_ADMIN_EMAIL="{{applications.bluesky.users.administrator.email}}" PDS_ADMIN_EMAIL="{{applications.bluesky.users.administrator.email}}"
PDS_SERVICE_DID="did:web:{{domains.bluesky_api}}" PDS_SERVICE_DID="did:web:{{domains.[application_id]['api']}}"
# See https://mattdyson.org/blog/2024/11/self-hosting-bluesky-pds/ # See https://mattdyson.org/blog/2024/11/self-hosting-bluesky-pds/
PDS_SERVICE_HANDLE_DOMAINS=".{{primary_domain}}" PDS_SERVICE_HANDLE_DOMAINS=".{{primary_domain}}"
@ -15,7 +15,7 @@ PDS_BLOBSTORE_DISK_LOCATION=/opt/pds/blocks
PDS_DATA_DIRECTORY: /opt/pds PDS_DATA_DIRECTORY: /opt/pds
PDS_BLOB_UPLOAD_LIMIT: 52428800 PDS_BLOB_UPLOAD_LIMIT: 52428800
PDS_DID_PLC_URL=https://plc.directory PDS_DID_PLC_URL=https://plc.directory
PDS_BSKY_APP_VIEW_URL=https://{{domains.bluesky_web}} PDS_BSKY_APP_VIEW_URL=https://{{domains.[application_id]['web']}}
PDS_BSKY_APP_VIEW_DID=did:web:{{domains.bluesky_web}} PDS_BSKY_APP_VIEW_DID=did:web:{{domains.[application_id]['web']}}
PDS_REPORT_SERVICE_URL=https://mod.bsky.app PDS_REPORT_SERVICE_URL=https://mod.bsky.app
PDS_REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac PDS_REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac

View File

@ -17,3 +17,19 @@ watch -n 2 "docker compose ps -a"
```bash ```bash
docker inspect --format='{{json .State.Health}}' <container_id> docker inspect --format='{{json .State.Health}}' <container_id>
``` ```
### 🔍 Logging with `journalctl`
All Docker Compose actions triggered by this role are logged to the system journal using `systemd-cat`. Output is simultaneously shown in the terminal and available via `journalctl`.
To view logs for a specific application:
```bash
journalctl -t docker-compose-<application_id> -f
```
Replace `<application_id>` with the actual project name (e.g. `discourse`, `nextcloud`, etc.).
This enables persistent and searchable logs for all container setups and rebuilds.

View File

@ -4,15 +4,18 @@
# https://github.com/ansible/ansible/issues/10244 # https://github.com/ansible/ansible/issues/10244
#- name: shut down docker compose project #- name: shut down docker compose project
# command: # command:
# cmd: docker-compose -p "{{application_id}}" down # cmd: docker-compose -p "{{ application_id }}" down
# listen: docker compose project setup # listen: docker compose project setup
# when: mode_reset | bool # when: mode_reset | bool
# default setup for docker compose files # default setup for docker compose files
- name: docker compose project setup - name: docker compose project setup
command: shell: >
cmd: "docker-compose -p {{application_id}} up -d --force-recreate --remove-orphans" docker-compose -p {{ application_id }} up -d --force-recreate --remove-orphans
chdir: "{{docker_compose.directories.instance}}" 2>&1 | tee >(systemd-cat -t docker-compose-{{ application_id }})
args:
chdir: "{{ docker_compose.directories.instance }}"
executable: /bin/bash
environment: environment:
COMPOSE_HTTP_TIMEOUT: 600 COMPOSE_HTTP_TIMEOUT: 600
DOCKER_CLIENT_TIMEOUT: 600 DOCKER_CLIENT_TIMEOUT: 600
@ -21,9 +24,12 @@
# it's necessary to rebuild when a build in the docker compose files is defined # it's necessary to rebuild when a build in the docker compose files is defined
# for performance reasons it's not recommended to use this if there is no build tag specified # for performance reasons it's not recommended to use this if there is no build tag specified
- name: docker compose project build and setup - name: docker compose project build and setup
command: shell: >
cmd: "docker-compose -p {{application_id}} up -d --force-recreate --build --remove-orphans" docker-compose -p {{ application_id }} up -d --force-recreate --build --remove-orphans
chdir: "{{docker_compose.directories.instance}}" 2>&1 | tee >(systemd-cat -t docker-compose-{{ application_id }})
args:
chdir: "{{ docker_compose.directories.instance }}"
executable: /bin/bash
environment: environment:
COMPOSE_HTTP_TIMEOUT: 600 COMPOSE_HTTP_TIMEOUT: 600
DOCKER_CLIENT_TIMEOUT: 600 DOCKER_CLIENT_TIMEOUT: 600

View File

@ -15,3 +15,13 @@ To reinitialize the container execute:
```bash ```bash
docker network connect discourse_default central-postgres && /opt/docker/discourse/services/discourse_repository/launcher rebuild discourse_application docker network connect discourse_default central-postgres && /opt/docker/discourse/services/discourse_repository/launcher rebuild discourse_application
``` ```
### 🔍 Logging with `journalctl`
All build actions triggered by this role are logged to the system journal using `systemd-cat`. Output is simultaneously shown in the terminal and available via `journalctl`.
To view logs for a specific application:
```bash
journalctl -t rebuild-discourse -f
```

View File

@ -17,7 +17,10 @@
listen: recreate discourse listen: recreate discourse
- name: rebuild discourse - name: rebuild discourse
command: shell: >
cmd: "./launcher rebuild {{applications[application_id].container}}" ./launcher rebuild {{applications[application_id].container}}
2>&1 | tee >(systemd-cat -t rebuild-{{ application_id }})
args:
executable: /bin/bash
chdir: "{{docker_repository_directory }}" chdir: "{{docker_repository_directory }}"
listen: recreate discourse listen: recreate discourse

View File

@ -835,7 +835,7 @@
"secret": "{{oidc.client.secret}}", "secret": "{{oidc.client.secret}}",
{%- set redirect_uris = [] %} {%- set redirect_uris = [] %}
{%- for application, domain in domains.items() %} {%- for application, domain in domains.items() %}
{%- if applications[application] is defined and (applications | is_feature_enabled('oauth2',application) or applications | is_feature_enabled('oidc',application)) %} {%- if applications[application] is defined and (applications | is_feature_enabled('oauth2',application) or applications | is_feature_enabled('oidc',application_id)) %}
{%- if domain is string %} {%- if domain is string %}
{%- set _ = redirect_uris.append(web_protocol ~ '://' ~ domain ~ '/*') %} {%- set _ = redirect_uris.append(web_protocol ~ '://' ~ domain ~ '/*') %}
{%- else %} {%- else %}

View File

@ -6,7 +6,7 @@
- name: "Include setup for domain '{{ domain }}'" - name: "Include setup for domain '{{ domain }}'"
include_role: include_role:
name: nginx-domain-setup name: nginx-domain-setup
loop: "{{ [domains.mastodon] + domains.mastodon_alternates }}" loop: "{{ domains.mastodon }}"
loop_control: loop_control:
loop_var: domain loop_var: domain
vars: vars:

View File

@ -4,7 +4,7 @@
LOCAL_DOMAIN={{domains[application_id]}} LOCAL_DOMAIN={{domains[application_id]}}
ALTERNATE_DOMAINS="{{ domains.mastodon_alternates | join(',') }}" ALTERNATE_DOMAINS="{{ domains.mastodon[1:] | join(',') }}"
SINGLE_USER_MODE={{applications.mastodon.single_user_mode}} SINGLE_USER_MODE={{applications.mastodon.single_user_mode}}
# Credentials # Credentials

View File

@ -143,14 +143,14 @@
- name: create admin account - name: create admin account
command: command:
cmd: docker compose exec -it synapse register_new_matrix_user -u {{applications.matrix.users.administrator.username}} -p {{applications[application_id].credentials.administrator_password}} -a -c /data/homeserver.yaml http://localhost:8008 cmd: docker compose exec -it synapse register_new_matrix_user -u {{applications[application_id].users.administrator.username}} -p {{applications[application_id].credentials.administrator_password}} -a -c /data/homeserver.yaml http://localhost:8008
chdir: "{{ docker_compose.directories.instance }}" chdir: "{{ docker_compose.directories.instance }}"
ignore_errors: true ignore_errors: true
when: applications.matrix.setup | bool when: applications[application_id].setup | bool
- name: create chatgpt bot - name: create chatgpt bot
command: command:
cmd: docker compose exec -it synapse register_new_matrix_user -u chatgptbot -p {{applications[application_id].credentials.chatgpt_bridge_user_password}} -a -c /data/homeserver.yaml http://localhost:8008 cmd: docker compose exec -it synapse register_new_matrix_user -u chatgptbot -p {{applications[application_id].credentials.chatgpt_bridge_user_password}} -a -c /data/homeserver.yaml http://localhost:8008
chdir: "{{ docker_compose.directories.instance }}" chdir: "{{ docker_compose.directories.instance }}"
ignore_errors: true ignore_errors: true
when: applications.matrix.setup | bool when: applications[application_id].setup | bool

View File

@ -3,7 +3,7 @@ services:
{% include 'roles/docker-central-database/templates/services/' + database_type + '.yml.j2' %} {% include 'roles/docker-central-database/templates/services/' + database_type + '.yml.j2' %}
synapse: synapse:
image: matrixdotorg/synapse:{{applications.matrix.synapse.version}} image: matrixdotorg/synapse:{{applications[application_id].synapse.version}}
container_name: matrix-synapse container_name: matrix-synapse
restart: {{docker_restart_policy}} restart: {{docker_restart_policy}}
logging: logging:
@ -27,17 +27,16 @@ services:
retries: 3 retries: 3
{% if bridges | bool %} {% if bridges | bool %}
{% include 'templates/docker/container/depends-on-also-database.yml.j2' %} {% include 'templates/docker/container/depends-on-also-database.yml.j2' %}
{% else %}
{% include 'templates/docker/container/depends-on-just-database.yml.j2' %}
{% endif %}
{% for item in bridges %} {% for item in bridges %}
mautrix-{{item.bridge_name}}: mautrix-{{item.bridge_name}}:
condition: service_healthy condition: service_healthy
{% endfor %} {% endfor %}
{% else %}
{% include 'templates/docker/container/depends-on-just-database.yml.j2' %}
{% endif %}
{% include 'templates/docker/container/networks.yml.j2' %} {% include 'templates/docker/container/networks.yml.j2' %}
element: element:
image: vectorim/element-web:{{applications.matrix.element.version}} image: vectorim/element-web:{{applications[application_id].element.version}}
container_name: matrix-element container_name: matrix-element
restart: {{docker_restart_policy}} restart: {{docker_restart_policy}}
volumes: volumes:
@ -65,7 +64,7 @@ services:
retries: 3 retries: 3
{% include 'templates/docker/container/networks.yml.j2' %} {% include 'templates/docker/container/networks.yml.j2' %}
{% endfor %} {% endfor %}
{% if applications[application_id] | bool %} {% if applications[application_id].plugins.chatgpt | bool %}
matrix-chatgpt-bot: matrix-chatgpt-bot:
restart: {{docker_restart_policy}} restart: {{docker_restart_policy}}
container_name: matrix-chatgpt container_name: matrix-chatgpt
@ -93,13 +92,13 @@ services:
KEYV_BOT_ENCRYPTION: 'false' KEYV_BOT_ENCRYPTION: 'false'
KEYV_BOT_STORAGE: 'true' KEYV_BOT_STORAGE: 'true'
MATRIX_HOMESERVER_URL: 'https://{{domains.synapse}}' MATRIX_HOMESERVER_URL: 'https://{{domains.synapse}}'
MATRIX_BOT_USERNAME: '@chatgptbot:{{applications.matrix.server_name}}' MATRIX_BOT_USERNAME: '@chatgptbot:{{applications[application_id].server_name}}'
MATRIX_ACCESS_TOKEN: '{{ applications[application_id].credentials.chatgpt_bridge_access_token | default('') }}' MATRIX_ACCESS_TOKEN: '{{ applications[application_id].credentials.chatgpt_bridge_access_token | default('') }}'
MATRIX_BOT_PASSWORD: '{{applications[application_id].credentials.chatgpt_bridge_user_password}}' MATRIX_BOT_PASSWORD: '{{applications[application_id].credentials.chatgpt_bridge_user_password}}'
MATRIX_DEFAULT_PREFIX: '!chatgpt' MATRIX_DEFAULT_PREFIX: '!chatgpt'
MATRIX_DEFAULT_PREFIX_REPLY: 'false' MATRIX_DEFAULT_PREFIX_REPLY: 'false'
#MATRIX_BLACKLIST: '' #MATRIX_BLACKLIST: ''
MATRIX_WHITELIST: ':{{applications.matrix.server_name}}' MATRIX_WHITELIST: ':{{applications[application_id].server_name}}'
MATRIX_AUTOJOIN: 'true' MATRIX_AUTOJOIN: 'true'
MATRIX_ENCRYPTION: 'true' MATRIX_ENCRYPTION: 'true'
MATRIX_THREADS: 'true' MATRIX_THREADS: 'true'
@ -109,6 +108,8 @@ services:
{% include 'templates/docker/compose/volumes.yml.j2' %} {% include 'templates/docker/compose/volumes.yml.j2' %}
synapse_data: synapse_data:
# chatgpt_data: {% if applications[application_id].plugins.chatgpt | bool %}
chatgpt_data:
{% endif %}
{% include 'templates/docker/compose/networks.yml.j2' %} {% include 'templates/docker/compose/networks.yml.j2' %}

View File

@ -3,7 +3,7 @@ homeserver:
# The address that this appservice can use to connect to the homeserver. # The address that this appservice can use to connect to the homeserver.
address: http://synapse:8008 address: http://synapse:8008
# The domain of the homeserver (for MXIDs, etc). # The domain of the homeserver (for MXIDs, etc).
domain: {{applications.matrix.server_name}} domain: {{applications[application_id].server_name}}
# Whether or not to verify the SSL certificate of the homeserver. # Whether or not to verify the SSL certificate of the homeserver.
# Only applies if address starts with https:// # Only applies if address starts with https://
verify_ssl: true verify_ssl: true
@ -143,7 +143,7 @@ bridge:
sync_direct_chat_list: false sync_direct_chat_list: false
# Servers to always allow double puppeting from # Servers to always allow double puppeting from
double_puppet_server_map: double_puppet_server_map:
{{applications.matrix.server_name}}: {{domains.synapse}} {{applications[application_id].server_name}}: {{domains.synapse}}
# Allow using double puppeting from any server with a valid client .well-known file. # Allow using double puppeting from any server with a valid client .well-known file.
double_puppet_allow_discovery: false double_puppet_allow_discovery: false
# Shared secrets for https://github.com/devture/matrix-synapse-shared-secret-auth # Shared secrets for https://github.com/devture/matrix-synapse-shared-secret-auth
@ -154,7 +154,7 @@ bridge:
# If using this for other servers than the bridge's server, # If using this for other servers than the bridge's server,
# you must also set the URL in the double_puppet_server_map. # you must also set the URL in the double_puppet_server_map.
login_shared_secret_map: login_shared_secret_map:
{{applications.matrix.server_name}}: {{applications[application_id].credentials.registration_shared_secret}} {{applications[application_id].server_name}}: {{applications[application_id].credentials.registration_shared_secret}}
# Should presence from Facebook be bridged? This doesn't use the same API as the Android app, # Should presence from Facebook be bridged? This doesn't use the same API as the Android app,
# so it might be more suspicious to Facebook. # so it might be more suspicious to Facebook.
presence_from_facebook: false presence_from_facebook: false
@ -380,8 +380,8 @@ bridge:
# mxid - Specific user # mxid - Specific user
permissions: permissions:
"*": relay "*": relay
"{{applications.matrix.server_name}}": user "{{applications[application_id].server_name}}": user
"@{{applications.matrix.users.administrator.username}}:{{applications.matrix.server_name}}": admin "@{{applications[application_id].users.administrator.username}}:{{applications[application_id].server_name}}": admin
relay: relay:
# Whether relay mode should be allowed. If allowed, `!fb set-relay` can be used to turn any # Whether relay mode should be allowed. If allowed, `!fb set-relay` can be used to turn any

View File

@ -10,7 +10,7 @@ homeserver:
# How often should the websocket be pinged? Pinging will be disabled if this is zero. # How often should the websocket be pinged? Pinging will be disabled if this is zero.
ping_interval_seconds: 0 ping_interval_seconds: 0
# The domain of the homeserver (also known as server_name, used for MXIDs, etc). # The domain of the homeserver (also known as server_name, used for MXIDs, etc).
domain: {{applications.matrix.server_name}} domain: {{applications[application_id].server_name}}
# What software is the homeserver running? # What software is the homeserver running?
# Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use "standard" here. # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use "standard" here.

View File

@ -3,7 +3,7 @@ homeserver:
# The address that this appservice can use to connect to the homeserver. # The address that this appservice can use to connect to the homeserver.
address: http://synapse:8008 address: http://synapse:8008
# The domain of the homeserver (also known as server_name, used for MXIDs, etc). # The domain of the homeserver (also known as server_name, used for MXIDs, etc).
domain: {{applications.matrix.server_name}} domain: {{applications[application_id].server_name}}
# Whether or not to verify the SSL certificate of the homeserver. # Whether or not to verify the SSL certificate of the homeserver.
# Only applies if address starts with https:// # Only applies if address starts with https://
verify_ssl: true verify_ssl: true
@ -134,7 +134,7 @@ bridge:
double_puppet_allow_discovery: false double_puppet_allow_discovery: false
# Servers to allow double puppeting from, even if double_puppet_allow_discovery is false. # Servers to allow double puppeting from, even if double_puppet_allow_discovery is false.
double_puppet_server_map: double_puppet_server_map:
{{applications.matrix.server_name}}: https://{{domains.synapse}} {{applications[application_id].server_name}}: https://{{domains.synapse}}
# Shared secret for https://github.com/devture/matrix-synapse-shared-secret-auth # Shared secret for https://github.com/devture/matrix-synapse-shared-secret-auth
# #
# If set, custom puppets will be enabled automatically for local users # If set, custom puppets will be enabled automatically for local users
@ -143,7 +143,7 @@ bridge:
# If using this for other servers than the bridge's server, # If using this for other servers than the bridge's server,
# you must also set the URL in the double_puppet_server_map. # you must also set the URL in the double_puppet_server_map.
login_shared_secret_map: login_shared_secret_map:
{{applications.matrix.server_name}}: {{applications[application_id].credentials.registration_shared_secret}} {{applications[application_id].server_name}}: {{applications[application_id].credentials.registration_shared_secret}}
# Whether or not created rooms should have federation enabled. # Whether or not created rooms should have federation enabled.
# If false, created portal rooms will never be federated. # If false, created portal rooms will never be federated.
federate_rooms: true federate_rooms: true
@ -359,8 +359,8 @@ bridge:
# mxid - Specific user # mxid - Specific user
permissions: permissions:
"*": relay "*": relay
"{{applications.matrix.server_name}}": user "{{applications[application_id].server_name}}": user
"@{{applications.matrix.users.administrator.username}}:{{applications.matrix.server_name}}": admin "@{{applications[application_id].users.administrator.username}}:{{applications[application_id].server_name}}": admin
relay: relay:
# Whether relay mode should be allowed. If allowed, `!ig set-relay` can be used to turn any # Whether relay mode should be allowed. If allowed, `!ig set-relay` can be used to turn any

View File

@ -3,7 +3,7 @@ homeserver:
# The address that this appservice can use to connect to the homeserver. # The address that this appservice can use to connect to the homeserver.
address: http://synapse:8008 address: http://synapse:8008
# The domain of the homeserver (also known as server_name, used for MXIDs, etc). # The domain of the homeserver (also known as server_name, used for MXIDs, etc).
domain: {{applications.matrix.server_name}} domain: {{applications[application_id].server_name}}
# What software is the homeserver running? # What software is the homeserver running?
# Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use "standard" here. # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use "standard" here.
@ -141,7 +141,7 @@ bridge:
federate_rooms: true federate_rooms: true
# Servers to always allow double puppeting from # Servers to always allow double puppeting from
double_puppet_server_map: double_puppet_server_map:
{{applications.matrix.server_name}}: https://{{domains.synapse}} {{applications[application_id].server_name}}: https://{{domains.synapse}}
# Allow using double puppeting from any server with a valid client .well-known file. # Allow using double puppeting from any server with a valid client .well-known file.
double_puppet_allow_discovery: false double_puppet_allow_discovery: false
# Shared secrets for https://github.com/devture/matrix-synapse-shared-secret-auth # Shared secrets for https://github.com/devture/matrix-synapse-shared-secret-auth
@ -150,7 +150,7 @@ bridge:
# instead of users having to find an access token and run `login-matrix` # instead of users having to find an access token and run `login-matrix`
# manually. # manually.
login_shared_secret_map: login_shared_secret_map:
{{applications.matrix.server_name}}: {{applications[application_id].credentials.registration_shared_secret}} {{applications[application_id].server_name}}: {{applications[application_id].credentials.registration_shared_secret}}
# Maximum time for handling Matrix events. Duration strings formatted for https://pkg.go.dev/time#ParseDuration # Maximum time for handling Matrix events. Duration strings formatted for https://pkg.go.dev/time#ParseDuration
# Null means there's no enforced timeout. # Null means there's no enforced timeout.
@ -274,8 +274,8 @@ bridge:
# mxid - Specific user # mxid - Specific user
permissions: permissions:
"*": relay "*": relay
"{{applications.matrix.server_name}}": user "{{applications[application_id].server_name}}": user
"@{{applications.matrix.users.administrator.username}}:{{applications.matrix.server_name}}": admin "@{{applications[application_id].users.administrator.username}}:{{applications[application_id].server_name}}": admin
# Settings for relay mode # Settings for relay mode
relay: relay:

View File

@ -3,7 +3,7 @@ homeserver:
# The address that this appservice can use to connect to the homeserver. # The address that this appservice can use to connect to the homeserver.
address: http://synapse:8008 address: http://synapse:8008
# The domain of the homeserver (also known as server_name, used for MXIDs, etc). # The domain of the homeserver (also known as server_name, used for MXIDs, etc).
domain: {{applications.matrix.server_name}} domain: {{applications[application_id].server_name}}
# What software is the homeserver running? # What software is the homeserver running?
# Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use "standard" here. # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use "standard" here.
@ -118,7 +118,7 @@ bridge:
# Servers to always allow double puppeting from # Servers to always allow double puppeting from
double_puppet_server_map: double_puppet_server_map:
{{applications.matrix.server_name}}: https://{{domains.synapse}} {{applications[application_id].server_name}}: https://{{domains.synapse}}
# Allow using double puppeting from any server with a valid client .well-known file. # Allow using double puppeting from any server with a valid client .well-known file.
double_puppet_allow_discovery: false double_puppet_allow_discovery: false
# Shared secrets for https://github.com/devture/matrix-synapse-shared-secret-auth # Shared secrets for https://github.com/devture/matrix-synapse-shared-secret-auth
@ -127,7 +127,7 @@ bridge:
# instead of users having to find an access token and run `login-matrix` # instead of users having to find an access token and run `login-matrix`
# manually. # manually.
login_shared_secret_map: login_shared_secret_map:
{{applications.matrix.server_name}}: {{applications[application_id].credentials.registration_shared_secret}} {{applications[application_id].server_name}}: {{applications[application_id].credentials.registration_shared_secret}}
message_handling_timeout: message_handling_timeout:
# Send an error message after this timeout, but keep waiting for the response until the deadline. # Send an error message after this timeout, but keep waiting for the response until the deadline.
@ -278,8 +278,8 @@ bridge:
# mxid - Specific user # mxid - Specific user
permissions: permissions:
"*": relay "*": relay
"{{applications.matrix.server_name}}": user "{{applications[application_id].server_name}}": user
"@{{applications.matrix.users.administrator.username}}:{{applications.matrix.server_name}}": admin "@{{applications[application_id].users.administrator.username}}:{{applications[application_id].server_name}}": admin
# Logging config. See https://github.com/tulir/zeroconfig for details. # Logging config. See https://github.com/tulir/zeroconfig for details.
logging: logging:

View File

@ -3,7 +3,7 @@ homeserver:
# The address that this appservice can use to connect to the homeserver. # The address that this appservice can use to connect to the homeserver.
address: http://synapse:8008 address: http://synapse:8008
# The domain of the homeserver (for MXIDs, etc). # The domain of the homeserver (for MXIDs, etc).
domain: {{applications.matrix.server_name}} domain: {{applications[application_id].server_name}}
# Whether or not to verify the SSL certificate of the homeserver. # Whether or not to verify the SSL certificate of the homeserver.
# Only applies if address starts with https:// # Only applies if address starts with https://
verify_ssl: true verify_ssl: true
@ -62,7 +62,7 @@ appservice:
prefix: /public prefix: /public
# The base URL where the public-facing endpoints are available. The prefix is not added # The base URL where the public-facing endpoints are available. The prefix is not added
# implicitly. # implicitly.
external: https://{{applications.matrix.server_name}}/public external: https://{{applications[application_id].server_name}}/public
# Provisioning API part of the web server for automated portal creation and fetching information. # Provisioning API part of the web server for automated portal creation and fetching information.
# Used by things like mautrix-manager (https://github.com/tulir/mautrix-manager). # Used by things like mautrix-manager (https://github.com/tulir/mautrix-manager).
@ -198,7 +198,7 @@ bridge:
sync_direct_chat_list: false sync_direct_chat_list: false
# Servers to always allow double puppeting from # Servers to always allow double puppeting from
double_puppet_server_map: double_puppet_server_map:
{{applications.matrix.server_name}}: https://{{domains.synapse}} {{applications[application_id].server_name}}: https://{{domains.synapse}}
# Allow using double puppeting from any server with a valid client .well-known file. # Allow using double puppeting from any server with a valid client .well-known file.
double_puppet_allow_discovery: false double_puppet_allow_discovery: false
# Shared secrets for https://github.com/devture/matrix-synapse-shared-secret-auth # Shared secrets for https://github.com/devture/matrix-synapse-shared-secret-auth
@ -209,7 +209,7 @@ bridge:
# If using this for other servers than the bridge's server, # If using this for other servers than the bridge's server,
# you must also set the URL in the double_puppet_server_map. # you must also set the URL in the double_puppet_server_map.
login_shared_secret_map: login_shared_secret_map:
{{applications.matrix.server_name}}: {{applications[application_id].credentials.registration_shared_secret}} {{applications[application_id].server_name}}: {{applications[application_id].credentials.registration_shared_secret}}
# Set to false to disable link previews in messages sent to Telegram. # Set to false to disable link previews in messages sent to Telegram.
telegram_link_preview: true telegram_link_preview: true
# Whether or not the !tg join command should do a HTTP request # Whether or not the !tg join command should do a HTTP request
@ -530,9 +530,9 @@ bridge:
# mxid - Specific user # mxid - Specific user
permissions: permissions:
"*": "relaybot" "*": "relaybot"
"public.{{applications.matrix.server_name}}": "user" "public.{{applications[application_id].server_name}}": "user"
"{{applications.matrix.server_name}}": "full" "{{applications[application_id].server_name}}": "full"
"@{{applications.matrix.users.administrator.username}}:{{applications.matrix.server_name}}": "admin" "@{{applications[application_id].users.administrator.username}}:{{applications[application_id].server_name}}": "admin"
# Options related to the message relay Telegram bot. # Options related to the message relay Telegram bot.
relaybot: relaybot:

View File

@ -3,7 +3,7 @@ homeserver:
# The address that this appservice can use to connect to the homeserver. # The address that this appservice can use to connect to the homeserver.
address: http://synapse:8008 address: http://synapse:8008
# The domain of the homeserver (also known as server_name, used for MXIDs, etc). # The domain of the homeserver (also known as server_name, used for MXIDs, etc).
domain: {{applications.matrix.server_name}} domain: {{applications[application_id].server_name}}
# What software is the homeserver running? # What software is the homeserver running?
# Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use "standard" here. # Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use "standard" here.
@ -236,7 +236,7 @@ bridge:
force_active_delivery_receipts: false force_active_delivery_receipts: false
# Servers to always allow double puppeting from # Servers to always allow double puppeting from
double_puppet_server_map: double_puppet_server_map:
{{applications.matrix.server_name}}: https://{{domains.synapse}} {{applications[application_id].server_name}}: https://{{domains.synapse}}
# Allow using double puppeting from any server with a valid client .well-known file. # Allow using double puppeting from any server with a valid client .well-known file.
double_puppet_allow_discovery: false double_puppet_allow_discovery: false
# Shared secrets for https://github.com/devture/matrix-synapse-shared-secret-auth # Shared secrets for https://github.com/devture/matrix-synapse-shared-secret-auth
@ -245,7 +245,7 @@ bridge:
# instead of users having to find an access token and run `login-matrix` # instead of users having to find an access token and run `login-matrix`
# manually. # manually.
login_shared_secret_map: login_shared_secret_map:
{{applications.matrix.server_name}}: {{applications[application_id].credentials.registration_shared_secret}} {{applications[application_id].server_name}}: {{applications[application_id].credentials.registration_shared_secret}}
# Whether to explicitly set the avatar and room name for private chat portal rooms. # Whether to explicitly set the avatar and room name for private chat portal rooms.
# If set to `default`, this will be enabled in encrypted rooms and disabled in unencrypted rooms. # If set to `default`, this will be enabled in encrypted rooms and disabled in unencrypted rooms.
# If set to `always`, all DM rooms will have explicit names and avatars set. # If set to `always`, all DM rooms will have explicit names and avatars set.
@ -434,8 +434,8 @@ bridge:
# mxid - Specific user # mxid - Specific user
permissions: permissions:
"*": relay "*": relay
"{{applications.matrix.server_name}}": user "{{applications[application_id].server_name}}": user
"@{{applications.matrix.users.administrator.username}}:{{applications.matrix.server_name}}": admin "@{{applications[application_id].users.administrator.username}}:{{applications[application_id].server_name}}": admin
# Settings for relay mode # Settings for relay mode
relay: relay:

View File

@ -1,4 +1,4 @@
server_name: "{{applications.matrix.server_name}}" server_name: "{{applications[application_id].server_name}}"
pid_file: /data/homeserver.pid pid_file: /data/homeserver.pid
listeners: listeners:
- port: 8008 - port: 8008

View File

@ -10,7 +10,7 @@ element:
version: "latest" version: "latest"
setup: false # Set true in inventory file to execute the setup and initializing procedures setup: false # Set true in inventory file to execute the setup and initializing procedures
features: features:
matomo: true matomo: false # Deactivated, because in html CSP restricts use
css: true css: true
portfolio_iframe: false portfolio_iframe: false
oidc: true # Deactivated OIDC due to this issue https://github.com/matrix-org/synapse/issues/10492 oidc: true # Deactivated OIDC due to this issue https://github.com/matrix-org/synapse/issues/10492
@ -25,9 +25,9 @@ csp:
whitelist: whitelist:
connect-src: connect-src:
- "{{ primary_domain }}" - "{{ primary_domain }}"
- "{{ domains.synapse }}" - "{{ domains.matrix }}"
script-src: script-src:
- "{{ domains.synapse }}" - "{{ domains.matrix }}"
- "https://cdn.jsdelivr.net" - "https://cdn.jsdelivr.net"
plugins: plugins:
# You need to enable them in the inventory file # You need to enable them in the inventory file
@ -39,4 +39,3 @@ plugins:
slack: false slack: false
telegram: false telegram: false
whatsapp: false whatsapp: false

View File

@ -3,7 +3,6 @@ credentials:
description: "Password for the Moodle database user" description: "Password for the Moodle database user"
algorithm: "bcrypt" algorithm: "bcrypt"
validation: "^\\$2[aby]\\$.{56}$" validation: "^\\$2[aby]\\$.{56}$"
user_password: user_password:
description: "Initial password for the Moodle admin user" description: "Initial password for the Moodle admin user"
algorithm: "sha256" algorithm: "sha256"

View File

@ -10,5 +10,15 @@
domain: "{{ domains[application_id] }}" domain: "{{ domains[application_id] }}"
http_port: "{{ ports.localhost.http[application_id] }}" http_port: "{{ ports.localhost.http[application_id] }}"
- name: "Transfer Dockerfile to {{ docker_compose.directories.instance }}"
template:
src: Dockerfile.j2
dest: "{{ docker_compose.directories.instance }}Dockerfile"
notify: docker compose project build and setup
- name: "copy docker-compose.yml and env file" - name: "copy docker-compose.yml and env file"
include_tasks: copy-docker-compose-and-env.yml include_tasks: copy-docker-compose-and-env.yml
- name: "Configure OIDC login for Moodle if enabled"
include_tasks: oidc.yml
when: applications | is_feature_enabled('oidc',application_id)

View File

@ -0,0 +1,24 @@
---
- name: "Upgrade Moodle to apply OIDC plugin"
command: "docker exec {{ container_name }} php admin/cli/upgrade.php --non-interactive"
- name: "Set Moodle OIDC configuration via CLI"
loop:
- { name: "issuerurl", value: "{{ oidc.client.issuer_url }}" }
- { name: "clientid", value: "{{ oidc.client.id }}" }
- { name: "clientsecret", value: "{{ oidc.client.secret }}" }
- { name: "authmethod", value: "oidc" }
- { name: "loginflow", value: "authorization_code" }
- { name: "idpname", value: "Keycloak" }
- { name: "scopes", value: "openid profile email" }
- { name: "authenticationendpoint", value: "{{ oidc.client.authorize_url }}" }
- { name: "tokenendpoint", value: "{{ oidc.client.token_url }}" }
- { name: "userinfoendpoint", value: "{{ oidc.client.user_info_url }}" }
loop_control:
label: "{{ item.name }}"
command: >
docker exec {{ container_name }} php admin/cli/cfg.php --component=auth_oidc
--name={{ item.name }} --set="{{ item.value }}"
- name: "Enable OIDC login"
command: "docker exec {{ container_name }} php admin/cli/cfg.php --name=auth --set=oidc"

View File

@ -0,0 +1,14 @@
FROM bitnami/moodle:{{ applications[application_id].version }}
{% if applications | is_feature_enabled('oidc',application_id) %}
# Install git (required to clone the OIDC plugin)
USER root
RUN install_packages git unzip
# Clone the Microsoft OIDC plugin into Moodle's auth directory
RUN git clone https://github.com/microsoft/moodle-auth_oidc.git \
/opt/bitnami/moodle/auth/oidc && \
chown -R www-data:www-data /opt/bitnami/moodle/auth/oidc
USER 1001
{% endif %}

View File

@ -2,8 +2,11 @@ services:
{% include 'roles/docker-central-database/templates/services/' + database_type + '.yml.j2' %} {% include 'roles/docker-central-database/templates/services/' + database_type + '.yml.j2' %}
moodle: moodle:
container_name: moodle container_name: {{ container_name }}
image: docker.io/bitnami/moodle:{{applications.moodle.version}} build:
context: .
dockerfile: Dockerfile
image: moodle_custom
ports: ports:
- 127.0.0.1:{{ports.localhost.http[application_id]}}:8080 - 127.0.0.1:{{ports.localhost.http[application_id]}}:8080
{% include 'roles/docker-compose/templates/services/base.yml.j2' %} {% include 'roles/docker-compose/templates/services/base.yml.j2' %}

View File

@ -4,10 +4,10 @@ MOODLE_DATABASE_USER={{database_username}}
MOODLE_DATABASE_NAME={{database_name}} MOODLE_DATABASE_NAME={{database_name}}
MOODLE_DATABASE_PASSWORD={{database_password}} MOODLE_DATABASE_PASSWORD={{database_password}}
ALLOW_EMPTY_PASSWORD=no ALLOW_EMPTY_PASSWORD=no
MOODLE_SITE_NAME="{{applications.moodle.site_titel}}" MOODLE_SITE_NAME="{{applications[application_id].site_titel}}"
MOODLE_SSLPROXY=yes MOODLE_SSLPROXY=yes
MOODLE_REVERSE_PROXY=yes MOODLE_REVERSE_PROXY=yes
MOODLE_USERNAME={{applications.moodle.administrator_name}} MOODLE_USERNAME={{applications[application_id].users.administrator.username}}
MOODLE_PASSWORD={{applications[application_id].credentials.user_password}} MOODLE_PASSWORD={{applications[application_id].credentials.user_password}}
MOODLE_EMAIL={{applications.moodle.users.administrator.email}} MOODLE_EMAIL={{applications[application_id].users.administrator.email}}
BITNAMI_DEBUG={% if enable_debug | bool %}true{% else %}false{% endif %} BITNAMI_DEBUG={% if enable_debug | bool %}true{% else %}false{% endif %}

View File

@ -9,6 +9,7 @@ features:
css: false css: false
portfolio_iframe: false portfolio_iframe: false
central_database: true central_database: true
oidc: false
csp: csp:
flags: flags:
script-src: script-src:

View File

@ -1,3 +1,4 @@
--- ---
application_id: "moodle" application_id: "moodle"
database_type: "mariadb" database_type: "mariadb"
container_name: "{{ application_id }}"

View File

@ -5,7 +5,7 @@
- name: "include create-domains.yml for peertube" - name: "include create-domains.yml for peertube"
include_tasks: create-domains.yml include_tasks: create-domains.yml
loop: "{{ [domains.peertube] + domains.peertube_alternates }}" loop: "{{ domains.peertube }}"
loop_control: loop_control:
loop_var: domain loop_var: domain
vars: vars:

View File

@ -2,6 +2,7 @@
- name: "include docker-compose role" - name: "include docker-compose role"
include_role: include_role:
name: docker-compose name: docker-compose
when: run_once_docker_portfolio is not defined
- name: "include role nginx-domain-setup for {{application_id}}" - name: "include role nginx-domain-setup for {{application_id}}"
include_role: include_role:
@ -9,10 +10,12 @@
vars: vars:
domain: "{{ domains[application_id] }}" domain: "{{ domains[application_id] }}"
http_port: "{{ ports.localhost.http[application_id] }}" http_port: "{{ ports.localhost.http[application_id] }}"
when: run_once_docker_portfolio is not defined
- name: "include role docker-repository-setup for {{application_id}}" - name: "include role docker-repository-setup for {{application_id}}"
include_role: include_role:
name: docker-repository-setup name: docker-repository-setup
when: run_once_docker_portfolio is not defined
- name: "Check if host-specific config.yaml exists in {{ config_inventory_path }}" - name: "Check if host-specific config.yaml exists in {{ config_inventory_path }}"
stat: stat:
@ -20,18 +23,22 @@
delegate_to: localhost delegate_to: localhost
become: false become: false
register: config_file register: config_file
when: run_once_docker_portfolio is not defined
- name: Load menu categories - name: Load menu categories
include_vars: include_vars:
file: "menu_categories.yml" file: "menu_categories.yml"
when: run_once_docker_portfolio is not defined
- name: Load docker cards - name: Load docker cards
set_fact: set_fact:
portfolio_cards: "{{ lookup('docker_cards', 'roles') }}" portfolio_cards: "{{ lookup('docker_cards', 'roles') }}"
when: run_once_docker_portfolio is not defined
- name: Group docker cards - name: Group docker cards
set_fact: set_fact:
portfolio_menu_data: "{{ lookup('docker_cards_grouped', portfolio_cards, portfolio_menu_categories) }}" portfolio_menu_data: "{{ lookup('docker_cards_grouped', portfolio_cards, portfolio_menu_categories) }}"
when: run_once_docker_portfolio is not defined
- name: Debug portfolio data - name: Debug portfolio data
debug: debug:
@ -39,24 +46,36 @@
portfolio_cards: "{{ portfolio_cards }}" portfolio_cards: "{{ portfolio_cards }}"
portfolio_menu_categories: "{{ portfolio_menu_categories}}" portfolio_menu_categories: "{{ portfolio_menu_categories}}"
portfolio_menu_data: "{{ portfolio_menu_data }}" portfolio_menu_data: "{{ portfolio_menu_data }}"
when: enable_debug | bool when:
- enable_debug | bool
- run_once_docker_portfolio is not defined
- name: Copy host-specific config.yaml if it exists - name: Copy host-specific config.yaml if it exists
template: template:
src: "{{ config_inventory_path }}" src: "{{ config_inventory_path }}"
dest: "{{docker_repository_path}}/app/config.yaml" dest: "{{docker_repository_path}}/app/config.yaml"
notify: docker compose project setup notify: docker compose project setup
when: config_file.stat.exists when:
- config_file.stat.exists
- run_once_docker_portfolio is not defined
- name: Copy default config.yaml from the role template if host-specific file does not exist - name: Copy default config.yaml from the role template if host-specific file does not exist
template: template:
src: "config.yaml.j2" src: "config.yaml.j2"
dest: "{{docker_repository_path}}/app/config.yaml" dest: "{{docker_repository_path}}/app/config.yaml"
notify: docker compose project setup notify: docker compose project setup
when: not config_file.stat.exists when:
- not config_file.stat.exists
- run_once_docker_portfolio is not defined
- name: add docker-compose.yml - name: add docker-compose.yml
template: template:
src: docker-compose.yml.j2 src: docker-compose.yml.j2
dest: "{docker_compose.directories.instance}}docker-compose.yml" dest: "{docker_compose.directories.instance}}docker-compose.yml"
notify: docker compose project setup notify: docker compose project setup
when: run_once_docker_portfolio is not defined
- name: run the portfolio tasks once
set_fact:
run_once_docker_portfolio: true
when: run_once_docker_portfolio is not defined

View File

@ -29,7 +29,7 @@ class TestGenerateDefaultApplications(unittest.TestCase):
shutil.rmtree(self.temp_dir) shutil.rmtree(self.temp_dir)
def test_script_generates_expected_yaml(self): def test_script_generates_expected_yaml(self):
script_path = Path(__file__).resolve().parent.parent.parent / "cli" / "generate_defaults_applications.py" script_path = Path(__file__).resolve().parent.parent.parent / "cli" / "generate-applications-defaults.py"
result = subprocess.run( result = subprocess.run(
[ [

View File

@ -7,7 +7,7 @@ PROJECT_ROOT = Path(__file__).parent.parent.parent.resolve()
sys.path.insert(0, str(PROJECT_ROOT)) sys.path.insert(0, str(PROJECT_ROOT))
# 2) Import from the cli package # 2) Import from the cli package
import cli.generate_vaulted_credentials as gvc import cli.create_credentials as gvc
class DummyProc: class DummyProc:
def __init__(self, returncode, stdout, stderr=''): def __init__(self, returncode, stdout, stderr=''):

View File

@ -0,0 +1,51 @@
import unittest
from filter_plugins.group_domain_filters import FilterModule
class TestAddDomainIfGroup(unittest.TestCase):
def setUp(self):
self.filter = FilterModule().filters()["add_domain_if_group"]
def test_add_string_value(self):
result = self.filter({}, "akaunting", "accounting.example.org", ["akaunting"])
self.assertEqual(result, {"akaunting": "accounting.example.org"})
def test_add_list_value(self):
result = self.filter({}, "mastodon", ["microblog.example.org"], ["mastodon"])
self.assertEqual(result, {"mastodon": ["microblog.example.org"]})
def test_add_dict_value(self):
result = self.filter({}, "bluesky", {"web": "bskyweb.example.org", "api": "bluesky.example.org"}, ["bluesky"])
self.assertEqual(result, {"bluesky": {"web": "bskyweb.example.org", "api": "bluesky.example.org"}})
def test_ignore_if_not_in_group(self):
result = self.filter({}, "akaunting", "accounting.example.org", ["wordpress"])
self.assertEqual(result, {})
def test_merge_with_existing(self):
initial = {"wordpress": ["blog.example.org"]}
result = self.filter(initial, "akaunting", "accounting.example.org", ["akaunting"])
self.assertEqual(result, {
"wordpress": ["blog.example.org"],
"akaunting": "accounting.example.org"
})
def test_dict_is_not_mutated(self):
base = {"keycloak": "auth.example.org"}
copy = dict(base) # make a copy for comparison
_ = self.filter(base, "akaunting", "accounting.example.org", ["akaunting"])
self.assertEqual(base, copy) # original must stay unchanged
def test_multiple_adds_accumulate(self):
result = {}
result = self.filter(result, "akaunting", "accounting.example.org", ["akaunting", "wordpress"])
result = self.filter(result, "wordpress", ["blog.example.org"], ["akaunting", "wordpress"])
result = self.filter(result, "bluesky", {"web": "bskyweb.example.org", "api": "bluesky.example.org"}, ["bluesky"])
self.assertEqual(result, {
"akaunting": "accounting.example.org",
"wordpress": ["blog.example.org"],
"bluesky": {"web": "bskyweb.example.org", "api": "bluesky.example.org"},
})
if __name__ == "__main__":
unittest.main()